Files
digital-pilates/services/cos.ts

187 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { COS_BUCKET, COS_REGION, buildPublicUrl } from '@/constants/Cos';
import { api } from '@/services/api';
type ServerCosToken = {
tmpSecretId: string;
tmpSecretKey: string;
sessionToken: string;
startTime?: number;
expiredTime?: number;
bucket?: string;
region?: string;
prefix?: string;
cdnDomain?: string;
};
type CosCredential = {
credentials: {
tmpSecretId: string;
tmpSecretKey: string;
sessionToken: string;
};
startTime?: number;
expiredTime?: number;
bucket?: string;
region?: string;
prefix?: string;
cdnDomain?: string;
};
type UploadOptions = {
key: string;
// React Native COS SDK 推荐使用本地文件路径file:// 或 content://
srcUri?: string;
// 为兼容旧实现Web/Blob。在 RN SDK 下会忽略 body
body?: any;
contentType?: string;
onProgress?: (progress: { percent: number }) => void;
signal?: AbortSignal;
};
let rnTransferManager: any | null = null;
let rnInitialized = false;
async function fetchCredential(): Promise<CosCredential> {
// 后端返回 { code, message, data }api.get 会提取 data
const data = await api.get<ServerCosToken>('/api/users/cos/upload-token');
console.log('fetchCredential', data);
return {
credentials: {
tmpSecretId: data.tmpSecretId,
tmpSecretKey: data.tmpSecretKey,
sessionToken: data.sessionToken,
},
startTime: data.startTime,
expiredTime: data.expiredTime,
bucket: data.bucket,
region: data.region,
prefix: data.prefix,
cdnDomain: data.cdnDomain,
};
}
export async function uploadToCos(options: UploadOptions): Promise<{ key: string; etag?: string; headers?: Record<string, string>; publicUrl?: string }> {
const { key, srcUri, contentType, onProgress, signal } = options;
const cred = await fetchCredential();
const bucket = COS_BUCKET || cred.bucket;
const region = COS_REGION || cred.region;
if (!bucket || !region) {
throw new Error('未配置 COS_BUCKET / COS_REGION且服务端未返回 bucket/region');
}
// 确保对象键以服务端授权的前缀开头(去除通配符,折叠斜杠,避免把 * 拼进 Key
const finalKey = ((): string => {
const rawPrefix = String(cred.prefix || '');
// 1) 去掉 * 与多余斜杠,再去掉首尾斜杠
const safePrefix = rawPrefix
.replace(/\*/g, '')
.replace(/\/{2,}/g, '/')
.replace(/^\/+|\/+$/g, '');
const normalizedKey = key.replace(/^\//, '');
if (!safePrefix) return normalizedKey;
if (normalizedKey.startsWith(safePrefix + '/')) return normalizedKey;
return `${safePrefix}/${normalizedKey}`.replace(/\/{2,}/g, '/');
})();
// 初始化 react-native-cos-sdk一次
if (!rnInitialized) {
// await Cos.initWithSessionCredentialCallback(async () => {
// // SDK 会在需要时调用该回调,我们返回当前的临时密钥
// return {
// tmpSecretId: cred.credentials.tmpSecretId,
// tmpSecretKey: cred.credentials.tmpSecretKey,
// sessionToken: cred.credentials.sessionToken,
// startTime: cred.startTime,
// expiredTime: cred.expiredTime,
// } as any;
// });
// const serviceConfig = { region, isDebuggable: true, isHttps: true } as any;
// await Cos.registerDefaultService(serviceConfig);
// const transferConfig = {
// forceSimpleUpload: false,
// enableVerification: true,
// divisionForUpload: 2 * 1024 * 1024,
// sliceSizeForUpload: 1 * 1024 * 1024,
// } as any;
// rnTransferManager = await Cos.registerDefaultTransferManger(serviceConfig, transferConfig);
rnInitialized = true;
}
if (!srcUri || typeof srcUri !== 'string') {
throw new Error('请提供本地文件路径 srcUri形如 file:/// 或 content://');
}
const controller = new AbortController();
if (signal) {
if (signal.aborted) controller.abort();
signal.addEventListener('abort', () => controller.abort(), { once: true });
}
return await new Promise((resolve, reject) => {
let cancelled = false;
let taskRef: any = null;
(async () => {
try {
taskRef = await rnTransferManager.upload(
bucket,
finalKey,
srcUri,
{
resultListener: {
successCallBack: (header: any) => {
if (cancelled) return;
const publicUrl = buildPublicUrl(finalKey);
const etag = header?.ETag || header?.headers?.ETag || header?.headers?.etag;
resolve({ key: finalKey, etag, headers: header, publicUrl });
},
failCallBack: (clientError: any, serviceError: any) => {
if (cancelled) return;
console.log('uploadToCos', { clientError, serviceError });
const err = clientError || serviceError || new Error('COS 上传失败');
reject(err);
},
},
progressCallback: (complete: number, target: number) => {
if (onProgress) {
const percent = target > 0 ? complete / target : 0;
onProgress({ percent });
}
},
contentType,
}
);
} catch (e) {
if (!cancelled) reject(e);
}
})();
controller.signal.addEventListener('abort', () => {
cancelled = true;
try { taskRef?.cancel?.(); } catch { }
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
export async function uploadWithRetry(options: UploadOptions & { maxRetries?: number; backoffMs?: number }): Promise<{ key: string; etag?: string }> {
const { maxRetries = 2, backoffMs = 800, ...rest } = options;
let attempt = 0;
// 简单指数退避
while (true) {
try {
return await uploadToCos(rest);
} catch (e: any) {
if (rest.signal?.aborted) throw e;
if (attempt >= maxRetries) throw e;
const wait = backoffMs * Math.pow(2, attempt);
await new Promise(r => setTimeout(r, wait));
attempt++;
}
}
}