cos-uploaderjs-jiniu
Version:
适用于 UniApp 的腾讯 COS 文件上传封装工具
707 lines (706 loc) • 28.9 kB
JavaScript
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 };