|
@@ -1,4 +1,5 @@
|
|
|
-import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
|
|
|
|
+import type { InferResponseType } from 'hono/client';
|
|
|
|
|
+import Taro from '@tarojs/taro';
|
|
|
import { fileClient } from "../api";
|
|
import { fileClient } from "../api";
|
|
|
|
|
|
|
|
export interface MinioProgressEvent {
|
|
export interface MinioProgressEvent {
|
|
@@ -16,13 +17,13 @@ export interface MinioProgressCallbacks {
|
|
|
onProgress?: (event: MinioProgressEvent) => void;
|
|
onProgress?: (event: MinioProgressEvent) => void;
|
|
|
onComplete?: () => void;
|
|
onComplete?: () => void;
|
|
|
onError?: (error: Error) => void;
|
|
onError?: (error: Error) => void;
|
|
|
- signal?: AbortSignal;
|
|
|
|
|
|
|
+ signal?: { aborted: boolean };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export interface UploadResult {
|
|
export interface UploadResult {
|
|
|
- fileUrl:string;
|
|
|
|
|
- fileKey:string;
|
|
|
|
|
- bucketName:string;
|
|
|
|
|
|
|
+ fileUrl: string;
|
|
|
|
|
+ fileKey: string;
|
|
|
|
|
+ bucketName: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface UploadPart {
|
|
interface UploadPart {
|
|
@@ -38,25 +39,28 @@ interface UploadProgressDetails {
|
|
|
partProgress?: number;
|
|
partProgress?: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type MinioMultipartUploadPolicy = InferResponseType<typeof fileClient["multipart-policy"]['$post'],200>
|
|
|
|
|
-type MinioUploadPolicy = InferResponseType<typeof fileClient["upload-policy"]['$post'],200>
|
|
|
|
|
-
|
|
|
|
|
|
|
+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
|
|
const PART_SIZE = 5 * 1024 * 1024; // 每部分5MB
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-export class MinIOXHRMultipartUploader {
|
|
|
|
|
|
|
+export class TaroMinIOMultipartUploader {
|
|
|
/**
|
|
/**
|
|
|
- * 使用XHR分段上传文件到MinIO
|
|
|
|
|
|
|
+ * 使用 Taro.uploadFile 分段上传文件到 MinIO
|
|
|
*/
|
|
*/
|
|
|
static async upload(
|
|
static async upload(
|
|
|
policy: MinioMultipartUploadPolicy,
|
|
policy: MinioMultipartUploadPolicy,
|
|
|
- file: File | Blob,
|
|
|
|
|
|
|
+ filePath: string,
|
|
|
key: string,
|
|
key: string,
|
|
|
callbacks?: MinioProgressCallbacks
|
|
callbacks?: MinioProgressCallbacks
|
|
|
): Promise<UploadResult> {
|
|
): Promise<UploadResult> {
|
|
|
const partSize = PART_SIZE;
|
|
const partSize = PART_SIZE;
|
|
|
- const totalSize = file.size;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 获取文件信息
|
|
|
|
|
+ const fileInfo = await Taro.getFileSystemManager().getFileInfo({
|
|
|
|
|
+ filePath
|
|
|
|
|
+ });
|
|
|
|
|
+ const totalSize = fileInfo.size;
|
|
|
const totalParts = Math.ceil(totalSize / partSize);
|
|
const totalParts = Math.ceil(totalSize / partSize);
|
|
|
const uploadedParts: UploadPart[] = [];
|
|
const uploadedParts: UploadPart[] = [];
|
|
|
|
|
|
|
@@ -73,20 +77,26 @@ export class MinIOXHRMultipartUploader {
|
|
|
|
|
|
|
|
// 分段上传
|
|
// 分段上传
|
|
|
for (let i = 0; i < totalParts; i++) {
|
|
for (let i = 0; i < totalParts; i++) {
|
|
|
|
|
+ if (callbacks?.signal?.aborted) {
|
|
|
|
|
+ throw new Error('上传已取消');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const start = i * partSize;
|
|
const start = i * partSize;
|
|
|
const end = Math.min(start + partSize, totalSize);
|
|
const end = Math.min(start + partSize, totalSize);
|
|
|
- const partBlob = file.slice(start, end);
|
|
|
|
|
const partNumber = i + 1;
|
|
const partNumber = i + 1;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 读取文件片段
|
|
|
|
|
+ const partData = await this.readFileSlice(filePath, start, end);
|
|
|
|
|
+
|
|
|
const etag = await this.uploadPart(
|
|
const etag = await this.uploadPart(
|
|
|
policy.partUrls[i],
|
|
policy.partUrls[i],
|
|
|
- partBlob,
|
|
|
|
|
|
|
+ partData,
|
|
|
callbacks,
|
|
callbacks,
|
|
|
{
|
|
{
|
|
|
partNumber,
|
|
partNumber,
|
|
|
totalParts,
|
|
totalParts,
|
|
|
- partSize: partBlob.size,
|
|
|
|
|
|
|
+ partSize: end - start,
|
|
|
totalSize
|
|
totalSize
|
|
|
}
|
|
}
|
|
|
);
|
|
);
|
|
@@ -137,56 +147,82 @@ export class MinIOXHRMultipartUploader {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 读取文件片段
|
|
|
|
|
+ private static readFileSlice(filePath: string, start: number, end: number): Promise<ArrayBuffer> {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ const fs = Taro.getFileSystemManager();
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fileData = fs.readFileSync(filePath, undefined, {
|
|
|
|
|
+ position: start,
|
|
|
|
|
+ length: end - start
|
|
|
|
|
+ });
|
|
|
|
|
+ resolve(fileData);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ reject(error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 上传单个片段
|
|
// 上传单个片段
|
|
|
- private static uploadPart(
|
|
|
|
|
|
|
+ private static async uploadPart(
|
|
|
uploadUrl: string,
|
|
uploadUrl: string,
|
|
|
- partBlob: Blob,
|
|
|
|
|
|
|
+ partData: ArrayBuffer,
|
|
|
callbacks?: MinioProgressCallbacks,
|
|
callbacks?: MinioProgressCallbacks,
|
|
|
progressDetails?: UploadProgressDetails
|
|
progressDetails?: UploadProgressDetails
|
|
|
): Promise<string> {
|
|
): Promise<string> {
|
|
|
return new Promise((resolve, reject) => {
|
|
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()
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ 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}`));
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- xhr.onload = () => {
|
|
|
|
|
- if (xhr.status >= 200 && xhr.status < 300) {
|
|
|
|
|
- // 获取ETag(MinIO返回的标识)
|
|
|
|
|
- const etag = xhr.getResponseHeader('ETag')?.replace(/"/g, '') || '';
|
|
|
|
|
- resolve(etag);
|
|
|
|
|
- } else {
|
|
|
|
|
- reject(new Error(`上传片段失败: ${xhr.status} ${xhr.statusText}`));
|
|
|
|
|
|
|
+ // 由于小程序 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({
|
|
|
|
|
+ url: uploadUrl,
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'Content-Type': 'application/octet-stream'
|
|
|
|
|
+ },
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
|
|
+ const etag = res.header['ETag']?.replace(/"/g, '') || '';
|
|
|
|
|
+ resolve(etag);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reject(new Error(`上传片段失败: ${res.statusCode}`));
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ reject(new Error(`上传片段失败: ${error.errMsg}`));
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- xhr.onerror = () => reject(new Error('上传片段失败'));
|
|
|
|
|
-
|
|
|
|
|
- xhr.open('PUT', uploadUrl);
|
|
|
|
|
- xhr.send(partBlob);
|
|
|
|
|
-
|
|
|
|
|
- if (callbacks?.signal) {
|
|
|
|
|
- callbacks.signal.addEventListener('abort', () => {
|
|
|
|
|
- xhr.abort();
|
|
|
|
|
- reject(new Error('上传已取消'));
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -197,12 +233,12 @@ export class MinIOXHRMultipartUploader {
|
|
|
uploadedParts: UploadPart[]
|
|
uploadedParts: UploadPart[]
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
const response = await fileClient["multipart-complete"].$post({
|
|
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 }))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ json: {
|
|
|
|
|
+ bucket: policy.bucket,
|
|
|
|
|
+ key,
|
|
|
|
|
+ uploadId: policy.uploadId,
|
|
|
|
|
+ parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
if (!response.ok) {
|
|
@@ -211,108 +247,173 @@ export class MinIOXHRMultipartUploader {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export class MinIOXHRUploader {
|
|
|
|
|
- /**
|
|
|
|
|
- * 使用XHR上传文件到MinIO
|
|
|
|
|
- */
|
|
|
|
|
- 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]) => {
|
|
|
|
|
- // 排除 policy 中的 key、host、prefix、ossType 字段
|
|
|
|
|
- if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
|
|
|
|
|
- formData.append(k, value);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- // 添加 自定义 key 字段
|
|
|
|
|
- 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()
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+export class TaroMinIOUploader {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 使用 Taro.uploadFile 上传文件到 MinIO
|
|
|
|
|
+ */
|
|
|
|
|
+ static async upload(
|
|
|
|
|
+ policy: MinioUploadPolicy,
|
|
|
|
|
+ filePath: string,
|
|
|
|
|
+ key: string,
|
|
|
|
|
+ callbacks?: MinioProgressCallbacks
|
|
|
|
|
+ ): Promise<UploadResult> {
|
|
|
|
|
+ // 获取文件信息
|
|
|
|
|
+ const fileInfo = await Taro.getFileSystemManager().getFileInfo({
|
|
|
|
|
+ filePath
|
|
|
|
|
+ });
|
|
|
|
|
+ const totalSize = fileInfo.size;
|
|
|
|
|
+
|
|
|
|
|
+ callbacks?.onProgress?.({
|
|
|
|
|
+ stage: 'uploading',
|
|
|
|
|
+ message: '准备上传文件...',
|
|
|
|
|
+ progress: 0,
|
|
|
|
|
+ details: {
|
|
|
|
|
+ loaded: 0,
|
|
|
|
|
+ total: totalSize
|
|
|
|
|
+ },
|
|
|
|
|
+ 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({
|
|
|
|
|
+ filePath,
|
|
|
|
|
+ success: (fileData) => {
|
|
|
|
|
+ // 构建 FormData
|
|
|
|
|
+ const formDataObj = new FormData();
|
|
|
|
|
+ Object.entries(formData).forEach(([key, value]) => {
|
|
|
|
|
+ formDataObj.append(key, value);
|
|
|
|
|
+ });
|
|
|
|
|
+ formDataObj.append('file', new Blob([fileData.data]));
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 Taro.request 上传
|
|
|
|
|
+ Taro.request({
|
|
|
|
|
+ url: policy.uploadPolicy.host,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: formDataObj,
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ const error = new Error(`上传失败: ${res.statusCode}`);
|
|
|
callbacks?.onError?.(error);
|
|
callbacks?.onError?.(error);
|
|
|
reject(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) {
|
|
|
|
|
- callbacks.signal.addEventListener('abort', () => {
|
|
|
|
|
- xhr.abort();
|
|
|
|
|
- reject(new Error('上传已取消'));
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ const err = new Error(`上传失败: ${error.errMsg}`);
|
|
|
|
|
+ callbacks?.onError?.(err);
|
|
|
|
|
+ reject(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ const err = new Error(`读取文件失败: ${error.errMsg}`);
|
|
|
|
|
+ callbacks?.onError?.(err);
|
|
|
|
|
+ reject(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 由于小程序限制,我们使用更简单的 uploadFile 方式
|
|
|
|
|
+ // 但先保存临时文件
|
|
|
|
|
+ this.uploadWithTempFile(policy, filePath, key, callbacks)
|
|
|
|
|
+ .then(resolve)
|
|
|
|
|
+ .catch(reject);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static async uploadWithTempFile(
|
|
|
|
|
+ policy: MinioUploadPolicy,
|
|
|
|
|
+ filePath: 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}`));
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 兼容原类名,提供向后兼容
|
|
|
|
|
+export const MinIOXHRMultipartUploader = TaroMinIOMultipartUploader;
|
|
|
|
|
+export const MinIOXHRUploader = TaroMinIOUploader;
|
|
|
|
|
|
|
|
export async function getUploadPolicy(key: string, fileName: string, fileType?: string, fileSize?: number): Promise<MinioUploadPolicy> {
|
|
export async function getUploadPolicy(key: string, fileName: string, fileType?: string, fileSize?: number): Promise<MinioUploadPolicy> {
|
|
|
const policyResponse = await fileClient["upload-policy"].$post({
|
|
const policyResponse = await fileClient["upload-policy"].$post({
|
|
@@ -347,7 +448,7 @@ export async function getMultipartUploadPolicy(totalSize: number, fileKey: strin
|
|
|
|
|
|
|
|
export async function uploadMinIOWithPolicy(
|
|
export async function uploadMinIOWithPolicy(
|
|
|
uploadPath: string,
|
|
uploadPath: string,
|
|
|
- file: File | Blob,
|
|
|
|
|
|
|
+ filePath: string,
|
|
|
fileKey: string,
|
|
fileKey: string,
|
|
|
callbacks?: MinioProgressCallbacks
|
|
callbacks?: MinioProgressCallbacks
|
|
|
): Promise<UploadResult> {
|
|
): Promise<UploadResult> {
|
|
@@ -358,28 +459,64 @@ export async function uploadMinIOWithPolicy(
|
|
|
if(uploadPath.startsWith('/')) uploadPath = uploadPath.replace(/^\//, '');
|
|
if(uploadPath.startsWith('/')) uploadPath = uploadPath.replace(/^\//, '');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 获取文件信息
|
|
|
|
|
+ const fileInfo = await Taro.getFileSystemManager().getFileInfo({
|
|
|
|
|
+ filePath
|
|
|
|
|
+ });
|
|
|
|
|
+ const fileSize = fileInfo.size;
|
|
|
|
|
|
|
|
- if( file.size > PART_SIZE ){
|
|
|
|
|
- if (!(file instanceof File)) {
|
|
|
|
|
- throw new Error('不支持的文件类型,无法获取文件名');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if(fileSize > PART_SIZE) {
|
|
|
const policy = await getMultipartUploadPolicy(
|
|
const policy = await getMultipartUploadPolicy(
|
|
|
- file.size,
|
|
|
|
|
|
|
+ fileSize,
|
|
|
`${uploadPath}${fileKey}`,
|
|
`${uploadPath}${fileKey}`,
|
|
|
- file.type,
|
|
|
|
|
- file.name
|
|
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ fileKey
|
|
|
);
|
|
);
|
|
|
- return MinIOXHRMultipartUploader.upload(
|
|
|
|
|
|
|
+ return TaroMinIOMultipartUploader.upload(
|
|
|
policy,
|
|
policy,
|
|
|
- file,
|
|
|
|
|
|
|
+ filePath,
|
|
|
policy.key,
|
|
policy.key,
|
|
|
callbacks
|
|
callbacks
|
|
|
);
|
|
);
|
|
|
- }else{
|
|
|
|
|
- if (!(file instanceof File)) {
|
|
|
|
|
- throw new Error('不支持的文件类型,无法获取文件名');
|
|
|
|
|
- }
|
|
|
|
|
- const policy = await getUploadPolicy(`${uploadPath}${fileKey}`, file.name, file.type, file.size);
|
|
|
|
|
- return MinIOXHRUploader.upload(policy, file, policy.uploadPolicy.key, callbacks);
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const policy = await getUploadPolicy(`${uploadPath}${fileKey}`, fileKey, undefined, fileSize);
|
|
|
|
|
+ return TaroMinIOUploader.upload(policy, filePath, policy.uploadPolicy.key, callbacks);
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 新增:小程序专用的上传函数
|
|
|
|
|
+export async function uploadMinIOWithTaroFile(
|
|
|
|
|
+ uploadPath: string,
|
|
|
|
|
+ tempFilePath: string,
|
|
|
|
|
+ fileName: string,
|
|
|
|
|
+ callbacks?: MinioProgressCallbacks
|
|
|
|
|
+): Promise<UploadResult> {
|
|
|
|
|
+ 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> {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ Taro.chooseImage({
|
|
|
|
|
+ count: 1,
|
|
|
|
|
+ sourceType,
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ const tempFilePath = res.tempFilePaths[0];
|
|
|
|
|
+ const fileName = res.tempFiles[0]?.name || tempFilePath.split('/').pop() || 'unnamed-file';
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await uploadMinIOWithPolicy(uploadPath, tempFilePath, fileName, callbacks);
|
|
|
|
|
+ resolve(result);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ reject(error);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: reject
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|