UNPKG

cos-uploaderjs-jiniu

Version:

适用于 UniApp 的腾讯 COS 文件上传封装工具

707 lines (706 loc) 28.9 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; // import SparkMD5 from 'spark-md5' import md5 from "js-md5"; import cloneDeep from "lodash/cloneDeep"; import CryptoJS from 'crypto-js'; // 可以签入签名的headers var signHeaders = [ 'cache-control', 'content-disposition', 'content-encoding', 'content-length', 'content-md5', 'expect', 'expires', 'host', 'if-match', 'if-modified-since', 'if-none-match', 'if-unmodified-since', 'origin', 'range', 'transfer-encoding', 'pic-operations', ]; /** * 获取文件的 MD5 * @param file File对象 * @returns Promise<string> md5字符串 */ function getFileMd5(file) { return new Promise((resolve, reject) => { const chunkSize = 2 * 1024 * 1024; // 每片2MB const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const loadNext = () => { const start = currentChunk * chunkSize; const end = Math.min(start + chunkSize, file.size); fileReader.readAsArrayBuffer(file.slice(start, end)); }; fileReader.onload = (e) => { spark.append(e.target.result); currentChunk++; if (currentChunk < chunks) { loadNext(); } else { resolve(spark.end()); } }; fileReader.onerror = () => reject('读取文件失败'); loadNext(); }); } function generateMd5FromUUID() { const uuid = generateUUID(); const hash = md5(uuid).toString(); return hash; } function formatFileSize(sizeInBytes) { if (sizeInBytes < 1024) { return sizeInBytes + ' B'; } else if (sizeInBytes < 1024 * 1024) { return (sizeInBytes / 1024).toFixed(2) + ' KB'; } else if (sizeInBytes < 1024 * 1024 * 1024) { return (sizeInBytes / (1024 * 1024)).toFixed(2) + ' MB'; } else { return (sizeInBytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } } export const fetchAccessInfo = (baseUrl, appChanneId) => __awaiter(void 0, void 0, void 0, function* () { try { const res = yield uni.request({ url: `${baseUrl}/ms-attachment/cos/getAccessInfo`, method: 'POST', header: { 'Content-Type': 'application/json' }, data: { _appid: appChanneId } }); const result = res.data; // 注意:直接取 .data,而不是解构 if (result.code !== 200) { console.error('获取上传信息失败:', result.msg); return null; } return result.data; } catch (err) { console.error('获取上传信息失败:', err); return null; } }); const getSignHeaderObj = (headers) => { var signHeaderObj = {}; for (var i in headers) { var key = i.toLowerCase(); if (key.indexOf('x-cos-') > -1 || key.indexOf('x-ci-') > -1 || signHeaders.indexOf(key) > -1) { signHeaderObj[i] = headers[i]; } } return signHeaderObj; }; function camSafeUrlEncode(str) { return encodeURIComponent(str) .replace(/!/g, '%21') .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29') .replace(/\*/g, '%2A'); } var getObjectKeys = function (obj, forKey) { var list = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { list.push(forKey ? camSafeUrlEncode(key).toLowerCase() : key); } } return list.sort(function (a, b) { a = a.toLowerCase(); b = b.toLowerCase(); return a === b ? 0 : a > b ? 1 : -1; }); }; /** * obj转为string * @param {Object} obj 需要转的对象,必须 * @param {Boolean} lowerCaseKey key是否转为小写,默认false,非必须 * @return {String} data 返回字符串 */ const obj2str = (obj, lowerCaseKey) => { var i, key, val; var list = []; var keyList = getObjectKeys(obj); for (i = 0; i < keyList.length; i++) { key = keyList[i]; val = obj[key] === undefined || obj[key] === null ? '' : '' + obj[key]; key = lowerCaseKey ? camSafeUrlEncode(key).toLowerCase() : camSafeUrlEncode(key); val = camSafeUrlEncode(val) || ''; list.push(key + '=' + val); } return list.join('&'); }; //获取签名 const getAuth = (opt) => { const now = Math.floor(Date.now() / 1000); const startTime = Math.max(opt.startTime, now - 60); const endTime = Math.min(opt.expiredTime, now + 900); const qSignTime = `${startTime};${endTime}`; const qKeyTime = qSignTime; var method = (opt.method || opt.Method || 'get').toLowerCase(); let pathname = opt.Pathname || opt.Key || '/'; //测试用的key后面可以去掉 if (!pathname.startsWith('/')) pathname = '/' + pathname; const queryParams = cloneDeep(opt.Query || opt.params || {}); let headers = getSignHeaderObj(cloneDeep(opt.Headers || opt.headers || {})); var qUrlParamList = getObjectKeys(queryParams, true).join(';').toLowerCase(); // #ifdef APP-PLUS headers = {}; // #endif const lowerCaseHeaders = {}; Object.keys(headers).forEach(key => { lowerCaseHeaders[key.toLowerCase()] = headers[key]; }); const headerKeys = Object.keys(lowerCaseHeaders).sort(); const qHeaderList = headerKeys.join(';'); // 步骤一:计算 SignKey const signKey = CryptoJS.HmacSHA1(qKeyTime, opt.SecretKey).toString(); const formatString = [ method, pathname, obj2str(queryParams, true), obj2str(lowerCaseHeaders, true), '' ].join('\n'); const stringToSign = [ 'sha1', qSignTime, CryptoJS.SHA1(formatString).toString(), '' ].join('\n'); const qSignature = CryptoJS.HmacSHA1(stringToSign, signKey).toString(); const authorization = [ 'q-sign-algorithm=sha1', 'q-ak=' + opt.SecretId, 'q-sign-time=' + qSignTime, 'q-key-time=' + qKeyTime, 'q-header-list=' + qHeaderList, 'q-url-param-list=' + qUrlParamList, 'q-signature=' + qSignature ].join('&'); return authorization; }; const getContentType = (ext) => { const map = { jpg: 'image/jpg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', mp4: 'video/mp4', mov: 'video/quicktime', // mov pdf: 'application/pdf', // 你可以根据需要扩展更多类型 }; return map[ext] || 'application/octet-stream'; }; // 文件上传封装(跨平台支持 COS 的 PUT 上传) // 导出一个异步函数,用于将文件上传到COS export function uploadFileToCOSByPlatform(fileData_1, getAccessInfo_1, appChanneId_1, baseUrl_1) { return __awaiter(this, arguments, void 0, function* (fileData, getAccessInfo, appChanneId, baseUrl, appName = 'qingyi') { var _a; if (!fileData) return null; if (!getAccessInfo) { getAccessInfo = yield fetchAccessInfo(baseUrl, appChanneId); } if (!getAccessInfo) return null; uni.showLoading({ title: '上传中...', }); let file; let arrayBuffer; // #ifdef APP-PLUS ({ file, arrayBuffer } = yield readFileWithArrayBuffer(fileData.filePath)); // #endif // #ifdef MP-WEIXIN file = fileData; // #endif let timestamp = new Date().getTime(); const ext = ((_a = file.name) === null || _a === void 0 ? void 0 : _a.split('.').pop()) || 'bin'; // 文件后缀名 let cosfileName = `${appName}_${timestamp}.${ext}`; // 获取文件类型 file.type = getContentType(ext); const key = `${getAccessInfo.dir}${cosfileName}`; console.log('key======================', key); if (file.size <= 0) { let title = `文件${file.name}大小为0,请重新选择文件`; uni.showToast({ title, icon: 'none' }); uni.hideLoading(); throw new Error('__FileSizeZero__'); } let cos = Object.assign({ SecretId: getAccessInfo.tmpSecretID, SecretKey: getAccessInfo.tmpSecretKey, SecurityToken: getAccessInfo.sessionToken, StartTime: getAccessInfo.startTime, ExpiredTime: getAccessInfo.expiredTime, method: 'put', headers: { 'content-length': file.size, 'host': `${getAccessInfo.bucket}.cos.${getAccessInfo.region}.myqcloud.com` }, Key: key }, getAccessInfo); const authorization = getAuth(cos); // 构造上传文件的URL const url = `https://${getAccessInfo.bucket}.cos.${getAccessInfo.region}.myqcloud.com/${key}`; // 构造请求头 const headers = { 'Content-Type': file.type || 'application/octet-stream', 'Authorization': authorization, 'x-cos-security-token': getAccessInfo.sessionToken, 'Content-Length': file.size, }; console.log('headers======================', headers); // 获取当前平台 const platform = uni.getSystemInfoSync().platform; // 大于8MB分片上传 const MAX_SIZE = 8 * 1024 * 1024; // 8MB // #ifdef MP-WEIXIN // 小程序平台 if (uni.getFileSystemManager) { // 获取文件系统管理器 const fs = uni.getFileSystemManager(); // 读取文件内容 const arrayBuffer = fs.readFileSync(fileData.filePath); // 大于8MB分片上传 if (file.size > MAX_SIZE) { try { const multipartRes = yield multipartUploadToCOS(file, arrayBuffer, getAccessInfo, key, appChanneId, baseUrl); return multipartRes; } catch (err) { console.error('分片上传失败:', err); throw err; } } // 调用上传文件到COS的函数 return yield putFileToCOS(url, arrayBuffer, headers, appChanneId, baseUrl, file, key); } // #endif // App 平台 if (platform === 'android' || platform === 'ios') { // 大于8MB分片上传 if (file.size > MAX_SIZE) { try { const multipartRes = yield multipartUploadToCOS(file, arrayBuffer, getAccessInfo, key, appChanneId, baseUrl); return multipartRes; } catch (err) { console.error('分片上传失败:', err); throw err; } } // 返回一个Promise对象 return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { console.log('获取的 arrayBuffer22222:', arrayBuffer); try { // 调用上传文件到COS的函数 const res = yield putFileToCOS(url, arrayBuffer, headers, appChanneId, baseUrl, file, key); // arrayBuffer = null; // 释放内存 resolve(res); } catch (err) { reject(err); } })); } // H5 平台(前提是你拿到了 File 对象) if (typeof FileReader !== 'undefined') { // 返回一个Promise对象 return new Promise((resolve, reject) => { // 创建文件读取器 const reader = new FileReader(); // 文件读取完成后的回调函数 reader.onload = function (e) { return __awaiter(this, void 0, void 0, function* () { // 获取文件内容的ArrayBuffer const arrayBuffer = e.target.result; try { // 调用上传文件到COS的函数 const res = yield putFileToCOS(url, arrayBuffer, headers, appChanneId, baseUrl, file, key); resolve(res); } catch (err) { reject(err); } }); }; // 文件读取出错后的回调函数 reader.onerror = reject; // 以ArrayBuffer的形式读取文件 reader.readAsArrayBuffer(fileData.filePath); // 注意:此处 filePath 应是 File 对象 }); } // 抛出错误,当前平台不支持文件读取 throw new Error('当前平台不支持文件读取'); }); } // 分片上传逻辑(需你完善) // 封装分片上传 function multipartUploadToCOS(file, arrayBuffer, getAccessInfo, key, appChanneId, baseUrl) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; const { bucket, region, sessionToken } = getAccessInfo; const cosHost = `https://${bucket}.cos.${region}.myqcloud.com`; const host = `${bucket}.cos.${region}.myqcloud.com`; const objectName = key; const initUrl = `${cosHost}/${objectName}?uploads`; const headers = { Host: host, 'Content-Length': 0, 'Content-Type': file.type || 'application/octet-stream', // or 'application/octet-stream' 'x-cos-security-token': getAccessInfo.sessionToken, // 临时密钥一定要带 'connection': 'keep-alive', // 关闭连接,避免长连接问题 Authorization: getAuth(Object.assign({ SecretId: getAccessInfo.tmpSecretID, SecretKey: getAccessInfo.tmpSecretKey, SecurityToken: getAccessInfo.sessionToken, Method: 'POST', Key: key, Bucket: bucket, Region: region }, getAccessInfo)), Date: new Date().toUTCString() }; console.log('headers22222222======================', headers); // 1️⃣ 初始化分片上传,获取 uploadId const initRes = yield uni.request({ url: initUrl, method: 'POST', header: headers }); const uploadIdMatch = initRes.data.match(/<UploadId>(.+?)<\/UploadId>/); if (!uploadIdMatch) throw new Error('初始化分片上传失败'); const uploadId = uploadIdMatch[1]; // 3️⃣ 分片处理 const chunkSize = 1024 * 1024 * 2; // 每块2MB const chunks = []; for (let start = 0; start < arrayBuffer.byteLength; start += chunkSize) { chunks.push(arrayBuffer.slice(start, start + chunkSize)); } // arrayBuffer = null; // 释放内存 const partETags = []; // 4️⃣ 上传每个分片 for (let i = 0; i < chunks.length; i++) { let partData = chunks[i]; const partNumber = i + 1; const uploadUrl = `${cosHost}/${objectName}?partNumber=${partNumber}&uploadId=${uploadId}`; const authorization = getAuth(Object.assign({ SecretId: getAccessInfo.tmpSecretID, SecretKey: getAccessInfo.tmpSecretKey, SecurityToken: sessionToken, Method: 'PUT', Key: key, Bucket: bucket, Region: region, Query: { partNumber: partNumber.toString(), uploadId: uploadId } }, getAccessInfo)); const res = yield uni.request({ url: uploadUrl, method: 'PUT', header: { Host: host, 'Content-Type': 'application/octet-stream', 'x-cos-security-token': sessionToken, 'Authorization': authorization, 'Content-Length': partData.byteLength, 'Connection': 'keep-alive', // 关闭连接,避免长连接问题 Date: new Date().toUTCString() }, data: partData }); console.log('分片上传结果===============', res); const etag = ((_a = res.header) === null || _a === void 0 ? void 0 : _a.ETag) || ((_b = res.header) === null || _b === void 0 ? void 0 : _b.Etag) || ((_c = res.header) === null || _c === void 0 ? void 0 : _c.etag); if (!etag) throw new Error(`第 ${partNumber} 块上传失败:缺少 ETag`); partETags.push({ PartNumber: partNumber, ETag: etag.replace(/"/g, '') }); // 去掉引号 } // 5️⃣完成分片上传 const completeUrl = `${cosHost}/${objectName}?uploadId=${uploadId}`; const completeAuthorization = getAuth(Object.assign({ SecretId: getAccessInfo.tmpSecretID, SecretKey: getAccessInfo.tmpSecretKey, SecurityToken: sessionToken, Method: 'POST', Key: key, Bucket: bucket, Region: region, Query: { uploadId } }, getAccessInfo)); const completeXml = buildCompleteMultipartXML(partETags); const completeRes = yield uni.request({ url: completeUrl, method: 'POST', header: { Host: host, 'Content-Type': 'application/xml', 'Authorization': completeAuthorization, 'x-cos-security-token': sessionToken, 'Content-Length': completeXml.length, // 这里的长度需要根据 completeXml 的实际长度来设置 'Connection': 'keep-alive', // 关闭连接,避免长连接问题 Date: new Date().toUTCString() }, data: completeXml }); if (!/CompleteMultipartUploadResult/.test(completeRes.data)) { throw new Error('分片合并失败'); } // 6️⃣ 上传元数据 const fileMd5 = generateMd5FromUUID(); const putData = { _appid: appChanneId, fileMd5, ext: file.name.split('.').pop().toLowerCase(), size: file.size, path: `/${key}`, url: `${cosHost}/${objectName}`, fileName: file.name }; const metaRes = yield doUploadMetadata(putData, baseUrl); uni.hideLoading(); // 如果元数据上传成功,返回文件信息 if ((metaRes === null || metaRes === void 0 ? void 0 : metaRes.code) === 200) { return { name: file.name, size: file.size, ext: putData.ext, path: key, url: metaRes.data.fileURL }; } else { throw new Error('元数据上传失败'); } }); } // 导出一个函数,用于读取文件并返回ArrayBuffer,和文件对象 export function readFileWithArrayBuffer(filePathOrData) { // 返回一个Promise对象 return new Promise((resolve, reject) => { // 如果filePathOrData是字符串,则将其赋值给filePath,否则将其filePath属性赋值给filePath const filePath = typeof filePathOrData === 'string' ? filePathOrData : filePathOrData.filePath; // 如果filePath不存在,则返回一个错误 if (!filePath) { return reject(new Error('filePath 不存在')); } // 使用plus.io.resolveLocalFileSystemURL方法解析本地文件路径 plus.io.resolveLocalFileSystemURL(filePath, (entry) => { // 使用entry.file方法获取文件对象 entry.file((file) => { // 创建一个FileReader对象 const reader = new plus.io.FileReader(); // 当读取结束时,执行onloadend方法 reader.onloadend = (e) => { try { // 将读取的结果转换为base64格式 const base64 = e.target.result.split(',')[1]; // 将base64转换为ArrayBuffer格式 const arrayBuffer = uni.base64ToArrayBuffer(base64); // 将文件对象和ArrayBuffer对象封装成一个对象 const result = { file, arrayBuffer }; // 解析成功,返回结果 resolve(result); } catch (err) { // 解析失败,返回错误 reject(err); } }; // 当读取出错时,执行onerror方法 reader.onerror = reject; // 将文件对象转换为base64格式 reader.readAsDataURL(file); }, reject); }, reject); }); } function buildCompleteMultipartXML(parts) { const xmlParts = parts.map((p) => `<Part><PartNumber>${p.PartNumber}</PartNumber><ETag>"${p.ETag}"</ETag></Part>`).join(''); return `<CompleteMultipartUpload>${xmlParts}</CompleteMultipartUpload>`; } // 定义一个异步函数,用于将文件上传到COS const putFileToCOS = (url, arrayBuffer, headers, appChanneId, baseUrl, file, key) => __awaiter(void 0, void 0, void 0, function* () { // 返回一个Promise对象 return new Promise((resolve, reject) => { // 发起uni.request请求 uni.request({ url, method: 'PUT', data: arrayBuffer, header: headers, success: (res) => __awaiter(void 0, void 0, void 0, function* () { // 如果请求成功 if (res.statusCode === 200) { console.log('文件上传成功', res); // 隐藏加载动画 uni.hideLoading(); // 生成文件的MD5值 const fileMd5 = generateMd5FromUUID(); // 获取图片信息 const dimensions = yield getImageDimensions(file); // 构造文件数据 const fileData = { name: file.name, size: file.size, ext: file.name.split('.').pop().toLowerCase(), path: key, width: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.width) || 0, height: (dimensions === null || dimensions === void 0 ? void 0 : dimensions.height) || 0, }; // 构造上传数据 const putData = { _appid: appChanneId, fileMd5, ext: fileData.ext, size: file.size, path: `/${key}`, url, fileName: file.name, }; try { // 发起上传元数据的请求 const res = yield doUploadMetadata(putData, baseUrl); // 隐藏加载动画 uni.hideLoading(); // 如果请求成功 if ((res === null || res === void 0 ? void 0 : res.code) === 200) { // 将上传结果中的文件URL赋值给文件数据 fileData.url = res.data.fileURL; resolve(fileData); // ✅ 成功返回上传结果 } else { console.error('元数据保存失败:', res); reject(new Error('上传元数据失败')); } } catch (e) { reject(e); } resolve({ success: true, url }); } else { console.error('COS上传失败', res); reject(res); } }), fail(err) { console.error('COS上传失败', err); reject(err); } }); }); }); // 导出一个异步函数,用于上传元数据 export const doUploadMetadata = (putData, baseUrl) => __awaiter(void 0, void 0, void 0, function* () { try { // 发送POST请求,上传元数据 const res = yield uni.request({ url: `${baseUrl}/ms-attachment/cos/object/add`, method: 'POST', header: { 'Content-Type': 'application/json' }, data: putData, }); // 获取返回的数据 const resJson = res.data; // 隐藏加载动画 uni.hideLoading(); // 如果返回的code为200,则返回数据 if (resJson.code === 200) { return resJson; } else { // 否则,打印错误信息,并返回null console.error('服务返回错误:', resJson); return null; } } catch (e) { // 如果请求异常,隐藏加载动画,打印错误信息,并返回null uni.hideLoading(); console.error('请求异常:', e); return null; } }); // 获取图片尺寸 const getImageDimensions = (file) => { // 返回一个 Promise 对象 return new Promise((resolve) => { // 如果文件类型不是图片类型,直接返回 null if (file.type && !file.type.indexOf('image') > -1) { resolve(null); // 如果不是图片类型,直接返回 null return; } let path = file.filePath; uni.getImageInfo({ src: path, success: (res) => { resolve({ width: res.width, height: res.height }); }, fail: () => { resolve(null); } }); }); }; // 生成UUID function generateUUID() { // 获取当前时间 let d = new Date().getTime(); // 如果浏览器支持performance.now()方法,则获取当前时间 if (typeof performance !== 'undefined' && typeof performance.now === 'function') { d += performance.now(); //返回值表示为从time origin之后到当前调用时经过的时间 } // 返回UUID return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { // 生成随机数 let r = (d + Math.random() * 16) % 16 | 0; // 更新时间 d = Math.floor(d / 16); // 返回随机数 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } // api直传 const defaultUpload = (uploadUrl, file, options = {}, showLoading = false) => { if (showLoading) { uni.showLoading({ title: '上传中...', }); } const header = Object.assign(Object.assign({}, options), { 'content-type': 'application/x-www-form-urlencoded' }); return new Promise((resolve, reject) => { uni.uploadFile({ url: uploadUrl, name: 'file', filePath: file, header, success(res) { if (showLoading) uni.hideLoading(); if (res.statusCode === 200) { const data = JSON.parse(res.data); if (data.code == 20000) { console.log(data); resolve(data.data); } else { reject(data); } } else { console.error('上传失败', res); reject(res); } }, fail(err) { if (showLoading) uni.hideLoading(); console.error('上传失败', err); reject(err); }, }); }); }; export { getFileMd5, generateMd5FromUUID, formatFileSize, generateUUID, defaultUpload };