minio.ts 25 KB

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