|
|
@@ -1,879 +0,0 @@
|
|
|
-import type { InferResponseType } from 'hono/client';
|
|
|
-import { fileClient } from "../api";
|
|
|
-import { isWeapp, isH5 } from './platform';
|
|
|
-import Taro from '@tarojs/taro';
|
|
|
-
|
|
|
-// 平台检测 - 使用统一的 platform.ts
|
|
|
-const isMiniProgram = isWeapp();
|
|
|
-const isBrowser = isH5();
|
|
|
-
|
|
|
-export interface MinioProgressEvent {
|
|
|
- stage: 'uploading' | 'complete' | 'error';
|
|
|
- message: string;
|
|
|
- progress: number;
|
|
|
- details?: {
|
|
|
- loaded: number;
|
|
|
- total: number;
|
|
|
- };
|
|
|
- timestamp: number;
|
|
|
-}
|
|
|
-
|
|
|
-export interface MinioProgressCallbacks {
|
|
|
- onProgress?: (event: MinioProgressEvent) => void;
|
|
|
- onComplete?: () => void;
|
|
|
- onError?: (error: Error) => void;
|
|
|
- signal?: AbortSignal | { aborted: boolean };
|
|
|
-}
|
|
|
-
|
|
|
-export interface UploadResult {
|
|
|
- fileUrl: string;
|
|
|
- fileKey: string;
|
|
|
- bucketName: string;
|
|
|
- fileId: number;
|
|
|
-}
|
|
|
-
|
|
|
-interface UploadPart {
|
|
|
- ETag: string;
|
|
|
- PartNumber: number;
|
|
|
-}
|
|
|
-
|
|
|
-interface UploadProgressDetails {
|
|
|
- partNumber: number;
|
|
|
- totalParts: number;
|
|
|
- partSize: number;
|
|
|
- totalSize: number;
|
|
|
- partProgress?: number;
|
|
|
-}
|
|
|
-
|
|
|
-type MinioMultipartUploadPolicy = InferResponseType<typeof fileClient["multipart-policy"]['$post'], 200>
|
|
|
-type MinioUploadPolicy = InferResponseType<typeof fileClient["upload-policy"]['$post'], 200>
|
|
|
-
|
|
|
-const PART_SIZE = 5 * 1024 * 1024; // 每部分5MB
|
|
|
-
|
|
|
-// ==================== H5 实现(保留原有代码) ====================
|
|
|
-export class MinIOXHRMultipartUploader {
|
|
|
- /**
|
|
|
- * 使用XHR分段上传文件到MinIO(H5环境)
|
|
|
- */
|
|
|
- static async upload(
|
|
|
- policy: MinioMultipartUploadPolicy,
|
|
|
- file: File | Blob,
|
|
|
- key: string,
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
- ): Promise<UploadResult> {
|
|
|
- const partSize = PART_SIZE;
|
|
|
- const totalSize = file.size;
|
|
|
- const totalParts = Math.ceil(totalSize / partSize);
|
|
|
- const uploadedParts: UploadPart[] = [];
|
|
|
-
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'uploading',
|
|
|
- message: '准备上传文件...',
|
|
|
- progress: 0,
|
|
|
- details: {
|
|
|
- loaded: 0,
|
|
|
- total: totalSize
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
-
|
|
|
- // 分段上传
|
|
|
- for (let i = 0; i < totalParts; i++) {
|
|
|
- const start = i * partSize;
|
|
|
- const end = Math.min(start + partSize, totalSize);
|
|
|
- const partBlob = file.slice(start, end);
|
|
|
- const partNumber = i + 1;
|
|
|
-
|
|
|
- try {
|
|
|
- const etag = await this.uploadPart(
|
|
|
- policy.partUrls[i],
|
|
|
- partBlob,
|
|
|
- callbacks,
|
|
|
- {
|
|
|
- partNumber,
|
|
|
- totalParts,
|
|
|
- partSize: partBlob.size,
|
|
|
- totalSize
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- uploadedParts.push({
|
|
|
- ETag: etag,
|
|
|
- PartNumber: partNumber
|
|
|
- });
|
|
|
-
|
|
|
- // 更新进度
|
|
|
- const progress = Math.round((end / totalSize) * 100);
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'uploading',
|
|
|
- message: `上传文件片段 ${partNumber}/${totalParts}`,
|
|
|
- progress,
|
|
|
- details: {
|
|
|
- loaded: end,
|
|
|
- total: totalSize,
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 完成上传
|
|
|
- try {
|
|
|
- const result = await this.completeMultipartUpload(policy, key, uploadedParts);
|
|
|
-
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'complete',
|
|
|
- message: '文件上传完成',
|
|
|
- progress: 100,
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
-
|
|
|
- callbacks?.onComplete?.();
|
|
|
- return {
|
|
|
- fileUrl: `${policy.host}/${key}`,
|
|
|
- fileKey: key,
|
|
|
- bucketName: policy.bucket,
|
|
|
- fileId: result.fileId
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 上传单个片段
|
|
|
- private static uploadPart(
|
|
|
- uploadUrl: string,
|
|
|
- partBlob: Blob,
|
|
|
- callbacks?: MinioProgressCallbacks,
|
|
|
- progressDetails?: UploadProgressDetails
|
|
|
- ): Promise<string> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const xhr = new XMLHttpRequest();
|
|
|
-
|
|
|
- xhr.upload.onprogress = (event) => {
|
|
|
- if (event.lengthComputable && callbacks?.onProgress) {
|
|
|
- const partProgress = Math.round((event.loaded / event.total) * 100);
|
|
|
- callbacks.onProgress({
|
|
|
- stage: 'uploading',
|
|
|
- message: `上传文件片段 ${progressDetails?.partNumber}/${progressDetails?.totalParts} (${partProgress}%)`,
|
|
|
- progress: Math.round((
|
|
|
- (progressDetails?.partNumber ? (progressDetails.partNumber - 1) * (progressDetails.partSize || 0) : 0) + event.loaded
|
|
|
- ) / (progressDetails?.totalSize || 1) * 100),
|
|
|
- details: {
|
|
|
- ...progressDetails,
|
|
|
- loaded: event.loaded,
|
|
|
- total: event.total
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- xhr.onload = () => {
|
|
|
- if (xhr.status >= 200 && xhr.status < 300) {
|
|
|
- const etag = xhr.getResponseHeader('ETag')?.replace(/"/g, '') || '';
|
|
|
- resolve(etag);
|
|
|
- } else {
|
|
|
- reject(new Error(`上传片段失败: ${xhr.status} ${xhr.statusText}`));
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- xhr.onerror = () => reject(new Error('上传片段失败'));
|
|
|
-
|
|
|
- xhr.open('PUT', uploadUrl);
|
|
|
- xhr.send(partBlob);
|
|
|
-
|
|
|
- if (callbacks?.signal) {
|
|
|
- if ('addEventListener' in callbacks.signal) {
|
|
|
- callbacks.signal.addEventListener('abort', () => {
|
|
|
- xhr.abort();
|
|
|
- reject(new Error('上传已取消'));
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 完成分段上传
|
|
|
- private static async completeMultipartUpload(
|
|
|
- policy: MinioMultipartUploadPolicy,
|
|
|
- key: string,
|
|
|
- uploadedParts: UploadPart[]
|
|
|
- ): Promise<{ fileId: number }> {
|
|
|
- const response = await fileClient["multipart-complete"].$post({
|
|
|
- json: {
|
|
|
- bucket: policy.bucket,
|
|
|
- key,
|
|
|
- uploadId: policy.uploadId,
|
|
|
- parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
|
|
|
- }
|
|
|
-
|
|
|
- return response.json();
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export class MinIOXHRUploader {
|
|
|
- /**
|
|
|
- * 使用XHR上传文件到MinIO(H5环境)
|
|
|
- */
|
|
|
- static upload(
|
|
|
- policy: MinioUploadPolicy,
|
|
|
- file: File | Blob,
|
|
|
- key: string,
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
- ): Promise<UploadResult> {
|
|
|
- const formData = new FormData();
|
|
|
-
|
|
|
- // 添加 MinIO 需要的字段
|
|
|
- Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
|
|
|
- if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
|
|
|
- formData.append(k, value);
|
|
|
- }
|
|
|
- });
|
|
|
- formData.append('key', key);
|
|
|
- formData.append('file', file);
|
|
|
-
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const xhr = new XMLHttpRequest();
|
|
|
-
|
|
|
- // 上传进度处理
|
|
|
- if (callbacks?.onProgress) {
|
|
|
- xhr.upload.onprogress = (event) => {
|
|
|
- if (event.lengthComputable) {
|
|
|
- callbacks.onProgress?.({
|
|
|
- stage: 'uploading',
|
|
|
- message: '正在上传文件...',
|
|
|
- progress: Math.round((event.loaded * 100) / event.total),
|
|
|
- details: {
|
|
|
- loaded: event.loaded,
|
|
|
- total: event.total
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 完成处理
|
|
|
- xhr.onload = () => {
|
|
|
- if (xhr.status >= 200 && xhr.status < 300) {
|
|
|
- if (callbacks?.onProgress) {
|
|
|
- callbacks.onProgress({
|
|
|
- stage: 'complete',
|
|
|
- message: '文件上传完成',
|
|
|
- progress: 100,
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- }
|
|
|
- callbacks?.onComplete?.();
|
|
|
- resolve({
|
|
|
- fileUrl: `${policy.uploadPolicy.host}/${key}`,
|
|
|
- fileKey: key,
|
|
|
- bucketName: policy.uploadPolicy.bucket,
|
|
|
- fileId: policy.file.id
|
|
|
- });
|
|
|
- } else {
|
|
|
- const error = new Error(`上传失败: ${xhr.status} ${xhr.statusText}`);
|
|
|
- callbacks?.onError?.(error);
|
|
|
- reject(error);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 错误处理
|
|
|
- xhr.onerror = () => {
|
|
|
- const error = new Error('上传失败');
|
|
|
- if (callbacks?.onProgress) {
|
|
|
- callbacks.onProgress({
|
|
|
- stage: 'error',
|
|
|
- message: '文件上传失败',
|
|
|
- progress: 0,
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- }
|
|
|
- callbacks?.onError?.(error);
|
|
|
- reject(error);
|
|
|
- };
|
|
|
-
|
|
|
- // 根据当前页面协议和 host 配置决定最终的上传地址
|
|
|
- const currentProtocol = typeof window !== 'undefined' ? window.location.protocol : 'https:';
|
|
|
- const host = policy.uploadPolicy.host?.startsWith('http')
|
|
|
- ? policy.uploadPolicy.host
|
|
|
- : `${currentProtocol}//${policy.uploadPolicy.host}`;
|
|
|
-
|
|
|
- xhr.open('POST', host);
|
|
|
- xhr.send(formData);
|
|
|
-
|
|
|
- // 处理取消
|
|
|
- if (callbacks?.signal) {
|
|
|
- if ('addEventListener' in callbacks.signal) {
|
|
|
- callbacks.signal.addEventListener('abort', () => {
|
|
|
- xhr.abort();
|
|
|
- reject(new Error('上传已取消'));
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 小程序实现 ====================
|
|
|
-export class TaroMinIOMultipartUploader {
|
|
|
- /**
|
|
|
- * 使用 Taro 分段上传文件到 MinIO(小程序环境)
|
|
|
- */
|
|
|
- static async upload(
|
|
|
- policy: MinioMultipartUploadPolicy,
|
|
|
- filePath: string,
|
|
|
- key: string,
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
- ): Promise<UploadResult> {
|
|
|
- const partSize = PART_SIZE;
|
|
|
-
|
|
|
- // 获取文件信息
|
|
|
- const fileInfo = await getFileInfoPromise(filePath);
|
|
|
- const totalSize = fileInfo.size;
|
|
|
- const totalParts = Math.ceil(totalSize / partSize);
|
|
|
- const uploadedParts: UploadPart[] = [];
|
|
|
-
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'uploading',
|
|
|
- message: '准备上传文件...',
|
|
|
- progress: 0,
|
|
|
- details: {
|
|
|
- loaded: 0,
|
|
|
- total: totalSize
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
-
|
|
|
- // 分段上传
|
|
|
- for (let i = 0; i < totalParts; i++) {
|
|
|
- if (callbacks?.signal && 'aborted' in callbacks.signal && callbacks.signal.aborted) {
|
|
|
- throw new Error('上传已取消');
|
|
|
- }
|
|
|
-
|
|
|
- const start = i * partSize;
|
|
|
- const end = Math.min(start + partSize, totalSize);
|
|
|
- const partNumber = i + 1;
|
|
|
-
|
|
|
- try {
|
|
|
- // 读取文件片段
|
|
|
- const partData = await this.readFileSlice(filePath, start, end);
|
|
|
-
|
|
|
- const etag = await this.uploadPart(
|
|
|
- policy.partUrls[i],
|
|
|
- partData,
|
|
|
- callbacks,
|
|
|
- {
|
|
|
- partNumber,
|
|
|
- totalParts,
|
|
|
- partSize: end - start,
|
|
|
- totalSize
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- uploadedParts.push({
|
|
|
- ETag: etag,
|
|
|
- PartNumber: partNumber
|
|
|
- });
|
|
|
-
|
|
|
- // 更新进度
|
|
|
- const progress = Math.round((end / totalSize) * 100);
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'uploading',
|
|
|
- message: `上传文件片段 ${partNumber}/${totalParts}`,
|
|
|
- progress,
|
|
|
- details: {
|
|
|
- loaded: end,
|
|
|
- total: totalSize,
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 完成上传
|
|
|
- try {
|
|
|
- const result = await this.completeMultipartUpload(policy, key, uploadedParts);
|
|
|
-
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'complete',
|
|
|
- message: '文件上传完成',
|
|
|
- progress: 100,
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
-
|
|
|
- callbacks?.onComplete?.();
|
|
|
- return {
|
|
|
- fileUrl: `${policy.host}/${key}`,
|
|
|
- fileKey: key,
|
|
|
- bucketName: policy.bucket,
|
|
|
- fileId: result.fileId
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
|
- throw error;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 读取文件片段
|
|
|
- private static async readFileSlice(filePath: string, start: number, end: number): Promise<ArrayBuffer> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- try {
|
|
|
- const fs = Taro?.getFileSystemManager?.();
|
|
|
- if (!fs) {
|
|
|
- reject(new Error('小程序文件系统不可用'));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const fileData = fs.readFileSync(filePath, undefined, start, end - start);
|
|
|
-
|
|
|
- // 确保返回 ArrayBuffer 类型
|
|
|
- if (typeof fileData === 'string') {
|
|
|
- // 将字符串转换为 ArrayBuffer
|
|
|
- const encoder = new TextEncoder();
|
|
|
- resolve(encoder.encode(fileData).buffer);
|
|
|
- } else if (fileData instanceof ArrayBuffer) {
|
|
|
- resolve(fileData);
|
|
|
- } else {
|
|
|
- // 处理其他可能的数据类型
|
|
|
- reject(new Error('文件数据类型不支持'));
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- reject(error);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 上传单个片段
|
|
|
- private static async uploadPart(
|
|
|
- uploadUrl: string,
|
|
|
- partData: ArrayBuffer,
|
|
|
- _callbacks?: MinioProgressCallbacks,
|
|
|
- _progressDetails?: UploadProgressDetails
|
|
|
- ): Promise<string> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- Taro?.request?.({
|
|
|
- url: uploadUrl,
|
|
|
- method: 'PUT',
|
|
|
- data: partData,
|
|
|
- header: {
|
|
|
- 'Content-Type': 'application/octet-stream'
|
|
|
- },
|
|
|
- success: (res: any) => {
|
|
|
- if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
- const etag = res.header?.['ETag']?.replace(/"/g, '') || '';
|
|
|
- resolve(etag);
|
|
|
- } else {
|
|
|
- reject(new Error(`上传片段失败: ${res.statusCode}`));
|
|
|
- }
|
|
|
- },
|
|
|
- fail: (error: any) => {
|
|
|
- reject(new Error(`上传片段失败: ${error.errMsg}`));
|
|
|
- }
|
|
|
- }) || reject(new Error('小程序环境不可用'));
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 完成分段上传
|
|
|
- private static async completeMultipartUpload(
|
|
|
- policy: MinioMultipartUploadPolicy,
|
|
|
- key: string,
|
|
|
- uploadedParts: UploadPart[]
|
|
|
- ): Promise<{ fileId: number }> {
|
|
|
- const response = await fileClient["multipart-complete"].$post({
|
|
|
- json: {
|
|
|
- bucket: policy.bucket,
|
|
|
- key,
|
|
|
- uploadId: policy.uploadId,
|
|
|
- parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
|
|
|
- }
|
|
|
-
|
|
|
- return await response.json();
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export class TaroMinIOUploader {
|
|
|
- /**
|
|
|
- * 使用 Taro 上传文件到 MinIO(小程序环境)
|
|
|
- */
|
|
|
- static async upload(
|
|
|
- policy: MinioUploadPolicy,
|
|
|
- filePath: string,
|
|
|
- key: string,
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
- ): Promise<UploadResult> {
|
|
|
- // 获取文件信息
|
|
|
- const fileInfo = await getFileInfoPromise(filePath);
|
|
|
- const totalSize = fileInfo.size;
|
|
|
-
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'uploading',
|
|
|
- message: '准备上传文件...',
|
|
|
- progress: 0,
|
|
|
- details: {
|
|
|
- loaded: 0,
|
|
|
- total: totalSize
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
-
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- // 准备表单数据 - 使用对象形式,Taro.uploadFile会自动处理
|
|
|
- const formData: Record<string, string> = {};
|
|
|
-
|
|
|
- // 添加 MinIO 需要的字段
|
|
|
- Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
|
|
|
- if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
|
|
|
- formData[k] = value;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- formData['key'] = key;
|
|
|
-
|
|
|
- // 使用 Taro.uploadFile 替代 FormData
|
|
|
- const uploadTask = Taro.uploadFile({
|
|
|
- url: policy.uploadPolicy.host,
|
|
|
- filePath: filePath,
|
|
|
- name: 'file',
|
|
|
- formData: formData,
|
|
|
- header: {
|
|
|
- 'Content-Type': 'multipart/form-data'
|
|
|
- },
|
|
|
- success: (res) => {
|
|
|
- if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'complete',
|
|
|
- message: '文件上传完成',
|
|
|
- progress: 100,
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- callbacks?.onComplete?.();
|
|
|
- resolve({
|
|
|
- fileUrl: `${policy.uploadPolicy.host}/${key}`,
|
|
|
- fileKey: key,
|
|
|
- bucketName: policy.uploadPolicy.bucket,
|
|
|
- fileId: policy.file.id
|
|
|
- });
|
|
|
- } else {
|
|
|
- reject(new Error(`上传失败: ${res.statusCode}`));
|
|
|
- }
|
|
|
- },
|
|
|
- fail: (error) => {
|
|
|
- reject(new Error(`上传失败: ${error.errMsg}`));
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 监听上传进度
|
|
|
- uploadTask.progress((res) => {
|
|
|
- if (res.totalBytesExpectedToSend > 0) {
|
|
|
- const currentProgress = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100);
|
|
|
- callbacks?.onProgress?.({
|
|
|
- stage: 'uploading',
|
|
|
- message: `上传中 ${currentProgress}%`,
|
|
|
- progress: currentProgress,
|
|
|
- details: {
|
|
|
- loaded: res.totalBytesSent,
|
|
|
- total: res.totalBytesExpectedToSend
|
|
|
- },
|
|
|
- timestamp: Date.now()
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 支持取消上传
|
|
|
- if (callbacks?.signal && 'aborted' in callbacks.signal) {
|
|
|
- if (callbacks.signal.aborted) {
|
|
|
- uploadTask.abort();
|
|
|
- reject(new Error('上传已取消'));
|
|
|
- }
|
|
|
-
|
|
|
- // 监听取消信号
|
|
|
- const checkAbort = () => {
|
|
|
- if (callbacks.signal?.aborted) {
|
|
|
- uploadTask.abort();
|
|
|
- reject(new Error('上传已取消'));
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 定期检查取消状态
|
|
|
- const abortInterval = setInterval(checkAbort, 100);
|
|
|
-
|
|
|
- // 清理定时器
|
|
|
- const cleanup = () => clearInterval(abortInterval);
|
|
|
- uploadTask.onProgressUpdate = cleanup;
|
|
|
- uploadTask.onHeadersReceived = cleanup;
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 统一 API ====================
|
|
|
-/**
|
|
|
- * 根据运行环境自动选择合适的上传器
|
|
|
- */
|
|
|
-export class UniversalMinIOMultipartUploader {
|
|
|
- static async upload(
|
|
|
- policy: MinioMultipartUploadPolicy,
|
|
|
- file: File | Blob | string,
|
|
|
- key: string,
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
- ): Promise<UploadResult> {
|
|
|
- if (isBrowser && (file instanceof File || file instanceof Blob)) {
|
|
|
- return MinIOXHRMultipartUploader.upload(policy, file, key, callbacks);
|
|
|
- } else if (isMiniProgram && typeof file === 'string') {
|
|
|
- return TaroMinIOMultipartUploader.upload(policy, file, key, callbacks);
|
|
|
- } else {
|
|
|
- throw new Error('不支持的运行环境或文件类型');
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export class UniversalMinIOUploader {
|
|
|
- static async upload(
|
|
|
- policy: MinioUploadPolicy,
|
|
|
- file: File | Blob | string,
|
|
|
- key: string,
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
- ): Promise<UploadResult> {
|
|
|
- if (isBrowser && (file instanceof File || file instanceof Blob)) {
|
|
|
- return MinIOXHRUploader.upload(policy, file, key, callbacks);
|
|
|
- } else if (isMiniProgram && typeof file === 'string') {
|
|
|
- return TaroMinIOUploader.upload(policy, file, key, callbacks);
|
|
|
- } else {
|
|
|
- throw new Error('不支持的运行环境或文件类型');
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 通用函数 ====================
|
|
|
-export async function getUploadPolicy(key: string, fileName: string, fileType?: string, fileSize?: number): Promise<MinioUploadPolicy> {
|
|
|
- const policyResponse = await fileClient["upload-policy"].$post({
|
|
|
- json: {
|
|
|
- path: key,
|
|
|
- name: fileName,
|
|
|
- type: fileType,
|
|
|
- size: fileSize
|
|
|
- }
|
|
|
- });
|
|
|
- if (!policyResponse.ok) {
|
|
|
- throw new Error('获取上传策略失败');
|
|
|
- }
|
|
|
- return policyResponse.json();
|
|
|
-}
|
|
|
-
|
|
|
-export async function getMultipartUploadPolicy(totalSize: number, fileKey: string, fileType?: string, fileName: string = 'unnamed-file') {
|
|
|
- const policyResponse = await fileClient["multipart-policy"].$post({
|
|
|
- json: {
|
|
|
- totalSize,
|
|
|
- partSize: PART_SIZE,
|
|
|
- fileKey,
|
|
|
- type: fileType,
|
|
|
- name: fileName
|
|
|
- }
|
|
|
- });
|
|
|
- if (!policyResponse.ok) {
|
|
|
- throw new Error('获取分段上传策略失败');
|
|
|
- }
|
|
|
- return await policyResponse.json();
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 统一的上传函数,自动适应运行环境
|
|
|
- */
|
|
|
-export async function uploadMinIOWithPolicy(
|
|
|
- uploadPath: string,
|
|
|
- file: File | Blob | string,
|
|
|
- fileKey: string,
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
-): Promise<UploadResult> {
|
|
|
- if(uploadPath === '/') uploadPath = '';
|
|
|
- else{
|
|
|
- if(!uploadPath.endsWith('/')) uploadPath = `${uploadPath}/`
|
|
|
- if(uploadPath.startsWith('/')) uploadPath = uploadPath.replace(/^\//, '');
|
|
|
- }
|
|
|
-
|
|
|
- let fileSize: number;
|
|
|
- let fileType: string | undefined;
|
|
|
- let fileName: string;
|
|
|
-
|
|
|
- if (isBrowser && (file instanceof File || file instanceof Blob)) {
|
|
|
- fileSize = file.size;
|
|
|
- fileType = (file as File).type || undefined;
|
|
|
- fileName = (file as File).name || fileKey;
|
|
|
- } else if (isMiniProgram && typeof file === 'string') {
|
|
|
- try {
|
|
|
- const fileInfo = await getFileInfoPromise(file);
|
|
|
- fileSize = fileInfo.size;
|
|
|
- fileType = undefined;
|
|
|
- fileName = fileKey;
|
|
|
- } catch {
|
|
|
- fileSize = 0;
|
|
|
- fileType = undefined;
|
|
|
- fileName = fileKey;
|
|
|
- }
|
|
|
- } else {
|
|
|
- throw new Error('不支持的文件类型');
|
|
|
- }
|
|
|
-
|
|
|
- if (fileSize > PART_SIZE) {
|
|
|
- if (isBrowser && !(file instanceof File)) {
|
|
|
- throw new Error('不支持的文件类型,无法获取文件名');
|
|
|
- }
|
|
|
-
|
|
|
- const policy = await getMultipartUploadPolicy(
|
|
|
- fileSize,
|
|
|
- `${uploadPath}${fileKey}`,
|
|
|
- fileType,
|
|
|
- fileName
|
|
|
- );
|
|
|
-
|
|
|
- if (isBrowser) {
|
|
|
- return MinIOXHRMultipartUploader.upload(policy, file as File | Blob, policy.key, callbacks);
|
|
|
- } else {
|
|
|
- return TaroMinIOMultipartUploader.upload(policy, file as string, policy.key, callbacks);
|
|
|
- }
|
|
|
- } else {
|
|
|
- if (isBrowser && !(file instanceof File)) {
|
|
|
- throw new Error('不支持的文件类型,无法获取文件名');
|
|
|
- }
|
|
|
-
|
|
|
- const policy = await getUploadPolicy(`${uploadPath}${fileKey}`, fileName, fileType, fileSize);
|
|
|
-
|
|
|
- if (isBrowser) {
|
|
|
- return MinIOXHRUploader.upload(policy, file as File | Blob, policy.uploadPolicy.key, callbacks);
|
|
|
- } else {
|
|
|
- return TaroMinIOUploader.upload(policy, file as string, policy.uploadPolicy.key, callbacks);
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 小程序工具函数 ====================
|
|
|
-/**
|
|
|
- * Promise封装的getFileInfo函数
|
|
|
- */
|
|
|
-async function getFileInfoPromise(filePath: string): Promise<{ size: number }> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const fs = Taro?.getFileSystemManager?.();
|
|
|
- if (!fs) {
|
|
|
- reject(new Error('小程序文件系统不可用'));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- fs.getFileInfo({
|
|
|
- filePath,
|
|
|
- success: (res) => {
|
|
|
- resolve({ size: res.size });
|
|
|
- },
|
|
|
- fail: (error) => {
|
|
|
- reject(new Error(`获取文件信息失败: ${error.errMsg}`));
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 新增:自动适应运行环境的文件选择并上传函数
|
|
|
-/**
|
|
|
- * 自动适应运行环境:选择文件并上传到 MinIO
|
|
|
- * 小程序:使用 Taro.chooseImage
|
|
|
- * H5:使用 input[type="file"]
|
|
|
- */
|
|
|
-export async function uploadFromSelect(
|
|
|
- uploadPath: string = '',
|
|
|
- options: {
|
|
|
- sourceType?: ('album' | 'camera')[],
|
|
|
- count?: number,
|
|
|
- accept?: string,
|
|
|
- maxSize?: number,
|
|
|
- } = {},
|
|
|
- callbacks?: MinioProgressCallbacks
|
|
|
-): Promise<UploadResult> {
|
|
|
- const { sourceType = ['album', 'camera'], count = 1, accept = '*', maxSize = 10 * 1024 * 1024 } = options;
|
|
|
-
|
|
|
- if (isMiniProgram) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
-
|
|
|
- Taro.chooseImage({
|
|
|
- count,
|
|
|
- sourceType: sourceType as any, // 确保类型兼容
|
|
|
- success: async (res) => {
|
|
|
- const tempFilePath = res.tempFilePaths[0];
|
|
|
- const fileName = res.tempFiles[0]?.originalFileObj?.name || tempFilePath.split('/').pop() || 'unnamed-file';
|
|
|
-
|
|
|
- try {
|
|
|
- const result = await uploadMinIOWithPolicy(uploadPath, tempFilePath, fileName, callbacks);
|
|
|
- resolve(result);
|
|
|
- } catch (error) {
|
|
|
- reject(error);
|
|
|
- }
|
|
|
- },
|
|
|
- fail: reject
|
|
|
- });
|
|
|
- });
|
|
|
- } else if (isBrowser) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const input = document.createElement('input');
|
|
|
- input.type = 'file';
|
|
|
- input.accept = accept;
|
|
|
- input.multiple = count > 1;
|
|
|
-
|
|
|
- input.onchange = async (event) => {
|
|
|
- const files = (event.target as HTMLInputElement).files;
|
|
|
- if (!files || files.length === 0) {
|
|
|
- reject(new Error('未选择文件'));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const file = files[0];
|
|
|
- if (file.size > maxSize) {
|
|
|
- reject(new Error(`文件大小超过限制: ${maxSize / 1024 / 1024}MB`));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const fileName = file.name || 'unnamed-file';
|
|
|
-
|
|
|
- try {
|
|
|
- const result = await uploadMinIOWithPolicy(uploadPath, file, fileName, callbacks);
|
|
|
- resolve(result);
|
|
|
- } catch (error) {
|
|
|
- reject(error);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- input.click();
|
|
|
- });
|
|
|
- } else {
|
|
|
- throw new Error('不支持的运行环境');
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 默认导出
|
|
|
-export default {
|
|
|
- MinIOXHRMultipartUploader,
|
|
|
- MinIOXHRUploader,
|
|
|
- TaroMinIOMultipartUploader,
|
|
|
- TaroMinIOUploader,
|
|
|
- UniversalMinIOMultipartUploader,
|
|
|
- UniversalMinIOUploader,
|
|
|
- getUploadPolicy,
|
|
|
- getMultipartUploadPolicy,
|
|
|
- uploadMinIOWithPolicy,
|
|
|
- uploadFromSelect
|
|
|
-};
|