大文件上传oss

67 阅读2分钟

一、整体功能概述

这个文件封装了一个阿里云 OSS 上传工具类 

Upload,主要能力有:

  • 初始化 OSS 客户端(带 STS token 自动刷新)
  • 支持小文件直接上传put
  • 支持大文件分片上传 + 断点续传multipartUpload + localStorage 记录 checkpoint)
  • 支持取消上传
  • 检查断点是否在 OSS 上仍有效listUploads
  • 根据 OSS 对象路径生成带签名的下载链接,支持触发浏览器下载
  • 提供一个简化入口 getUploadInstance(busiType, isCostRight) 返回已初始化好的实例

二、具体实现

import { getOssSign } from '@/api/common'; const nanoid = require('nanoid'); import { useUserStoreWithOut } from '@/store'; import { format } from 'date-fns';

const defaultConf = { region: 'oss-cn-hangzhou', //todo 是否要改名 bucket: 'yx-local', }; const dateStr = format(new Date(), 'yyyy_MM_dd');

const tenantId = useUserStoreWithOut().vuex_tenantId ||localStorage.getItem('vuex_tenantId');

const env = process.env.npm_config_env || 'test';

const buckPathBase = my/${env}/${tenantId}/${dateStr}/;

const uploadOptions = {

parallel: 5,// 设置并发上传的分片数量。
partSize: 2 * 1024 * 1024,// 设置分片大小。默认值为1 MB,最小值为100 KB。
// headers,
// 自定义元数据,通过HeadObject接口可以获取Object的元数据。

};

export default class Upload {

constructor(options) {
	const { busiType, isCostRight } = options;
	if (!busiType) {
		throw new Error('请传入业务类型');
	}
	const bucketPath = `${buckPathBase}${busiType}/`;
            
	this.options = { ...defaultConf, ...options, bucketPath, isCostRight };
}
// 初始化oss实例
async initOssClient() {
	try {
		const accessConf = await this.getStsToken();
		const { region, bucket } = this.options;
		this.client = new OSS({
			// yourRegion填写Bucket所在地域。以华东1(杭州)为例,yourRegion填写为oss-cn-hangzhou。
			region,
			bucket,
			timeout: 120000, // 2min
			...accessConf,
			refreshSTSToken: async () => {
				return await this.getStsToken();
			},
			refreshSTSTokenInterval: 300000, // 5min刷一次
		});
	} catch (error) {
		console.log('初始化client oss失败', error);
		this.client = null;
	}
}
async getStsToken() {
	const { accessKeyId, accessKeySecret, securityToken } = await getOssSign({
		isUse: this.options.isCostRight,
	});
	return {
		accessKeyId: accessKeyId,
		accessKeySecret: accessKeySecret,
		// 从STS服务获取的安全令牌(SecurityToken)。
		stsToken: securityToken,
	};
}
// 取消上传
cancel() {
	this.client.cancel();
}
/**
 *
 * @param {File} file 待上传的文件
 * @param {object} param1 上传配置信息
 * @param {number} retryTimes 可重试上传次数
 * @returns
 */
getFileExtension(file) {
	const { name, type } = file;
	const dotIndex = name.lastIndexOf('.');
	if (dotIndex === -1 || dotIndex === 0) return type?.split('/')?.[1]; // 没有后缀或文件名以点开头
	return name.substring(dotIndex + 1);
}

async upload(file, { progressCb, ...restOption }) {
	const extName = this.getFileExtension(file);
	const uniquKey = nanoid.nanoid(16);
	let bucketUniqueFileName = `${uniquKey}.${extName}`;
	if (file.size < uploadOptions.partSize) {
		if (restOption?.customPath) {
			//自定义上传路径
			return await this.commonUpload(file, this.options.bucketPath + restOption.customPath);
		} else {
			return await this.commonUpload(file, this.options.bucketPath + bucketUniqueFileName);
		}
	} else {
		return new Promise(async (resolve, reject) => {
			try {
				const storageKey = `${file.name}-${file.lastModified}-${file.size}`;
				const option = {
					...uploadOptions,
					...restOption,
					progress: (p, cpt, res) => {
						// 缓存记录每个断点分片信息。
						const checkpointMapInfo = {
							bucketUniqueFileName,
							cpt,
						};
						// todo 设置localstorage过期时间
						localStorage.setItem(storageKey, JSON.stringify(checkpointMapInfo));
						// 上传完成之后晴空断点信息。
						if (p * 100 == 100) {
							localStorage.removeItem(storageKey);
						}
						!!progressCb && progressCb(p * 100);
					},
				};
				if (localStorage.getItem(storageKey)) {
					// 命中断点续传缓存
					const checkpointMapInfo = JSON.parse(localStorage.getItem(storageKey));
					bucketUniqueFileName = checkpointMapInfo.bucketUniqueFileName;
					const hasUpload = await this.checkOssHasUploads(checkpointMapInfo?.cpt?.uploadId);
					if (hasUpload) {
						// 注意:碎片过期时间是3天,如果当前断点信息无对应上面id, 则重新上传
						option.checkpoint = checkpointMapInfo.cpt;
					}
				}
				let loadPath = this.options.bucketPath + bucketUniqueFileName;
				if (option.customPath) {
					//自定义上传路径
					loadPath = this.options.bucketPath + option.customPath;
				}
				this.client
					.multipartUpload(loadPath, file, option)
					.then(res => {
						resolve(res);
					})
					.catch(e => {
						console.log(e, 777777);
						Object.keys(e).forEach(key => {
							console.log(e[key], key);
						});
						if (e.name.indexOf('ConnectionTimeoutError') > -1 || e.name.indexOf('RequestError') > -1) {
							reject({
								message: '网络超时或者中断,请检查当前网络',
								type: 'timeout',
							});
						} else if (e.name.indexOf('abort') > -1) {
							localStorage.removeItem(storageKey);
							reject({
								message: '上传过程出现异常中断,请重试',
								type: 'abort',
							});
						}
					});
			} catch (error) {
				console.log(error);
			}
		});
	}
}
// 普通上传
async commonUpload(file, bucketPath) {
	return new Promise((resolve, reject) => {
		this.client
			.put(bucketPath, file)
			.then(res => {
				resolve(res);
			})
			.catch(e => {
				console.log(e, '===commonUpload====');
				if (e.name.indexOf('ConnectionTimeoutError') > -1) {
					reject({
						message: '网络超时或者中断,请检查当前网络',
						type: 'timeout',
					});
				}
			});
	});
}
/**
 *
 * @param {number} uploadId 上传文件id
 * @returns oss上是否有对应(断点)碎片信息
 */
async checkOssHasUploads(uploadId) {
	try {
		const result = await this.client.listUploads({
			'max-uploads': 1,
			'upload-id-marker': uploadId,
		});
		return result.uploads?.length > 0;
	} catch (error) {
		console.log(error);
		return false;
	}
}
// 下载文件 后端存bucket路径,不存完整路径,通过签名生成完整路径(实效性),防止资源盗刷
getSignatureUrl(filePath, filename, isDownload) {
	// 配置响应头实现通过URL访问时自动下载文件,并设置下载后的文件名。
	const response = {
		'content-disposition': `attachment; filename=${encodeURIComponent(filename)}`,
	};
	// 填写Object完整路径。Object完整路径中不能包含Bucket名称。
	const url = this.client.signatureUrl(filePath, { response });
	if (isDownload) {
		const link = document.createElement('a');
		link.style.display = 'none';
		// 设置下载地址
		link.setAttribute('href', url);
		// 设置文件名
		link.setAttribute('download', filename);
		document.body.appendChild(link);
		link.click();
		document.body.removeChild(link);
	} else {
		return url;
	}
}

}

export const getUploadInstance = async (busiType, isCostRight = true) => {

const uploadInstance = new Upload({
	busiType,
	isCostRight,
});
// 工具upload类构造函器不许使用await
await uploadInstance.initOssClient();
return uploadInstance;

};