minio.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. import type { InferResponseType } from 'hono/client';
  2. import { fileClient } from "../api";
  3. import { isWeapp, isH5 } from './platform';
  4. // 平台检测 - 使用统一的 platform.ts
  5. const isMiniProgram = isWeapp();
  6. const isBrowser = isH5();
  7. // 如果支持 Taro,优先使用 Taro
  8. let Taro: any = null;
  9. if (isMiniProgram) {
  10. try {
  11. Taro = require('@tarojs/taro');
  12. } catch (e) {
  13. // 忽略错误,回退到原生小程序
  14. }
  15. }
  16. export interface MinioProgressEvent {
  17. stage: 'uploading' | 'complete' | 'error';
  18. message: string;
  19. progress: number;
  20. details?: {
  21. loaded: number;
  22. total: number;
  23. };
  24. timestamp: number;
  25. }
  26. export interface MinioProgressCallbacks {
  27. onProgress?: (event: MinioProgressEvent) => void;
  28. onComplete?: () => void;
  29. onError?: (error: Error) => void;
  30. signal?: AbortSignal | { aborted: boolean };
  31. }
  32. export interface UploadResult {
  33. fileUrl: string;
  34. fileKey: string;
  35. bucketName: string;
  36. }
  37. interface UploadPart {
  38. ETag: string;
  39. PartNumber: number;
  40. }
  41. interface UploadProgressDetails {
  42. partNumber: number;
  43. totalParts: number;
  44. partSize: number;
  45. totalSize: number;
  46. partProgress?: number;
  47. }
  48. type MinioMultipartUploadPolicy = InferResponseType<typeof fileClient["multipart-policy"]['$post'], 200>
  49. type MinioUploadPolicy = InferResponseType<typeof fileClient["upload-policy"]['$post'], 200>
  50. const PART_SIZE = 5 * 1024 * 1024; // 每部分5MB
  51. // ==================== H5 实现(保留原有代码) ====================
  52. export class MinIOXHRMultipartUploader {
  53. /**
  54. * 使用XHR分段上传文件到MinIO(H5环境)
  55. */
  56. static async upload(
  57. policy: MinioMultipartUploadPolicy,
  58. file: File | Blob,
  59. key: string,
  60. callbacks?: MinioProgressCallbacks
  61. ): Promise<UploadResult> {
  62. const partSize = PART_SIZE;
  63. const totalSize = file.size;
  64. const totalParts = Math.ceil(totalSize / partSize);
  65. const uploadedParts: UploadPart[] = [];
  66. callbacks?.onProgress?.({
  67. stage: 'uploading',
  68. message: '准备上传文件...',
  69. progress: 0,
  70. details: {
  71. loaded: 0,
  72. total: totalSize
  73. },
  74. timestamp: Date.now()
  75. });
  76. // 分段上传
  77. for (let i = 0; i < totalParts; i++) {
  78. const start = i * partSize;
  79. const end = Math.min(start + partSize, totalSize);
  80. const partBlob = file.slice(start, end);
  81. const partNumber = i + 1;
  82. try {
  83. const etag = await this.uploadPart(
  84. policy.partUrls[i],
  85. partBlob,
  86. callbacks,
  87. {
  88. partNumber,
  89. totalParts,
  90. partSize: partBlob.size,
  91. totalSize
  92. }
  93. );
  94. uploadedParts.push({
  95. ETag: etag,
  96. PartNumber: partNumber
  97. });
  98. // 更新进度
  99. const progress = Math.round((end / totalSize) * 100);
  100. callbacks?.onProgress?.({
  101. stage: 'uploading',
  102. message: `上传文件片段 ${partNumber}/${totalParts}`,
  103. progress,
  104. details: {
  105. loaded: end,
  106. total: totalSize,
  107. },
  108. timestamp: Date.now()
  109. });
  110. } catch (error) {
  111. callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
  112. throw error;
  113. }
  114. }
  115. // 完成上传
  116. try {
  117. await this.completeMultipartUpload(policy, key, uploadedParts);
  118. callbacks?.onProgress?.({
  119. stage: 'complete',
  120. message: '文件上传完成',
  121. progress: 100,
  122. timestamp: Date.now()
  123. });
  124. callbacks?.onComplete?.();
  125. return {
  126. fileUrl: `${policy.host}/${key}`,
  127. fileKey: key,
  128. bucketName: policy.bucket
  129. };
  130. } catch (error) {
  131. callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
  132. throw error;
  133. }
  134. }
  135. // 上传单个片段
  136. private static uploadPart(
  137. uploadUrl: string,
  138. partBlob: Blob,
  139. callbacks?: MinioProgressCallbacks,
  140. progressDetails?: UploadProgressDetails
  141. ): Promise<string> {
  142. return new Promise((resolve, reject) => {
  143. const xhr = new XMLHttpRequest();
  144. xhr.upload.onprogress = (event) => {
  145. if (event.lengthComputable && callbacks?.onProgress) {
  146. const partProgress = Math.round((event.loaded / event.total) * 100);
  147. callbacks.onProgress({
  148. stage: 'uploading',
  149. message: `上传文件片段 ${progressDetails?.partNumber}/${progressDetails?.totalParts} (${partProgress}%)`,
  150. progress: Math.round((
  151. (progressDetails?.partNumber ? (progressDetails.partNumber - 1) * (progressDetails.partSize || 0) : 0) + event.loaded
  152. ) / (progressDetails?.totalSize || 1) * 100),
  153. details: {
  154. ...progressDetails,
  155. loaded: event.loaded,
  156. total: event.total
  157. },
  158. timestamp: Date.now()
  159. });
  160. }
  161. };
  162. xhr.onload = () => {
  163. if (xhr.status >= 200 && xhr.status < 300) {
  164. const etag = xhr.getResponseHeader('ETag')?.replace(/"/g, '') || '';
  165. resolve(etag);
  166. } else {
  167. reject(new Error(`上传片段失败: ${xhr.status} ${xhr.statusText}`));
  168. }
  169. };
  170. xhr.onerror = () => reject(new Error('上传片段失败'));
  171. xhr.open('PUT', uploadUrl);
  172. xhr.send(partBlob);
  173. if (callbacks?.signal) {
  174. if ('addEventListener' in callbacks.signal) {
  175. callbacks.signal.addEventListener('abort', () => {
  176. xhr.abort();
  177. reject(new Error('上传已取消'));
  178. });
  179. }
  180. }
  181. });
  182. }
  183. // 完成分段上传
  184. private static async completeMultipartUpload(
  185. policy: MinioMultipartUploadPolicy,
  186. key: string,
  187. uploadedParts: UploadPart[]
  188. ): Promise<void> {
  189. const response = await fileClient["multipart-complete"].$post({
  190. json: {
  191. bucket: policy.bucket,
  192. key,
  193. uploadId: policy.uploadId,
  194. parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
  195. }
  196. });
  197. if (!response.ok) {
  198. throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
  199. }
  200. }
  201. }
  202. export class MinIOXHRUploader {
  203. /**
  204. * 使用XHR上传文件到MinIO(H5环境)
  205. */
  206. static upload(
  207. policy: MinioUploadPolicy,
  208. file: File | Blob,
  209. key: string,
  210. callbacks?: MinioProgressCallbacks
  211. ): Promise<UploadResult> {
  212. const formData = new FormData();
  213. // 添加 MinIO 需要的字段
  214. Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
  215. if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
  216. formData.append(k, value);
  217. }
  218. });
  219. formData.append('key', key);
  220. formData.append('file', file);
  221. return new Promise((resolve, reject) => {
  222. const xhr = new XMLHttpRequest();
  223. // 上传进度处理
  224. if (callbacks?.onProgress) {
  225. xhr.upload.onprogress = (event) => {
  226. if (event.lengthComputable) {
  227. callbacks.onProgress?.({
  228. stage: 'uploading',
  229. message: '正在上传文件...',
  230. progress: Math.round((event.loaded * 100) / event.total),
  231. details: {
  232. loaded: event.loaded,
  233. total: event.total
  234. },
  235. timestamp: Date.now()
  236. });
  237. }
  238. };
  239. }
  240. // 完成处理
  241. xhr.onload = () => {
  242. if (xhr.status >= 200 && xhr.status < 300) {
  243. if (callbacks?.onProgress) {
  244. callbacks.onProgress({
  245. stage: 'complete',
  246. message: '文件上传完成',
  247. progress: 100,
  248. timestamp: Date.now()
  249. });
  250. }
  251. callbacks?.onComplete?.();
  252. resolve({
  253. fileUrl: `${policy.uploadPolicy.host}/${key}`,
  254. fileKey: key,
  255. bucketName: policy.uploadPolicy.bucket
  256. });
  257. } else {
  258. const error = new Error(`上传失败: ${xhr.status} ${xhr.statusText}`);
  259. callbacks?.onError?.(error);
  260. reject(error);
  261. }
  262. };
  263. // 错误处理
  264. xhr.onerror = () => {
  265. const error = new Error('上传失败');
  266. if (callbacks?.onProgress) {
  267. callbacks.onProgress({
  268. stage: 'error',
  269. message: '文件上传失败',
  270. progress: 0,
  271. timestamp: Date.now()
  272. });
  273. }
  274. callbacks?.onError?.(error);
  275. reject(error);
  276. };
  277. // 根据当前页面协议和 host 配置决定最终的上传地址
  278. const currentProtocol = typeof window !== 'undefined' ? window.location.protocol : 'https:';
  279. const host = policy.uploadPolicy.host?.startsWith('http')
  280. ? policy.uploadPolicy.host
  281. : `${currentProtocol}//${policy.uploadPolicy.host}`;
  282. xhr.open('POST', host);
  283. xhr.send(formData);
  284. // 处理取消
  285. if (callbacks?.signal) {
  286. if ('addEventListener' in callbacks.signal) {
  287. callbacks.signal.addEventListener('abort', () => {
  288. xhr.abort();
  289. reject(new Error('上传已取消'));
  290. });
  291. }
  292. }
  293. });
  294. }
  295. }
  296. // ==================== 小程序实现 ====================
  297. export class TaroMinIOMultipartUploader {
  298. /**
  299. * 使用 Taro 分段上传文件到 MinIO(小程序环境)
  300. */
  301. static async upload(
  302. policy: MinioMultipartUploadPolicy,
  303. filePath: string,
  304. key: string,
  305. callbacks?: MinioProgressCallbacks
  306. ): Promise<UploadResult> {
  307. const partSize = PART_SIZE;
  308. // 获取文件信息
  309. const fileInfo = await Taro?.getFileSystemManager?.()?.getFileInfo({
  310. filePath
  311. }) || { size: 0 };
  312. const totalSize = fileInfo.size;
  313. const totalParts = Math.ceil(totalSize / partSize);
  314. const uploadedParts: UploadPart[] = [];
  315. callbacks?.onProgress?.({
  316. stage: 'uploading',
  317. message: '准备上传文件...',
  318. progress: 0,
  319. details: {
  320. loaded: 0,
  321. total: totalSize
  322. },
  323. timestamp: Date.now()
  324. });
  325. // 分段上传
  326. for (let i = 0; i < totalParts; i++) {
  327. if (callbacks?.signal && 'aborted' in callbacks.signal && callbacks.signal.aborted) {
  328. throw new Error('上传已取消');
  329. }
  330. const start = i * partSize;
  331. const end = Math.min(start + partSize, totalSize);
  332. const partNumber = i + 1;
  333. try {
  334. // 读取文件片段
  335. const partData = await this.readFileSlice(filePath, start, end);
  336. const etag = await this.uploadPart(
  337. policy.partUrls[i],
  338. partData,
  339. callbacks,
  340. {
  341. partNumber,
  342. totalParts,
  343. partSize: end - start,
  344. totalSize
  345. }
  346. );
  347. uploadedParts.push({
  348. ETag: etag,
  349. PartNumber: partNumber
  350. });
  351. // 更新进度
  352. const progress = Math.round((end / totalSize) * 100);
  353. callbacks?.onProgress?.({
  354. stage: 'uploading',
  355. message: `上传文件片段 ${partNumber}/${totalParts}`,
  356. progress,
  357. details: {
  358. loaded: end,
  359. total: totalSize,
  360. },
  361. timestamp: Date.now()
  362. });
  363. } catch (error) {
  364. callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
  365. throw error;
  366. }
  367. }
  368. // 完成上传
  369. try {
  370. await this.completeMultipartUpload(policy, key, uploadedParts);
  371. callbacks?.onProgress?.({
  372. stage: 'complete',
  373. message: '文件上传完成',
  374. progress: 100,
  375. timestamp: Date.now()
  376. });
  377. callbacks?.onComplete?.();
  378. return {
  379. fileUrl: `${policy.host}/${key}`,
  380. fileKey: key,
  381. bucketName: policy.bucket
  382. };
  383. } catch (error) {
  384. callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
  385. throw error;
  386. }
  387. }
  388. // 读取文件片段
  389. private static async readFileSlice(filePath: string, start: number, end: number): Promise<ArrayBuffer> {
  390. return new Promise((resolve, reject) => {
  391. try {
  392. const fs = Taro?.getFileSystemManager?.();
  393. if (!fs) {
  394. reject(new Error('小程序文件系统不可用'));
  395. return;
  396. }
  397. const fileData = fs.readFileSync(filePath, undefined, {
  398. position: start,
  399. length: end - start
  400. });
  401. resolve(fileData);
  402. } catch (error) {
  403. reject(error);
  404. }
  405. });
  406. }
  407. // 上传单个片段
  408. private static async uploadPart(
  409. uploadUrl: string,
  410. partData: ArrayBuffer,
  411. callbacks?: MinioProgressCallbacks,
  412. progressDetails?: UploadProgressDetails
  413. ): Promise<string> {
  414. return new Promise((resolve, reject) => {
  415. Taro?.request?.({
  416. url: uploadUrl,
  417. method: 'PUT',
  418. data: partData,
  419. header: {
  420. 'Content-Type': 'application/octet-stream'
  421. },
  422. success: (res: any) => {
  423. if (res.statusCode >= 200 && res.statusCode < 300) {
  424. const etag = res.header?.['ETag']?.replace(/"/g, '') || '';
  425. resolve(etag);
  426. } else {
  427. reject(new Error(`上传片段失败: ${res.statusCode}`));
  428. }
  429. },
  430. fail: (error: any) => {
  431. reject(new Error(`上传片段失败: ${error.errMsg}`));
  432. }
  433. }) || reject(new Error('小程序环境不可用'));
  434. });
  435. }
  436. // 完成分段上传
  437. private static async completeMultipartUpload(
  438. policy: MinioMultipartUploadPolicy,
  439. key: string,
  440. uploadedParts: UploadPart[]
  441. ): Promise<void> {
  442. const response = await fileClient["multipart-complete"].$post({
  443. json: {
  444. bucket: policy.bucket,
  445. key,
  446. uploadId: policy.uploadId,
  447. parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
  448. }
  449. });
  450. if (!response.ok) {
  451. throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
  452. }
  453. }
  454. }
  455. export class TaroMinIOUploader {
  456. /**
  457. * 使用 Taro 上传文件到 MinIO(小程序环境)
  458. */
  459. static async upload(
  460. policy: MinioUploadPolicy,
  461. filePath: string,
  462. key: string,
  463. callbacks?: MinioProgressCallbacks
  464. ): Promise<UploadResult> {
  465. // 获取文件信息
  466. const fileInfo = await Taro?.getFileSystemManager?.()?.getFileInfo({
  467. filePath
  468. }) || { size: 0 };
  469. const totalSize = fileInfo.size;
  470. callbacks?.onProgress?.({
  471. stage: 'uploading',
  472. message: '准备上传文件...',
  473. progress: 0,
  474. details: {
  475. loaded: 0,
  476. total: totalSize
  477. },
  478. timestamp: Date.now()
  479. });
  480. return new Promise((resolve, reject) => {
  481. // 准备表单数据
  482. const formData: Record<string, any> = {};
  483. // 添加 MinIO 需要的字段
  484. Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
  485. if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
  486. formData[k] = value;
  487. }
  488. });
  489. formData['key'] = key;
  490. Taro?.getFileSystemManager?.()?.readFile({
  491. filePath,
  492. success: (fileData) => {
  493. const formDataObj = new FormData();
  494. Object.entries(formData).forEach(([k, value]) => {
  495. formDataObj.append(k, value);
  496. });
  497. formDataObj.append('file', new Blob([fileData.data]));
  498. Taro?.request?.({
  499. url: policy.uploadPolicy.host,
  500. method: 'POST',
  501. data: formDataObj,
  502. header: {
  503. 'Content-Type': 'multipart/form-data'
  504. },
  505. success: (res: any) => {
  506. if (res.statusCode >= 200 && res.statusCode < 300) {
  507. callbacks?.onProgress?.({
  508. stage: 'complete',
  509. message: '文件上传完成',
  510. progress: 100,
  511. timestamp: Date.now()
  512. });
  513. callbacks?.onComplete?.();
  514. resolve({
  515. fileUrl: `${policy.uploadPolicy.host}/${key}`,
  516. fileKey: key,
  517. bucketName: policy.uploadPolicy.bucket
  518. });
  519. } else {
  520. reject(new Error(`上传失败: ${res.statusCode}`));
  521. }
  522. },
  523. fail: (error: any) => {
  524. reject(new Error(`上传失败: ${error.errMsg}`));
  525. }
  526. });
  527. },
  528. fail: (error: any) => {
  529. reject(new Error(`读取文件失败: ${error.errMsg}`));
  530. }
  531. }) || reject(new Error('小程序环境不可用'));
  532. });
  533. }
  534. }
  535. // ==================== 统一 API ====================
  536. /**
  537. * 根据运行环境自动选择合适的上传器
  538. */
  539. export class UniversalMinIOMultipartUploader {
  540. static async upload(
  541. policy: MinioMultipartUploadPolicy,
  542. file: File | Blob | string,
  543. key: string,
  544. callbacks?: MinioProgressCallbacks
  545. ): Promise<UploadResult> {
  546. if (isBrowser && (file instanceof File || file instanceof Blob)) {
  547. return MinIOXHRMultipartUploader.upload(policy, file, key, callbacks);
  548. } else if (isMiniProgram && typeof file === 'string') {
  549. return TaroMinIOMultipartUploader.upload(policy, file, key, callbacks);
  550. } else {
  551. throw new Error('不支持的运行环境或文件类型');
  552. }
  553. }
  554. }
  555. export class UniversalMinIOUploader {
  556. static async upload(
  557. policy: MinioUploadPolicy,
  558. file: File | Blob | string,
  559. key: string,
  560. callbacks?: MinioProgressCallbacks
  561. ): Promise<UploadResult> {
  562. if (isBrowser && (file instanceof File || file instanceof Blob)) {
  563. return MinIOXHRUploader.upload(policy, file, key, callbacks);
  564. } else if (isMiniProgram && typeof file === 'string') {
  565. return TaroMinIOUploader.upload(policy, file, key, callbacks);
  566. } else {
  567. throw new Error('不支持的运行环境或文件类型');
  568. }
  569. }
  570. }
  571. // ==================== 通用函数 ====================
  572. export async function getUploadPolicy(key: string, fileName: string, fileType?: string, fileSize?: number): Promise<MinioUploadPolicy> {
  573. const policyResponse = await fileClient["upload-policy"].$post({
  574. json: {
  575. path: key,
  576. name: fileName,
  577. type: fileType,
  578. size: fileSize
  579. }
  580. });
  581. if (!policyResponse.ok) {
  582. throw new Error('获取上传策略失败');
  583. }
  584. return policyResponse.json();
  585. }
  586. export async function getMultipartUploadPolicy(totalSize: number, fileKey: string, fileType?: string, fileName: string = 'unnamed-file') {
  587. const policyResponse = await fileClient["multipart-policy"].$post({
  588. json: {
  589. totalSize,
  590. partSize: PART_SIZE,
  591. fileKey,
  592. type: fileType,
  593. name: fileName
  594. }
  595. });
  596. if (!policyResponse.ok) {
  597. throw new Error('获取分段上传策略失败');
  598. }
  599. return await policyResponse.json();
  600. }
  601. /**
  602. * 统一的上传函数,自动适应运行环境
  603. */
  604. export async function uploadMinIOWithPolicy(
  605. uploadPath: string,
  606. file: File | Blob | string,
  607. fileKey: string,
  608. callbacks?: MinioProgressCallbacks
  609. ): Promise<UploadResult> {
  610. if(uploadPath === '/') uploadPath = '';
  611. else{
  612. if(!uploadPath.endsWith('/')) uploadPath = `${uploadPath}/`
  613. if(uploadPath.startsWith('/')) uploadPath = uploadPath.replace(/^\//, '');
  614. }
  615. let fileSize: number;
  616. let fileType: string | undefined;
  617. let fileName: string;
  618. if (isBrowser && (file instanceof File || file instanceof Blob)) {
  619. fileSize = file.size;
  620. fileType = (file as File).type || undefined;
  621. fileName = (file as File).name || fileKey;
  622. } else if (isMiniProgram && typeof file === 'string') {
  623. try {
  624. const fileInfo = await Taro?.getFileSystemManager?.()?.getFileInfo({ filePath: file });
  625. fileSize = fileInfo?.size || 0;
  626. fileType = undefined;
  627. fileName = fileKey;
  628. } catch {
  629. fileSize = 0;
  630. fileType = undefined;
  631. fileName = fileKey;
  632. }
  633. } else {
  634. throw new Error('不支持的文件类型');
  635. }
  636. if (fileSize > PART_SIZE) {
  637. if (isBrowser && !(file instanceof File)) {
  638. throw new Error('不支持的文件类型,无法获取文件名');
  639. }
  640. const policy = await getMultipartUploadPolicy(
  641. fileSize,
  642. `${uploadPath}${fileKey}`,
  643. fileType,
  644. fileName
  645. );
  646. if (isBrowser) {
  647. return MinIOXHRMultipartUploader.upload(policy, file as File | Blob, policy.key, callbacks);
  648. } else {
  649. return TaroMinIOMultipartUploader.upload(policy, file as string, policy.key, callbacks);
  650. }
  651. } else {
  652. if (isBrowser && !(file instanceof File)) {
  653. throw new Error('不支持的文件类型,无法获取文件名');
  654. }
  655. const policy = await getUploadPolicy(`${uploadPath}${fileKey}`, fileName, fileType, fileSize);
  656. if (isBrowser) {
  657. return MinIOXHRUploader.upload(policy, file as File | Blob, policy.uploadPolicy.key, callbacks);
  658. } else {
  659. return TaroMinIOUploader.upload(policy, file as string, policy.uploadPolicy.key, callbacks);
  660. }
  661. }
  662. }
  663. // ==================== 小程序专用函数 ====================
  664. /**
  665. * 小程序专用:从选择器上传
  666. */
  667. export async function uploadMinIOWithTaroFile(
  668. uploadPath: string,
  669. tempFilePath: string,
  670. fileName: string,
  671. callbacks?: MinioProgressCallbacks
  672. ): Promise<UploadResult> {
  673. if (!isMiniProgram) {
  674. throw new Error('此功能仅支持小程序环境');
  675. }
  676. const fileKey = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${fileName}`;
  677. return uploadMinIOWithPolicy(uploadPath, tempFilePath, fileKey, callbacks);
  678. }
  679. /**
  680. * 小程序专用:从相册或相机选择并上传
  681. */
  682. export async function uploadFromChoose(
  683. sourceType: ('album' | 'camera')[] = ['album', 'camera'],
  684. uploadPath: string = '',
  685. callbacks?: MinioProgressCallbacks
  686. ): Promise<UploadResult> {
  687. if (!isMiniProgram) {
  688. throw new Error('此功能仅支持小程序环境');
  689. }
  690. return new Promise((resolve, reject) => {
  691. if (typeof wx === 'undefined') {
  692. reject(new Error('小程序环境未找到'));
  693. return;
  694. }
  695. wx.chooseImage({
  696. count: 1,
  697. sourceType,
  698. success: async (res) => {
  699. const tempFilePath = res.tempFilePaths[0];
  700. const fileName = res.tempFiles[0]?.name || tempFilePath.split('/').pop() || 'unnamed-file';
  701. try {
  702. const result = await uploadMinIOWithPolicy(uploadPath, tempFilePath, fileName, callbacks);
  703. resolve(result);
  704. } catch (error) {
  705. reject(error);
  706. }
  707. },
  708. fail: reject
  709. });
  710. });
  711. }
  712. // 默认导出
  713. export default {
  714. MinIOXHRMultipartUploader,
  715. MinIOXHRUploader,
  716. TaroMinIOMultipartUploader,
  717. TaroMinIOUploader,
  718. UniversalMinIOMultipartUploader,
  719. UniversalMinIOUploader,
  720. getUploadPolicy,
  721. getMultipartUploadPolicy,
  722. uploadMinIOWithPolicy,
  723. uploadMinIOWithTaroFile,
  724. uploadFromChoose
  725. };