|
|
@@ -1,6 +1,20 @@
|
|
|
import type { InferResponseType } from 'hono/client';
|
|
|
-import Taro from '@tarojs/taro';
|
|
|
import { fileClient } from "../api";
|
|
|
+import { isWeapp, isH5 } from './platform';
|
|
|
+
|
|
|
+// 平台检测 - 使用统一的 platform.ts
|
|
|
+const isMiniProgram = isWeapp();
|
|
|
+const isBrowser = isH5();
|
|
|
+
|
|
|
+// 如果支持 Taro,优先使用 Taro
|
|
|
+let Taro: any = null;
|
|
|
+if (isMiniProgram) {
|
|
|
+ try {
|
|
|
+ Taro = require('@tarojs/taro');
|
|
|
+ } catch (e) {
|
|
|
+ // 忽略错误,回退到原生小程序
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
export interface MinioProgressEvent {
|
|
|
stage: 'uploading' | 'complete' | 'error';
|
|
|
@@ -17,7 +31,7 @@ export interface MinioProgressCallbacks {
|
|
|
onProgress?: (event: MinioProgressEvent) => void;
|
|
|
onComplete?: () => void;
|
|
|
onError?: (error: Error) => void;
|
|
|
- signal?: { aborted: boolean };
|
|
|
+ signal?: AbortSignal | { aborted: boolean };
|
|
|
}
|
|
|
|
|
|
export interface UploadResult {
|
|
|
@@ -44,9 +58,281 @@ type MinioUploadPolicy = InferResponseType<typeof fileClient["upload-policy"]['$
|
|
|
|
|
|
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 {
|
|
|
+ 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
|
|
|
+ };
|
|
|
+ } 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<void> {
|
|
|
+ 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}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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
|
|
|
+ });
|
|
|
+ } 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.uploadFile 分段上传文件到 MinIO
|
|
|
+ * 使用 Taro 分段上传文件到 MinIO(小程序环境)
|
|
|
*/
|
|
|
static async upload(
|
|
|
policy: MinioMultipartUploadPolicy,
|
|
|
@@ -57,9 +343,9 @@ export class TaroMinIOMultipartUploader {
|
|
|
const partSize = PART_SIZE;
|
|
|
|
|
|
// 获取文件信息
|
|
|
- const fileInfo = await Taro.getFileSystemManager().getFileInfo({
|
|
|
+ const fileInfo = await Taro?.getFileSystemManager?.()?.getFileInfo({
|
|
|
filePath
|
|
|
- });
|
|
|
+ }) || { size: 0 };
|
|
|
const totalSize = fileInfo.size;
|
|
|
const totalParts = Math.ceil(totalSize / partSize);
|
|
|
const uploadedParts: UploadPart[] = [];
|
|
|
@@ -77,7 +363,7 @@ export class TaroMinIOMultipartUploader {
|
|
|
|
|
|
// 分段上传
|
|
|
for (let i = 0; i < totalParts; i++) {
|
|
|
- if (callbacks?.signal?.aborted) {
|
|
|
+ if (callbacks?.signal && 'aborted' in callbacks.signal && callbacks.signal.aborted) {
|
|
|
throw new Error('上传已取消');
|
|
|
}
|
|
|
|
|
|
@@ -148,10 +434,15 @@ export class TaroMinIOMultipartUploader {
|
|
|
}
|
|
|
|
|
|
// 读取文件片段
|
|
|
- private static readFileSlice(filePath: string, start: number, end: number): Promise<ArrayBuffer> {
|
|
|
+ private static async readFileSlice(filePath: string, start: number, end: number): Promise<ArrayBuffer> {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
- const fs = Taro.getFileSystemManager();
|
|
|
try {
|
|
|
+ const fs = Taro?.getFileSystemManager?.();
|
|
|
+ if (!fs) {
|
|
|
+ reject(new Error('小程序文件系统不可用'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const fileData = fs.readFileSync(filePath, undefined, {
|
|
|
position: start,
|
|
|
length: end - start
|
|
|
@@ -171,58 +462,25 @@ export class TaroMinIOMultipartUploader {
|
|
|
progressDetails?: UploadProgressDetails
|
|
|
): Promise<string> {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
- const uploadTask = Taro.uploadFile({
|
|
|
- url: uploadUrl,
|
|
|
- filePath: '',
|
|
|
- name: 'file',
|
|
|
- formData: {},
|
|
|
- header: {
|
|
|
- 'Content-Type': 'application/octet-stream'
|
|
|
- },
|
|
|
- success: (res) => {
|
|
|
- if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
- // 从响应头获取ETag
|
|
|
- const etag = res.header['ETag']?.replace(/"/g, '') || '';
|
|
|
- resolve(etag);
|
|
|
- } else {
|
|
|
- reject(new Error(`上传片段失败: ${res.statusCode} ${res.errMsg}`));
|
|
|
- }
|
|
|
- },
|
|
|
- fail: (error) => {
|
|
|
- reject(new Error(`上传片段失败: ${error.errMsg}`));
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 由于小程序 uploadFile 不直接支持 ArrayBuffer,我们需要使用 putFile
|
|
|
- // 改用 fetch API 上传二进制数据
|
|
|
- this.uploadBinaryData(uploadUrl, partData)
|
|
|
- .then(resolve)
|
|
|
- .catch(reject);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 上传二进制数据
|
|
|
- private static async uploadBinaryData(uploadUrl: string, data: ArrayBuffer): Promise<string> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- Taro.request({
|
|
|
+ Taro?.request?.({
|
|
|
url: uploadUrl,
|
|
|
method: 'PUT',
|
|
|
- data: data,
|
|
|
+ data: partData,
|
|
|
header: {
|
|
|
'Content-Type': 'application/octet-stream'
|
|
|
},
|
|
|
- success: (res) => {
|
|
|
+ success: (res: any) => {
|
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
- const etag = res.header['ETag']?.replace(/"/g, '') || '';
|
|
|
+ const etag = res.header?.['ETag']?.replace(/"/g, '') || '';
|
|
|
resolve(etag);
|
|
|
} else {
|
|
|
reject(new Error(`上传片段失败: ${res.statusCode}`));
|
|
|
}
|
|
|
},
|
|
|
- fail: (error) => {
|
|
|
+ fail: (error: any) => {
|
|
|
reject(new Error(`上传片段失败: ${error.errMsg}`));
|
|
|
}
|
|
|
- });
|
|
|
+ }) || reject(new Error('小程序环境不可用'));
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -249,7 +507,7 @@ export class TaroMinIOMultipartUploader {
|
|
|
|
|
|
export class TaroMinIOUploader {
|
|
|
/**
|
|
|
- * 使用 Taro.uploadFile 上传文件到 MinIO
|
|
|
+ * 使用 Taro 上传文件到 MinIO(小程序环境)
|
|
|
*/
|
|
|
static async upload(
|
|
|
policy: MinioUploadPolicy,
|
|
|
@@ -258,9 +516,9 @@ export class TaroMinIOUploader {
|
|
|
callbacks?: MinioProgressCallbacks
|
|
|
): Promise<UploadResult> {
|
|
|
// 获取文件信息
|
|
|
- const fileInfo = await Taro.getFileSystemManager().getFileInfo({
|
|
|
+ const fileInfo = await Taro?.getFileSystemManager?.()?.getFileInfo({
|
|
|
filePath
|
|
|
- });
|
|
|
+ }) || { size: 0 };
|
|
|
const totalSize = fileInfo.size;
|
|
|
|
|
|
callbacks?.onProgress?.({
|
|
|
@@ -274,40 +532,36 @@ export class TaroMinIOUploader {
|
|
|
timestamp: Date.now()
|
|
|
});
|
|
|
|
|
|
- // 准备表单数据
|
|
|
- const formData: Record<string, any> = {};
|
|
|
-
|
|
|
- // 添加 MinIO 需要的字段
|
|
|
- Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
|
|
|
- if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
|
|
|
- formData[k] = value;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 添加自定义 key 字段
|
|
|
- formData['key'] = key;
|
|
|
-
|
|
|
return new Promise((resolve, reject) => {
|
|
|
- // 使用 Taro 的文件系统读取文件
|
|
|
- Taro.getFileSystemManager().readFile({
|
|
|
+ // 准备表单数据
|
|
|
+ const formData: Record<string, any> = {};
|
|
|
+
|
|
|
+ // 添加 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?.getFileSystemManager?.()?.readFile({
|
|
|
filePath,
|
|
|
success: (fileData) => {
|
|
|
- // 构建 FormData
|
|
|
const formDataObj = new FormData();
|
|
|
- Object.entries(formData).forEach(([key, value]) => {
|
|
|
- formDataObj.append(key, value);
|
|
|
+ Object.entries(formData).forEach(([k, value]) => {
|
|
|
+ formDataObj.append(k, value);
|
|
|
});
|
|
|
formDataObj.append('file', new Blob([fileData.data]));
|
|
|
|
|
|
- // 使用 Taro.request 上传
|
|
|
- Taro.request({
|
|
|
+ Taro?.request?.({
|
|
|
url: policy.uploadPolicy.host,
|
|
|
method: 'POST',
|
|
|
data: formDataObj,
|
|
|
header: {
|
|
|
'Content-Type': 'multipart/form-data'
|
|
|
},
|
|
|
- success: (res) => {
|
|
|
+ success: (res: any) => {
|
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
callbacks?.onProgress?.({
|
|
|
stage: 'complete',
|
|
|
@@ -322,95 +576,61 @@ export class TaroMinIOUploader {
|
|
|
bucketName: policy.uploadPolicy.bucket
|
|
|
});
|
|
|
} else {
|
|
|
- const error = new Error(`上传失败: ${res.statusCode}`);
|
|
|
- callbacks?.onError?.(error);
|
|
|
- reject(error);
|
|
|
+ reject(new Error(`上传失败: ${res.statusCode}`));
|
|
|
}
|
|
|
},
|
|
|
- fail: (error) => {
|
|
|
- const err = new Error(`上传失败: ${error.errMsg}`);
|
|
|
- callbacks?.onError?.(err);
|
|
|
- reject(err);
|
|
|
+ fail: (error: any) => {
|
|
|
+ reject(new Error(`上传失败: ${error.errMsg}`));
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
- fail: (error) => {
|
|
|
- const err = new Error(`读取文件失败: ${error.errMsg}`);
|
|
|
- callbacks?.onError?.(err);
|
|
|
- reject(err);
|
|
|
+ fail: (error: any) => {
|
|
|
+ reject(new Error(`读取文件失败: ${error.errMsg}`));
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- // 由于小程序限制,我们使用更简单的 uploadFile 方式
|
|
|
- // 但先保存临时文件
|
|
|
- this.uploadWithTempFile(policy, filePath, key, callbacks)
|
|
|
- .then(resolve)
|
|
|
- .catch(reject);
|
|
|
+ }) || reject(new Error('小程序环境不可用'));
|
|
|
});
|
|
|
}
|
|
|
-
|
|
|
- private static async uploadWithTempFile(
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 统一 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,
|
|
|
- filePath: string,
|
|
|
+ file: File | Blob | string,
|
|
|
key: string,
|
|
|
callbacks?: MinioProgressCallbacks
|
|
|
): Promise<UploadResult> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- // 由于小程序的 uploadFile 只支持文件路径,我们需要构建完整的 FormData
|
|
|
- // 这里使用 request 方式上传
|
|
|
- Taro.getFileSystemManager().readFile({
|
|
|
- filePath,
|
|
|
- success: (fileData) => {
|
|
|
- const formData = new FormData();
|
|
|
-
|
|
|
- // 添加所有必需的字段
|
|
|
- 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', new Blob([fileData.data]));
|
|
|
-
|
|
|
- Taro.request({
|
|
|
- url: policy.uploadPolicy.host,
|
|
|
- method: 'POST',
|
|
|
- data: 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
|
|
|
- });
|
|
|
- } else {
|
|
|
- reject(new Error(`上传失败: ${res.statusCode}`));
|
|
|
- }
|
|
|
- },
|
|
|
- fail: (error) => {
|
|
|
- reject(new Error(`上传失败: ${error.errMsg}`));
|
|
|
- }
|
|
|
- });
|
|
|
- },
|
|
|
- fail: (error) => {
|
|
|
- reject(new Error(`读取文件失败: ${error.errMsg}`));
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
+ 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: {
|
|
|
@@ -442,63 +662,112 @@ export async function getMultipartUploadPolicy(totalSize: number, fileKey: strin
|
|
|
return await policyResponse.json();
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 统一的上传函数,自动适应运行环境
|
|
|
+ */
|
|
|
export async function uploadMinIOWithPolicy(
|
|
|
uploadPath: string,
|
|
|
- filePath: 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(/^\//, '');
|
|
|
}
|
|
|
|
|
|
- // 获取文件信息
|
|
|
- const fileInfo = await Taro.getFileSystemManager().getFileInfo({
|
|
|
- filePath
|
|
|
- });
|
|
|
- const fileSize = fileInfo.size;
|
|
|
+ let fileSize: number;
|
|
|
+ let fileType: string | undefined;
|
|
|
+ let fileName: string;
|
|
|
|
|
|
- if(fileSize > PART_SIZE) {
|
|
|
+ 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 Taro?.getFileSystemManager?.()?.getFileInfo({ filePath: file });
|
|
|
+ fileSize = fileInfo?.size || 0;
|
|
|
+ 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}`,
|
|
|
- undefined,
|
|
|
- fileKey
|
|
|
- );
|
|
|
- return TaroMinIOMultipartUploader.upload(
|
|
|
- policy,
|
|
|
- filePath,
|
|
|
- policy.key,
|
|
|
- callbacks
|
|
|
+ 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 {
|
|
|
- const policy = await getUploadPolicy(`${uploadPath}${fileKey}`, fileKey, undefined, fileSize);
|
|
|
- return TaroMinIOUploader.upload(policy, filePath, policy.uploadPolicy.key, callbacks);
|
|
|
+ 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);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 新增:小程序专用的上传函数
|
|
|
+// ==================== 小程序专用函数 ====================
|
|
|
+/**
|
|
|
+ * 小程序专用:从选择器上传
|
|
|
+ */
|
|
|
export async function uploadMinIOWithTaroFile(
|
|
|
uploadPath: string,
|
|
|
tempFilePath: string,
|
|
|
fileName: string,
|
|
|
callbacks?: MinioProgressCallbacks
|
|
|
): Promise<UploadResult> {
|
|
|
+ if (!isMiniProgram) {
|
|
|
+ throw new Error('此功能仅支持小程序环境');
|
|
|
+ }
|
|
|
const fileKey = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${fileName}`;
|
|
|
return uploadMinIOWithPolicy(uploadPath, tempFilePath, fileKey, callbacks);
|
|
|
}
|
|
|
|
|
|
-// 新增:从 chooseImage 或 chooseVideo 获取文件
|
|
|
+/**
|
|
|
+ * 小程序专用:从相册或相机选择并上传
|
|
|
+ */
|
|
|
export async function uploadFromChoose(
|
|
|
sourceType: ('album' | 'camera')[] = ['album', 'camera'],
|
|
|
uploadPath: string = '',
|
|
|
callbacks?: MinioProgressCallbacks
|
|
|
): Promise<UploadResult> {
|
|
|
+ if (!isMiniProgram) {
|
|
|
+ throw new Error('此功能仅支持小程序环境');
|
|
|
+ }
|
|
|
+
|
|
|
return new Promise((resolve, reject) => {
|
|
|
- Taro.chooseImage({
|
|
|
+ if (typeof wx === 'undefined') {
|
|
|
+ reject(new Error('小程序环境未找到'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ wx.chooseImage({
|
|
|
count: 1,
|
|
|
sourceType,
|
|
|
success: async (res) => {
|
|
|
@@ -515,4 +784,19 @@ export async function uploadFromChoose(
|
|
|
fail: reject
|
|
|
});
|
|
|
});
|
|
|
-}
|
|
|
+}
|
|
|
+
|
|
|
+// 默认导出
|
|
|
+export default {
|
|
|
+ MinIOXHRMultipartUploader,
|
|
|
+ MinIOXHRUploader,
|
|
|
+ TaroMinIOMultipartUploader,
|
|
|
+ TaroMinIOUploader,
|
|
|
+ UniversalMinIOMultipartUploader,
|
|
|
+ UniversalMinIOUploader,
|
|
|
+ getUploadPolicy,
|
|
|
+ getMultipartUploadPolicy,
|
|
|
+ uploadMinIOWithPolicy,
|
|
|
+ uploadMinIOWithTaroFile,
|
|
|
+ uploadFromChoose
|
|
|
+};
|