minio.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  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. if (isMiniProgram) {
  9. try {
  10. Taro = require('@tarojs/taro');
  11. } catch (e) {
  12. // 忽略错误,回退到原生小程序
  13. }
  14. }
  15. export interface MinioProgressEvent {
  16. stage: 'uploading' | 'complete' | 'error';
  17. message: string;
  18. progress: number;
  19. details?: {
  20. loaded: number;
  21. total: number;
  22. };
  23. timestamp: number;
  24. }
  25. export interface MinioProgressCallbacks {
  26. onProgress?: (event: MinioProgressEvent) => void;
  27. onComplete?: () => void;
  28. onError?: (error: Error) => void;
  29. signal?: AbortSignal | { aborted: boolean };
  30. }
  31. export interface UploadResult {
  32. fileUrl: string;
  33. fileKey: string;
  34. bucketName: string;
  35. fileId: number;
  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. fileId: result.file.id
  130. };
  131. } catch (error) {
  132. callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
  133. throw error;
  134. }
  135. }
  136. // 上传单个片段
  137. private static uploadPart(
  138. uploadUrl: string,
  139. partBlob: Blob,
  140. callbacks?: MinioProgressCallbacks,
  141. progressDetails?: UploadProgressDetails
  142. ): Promise<string> {
  143. return new Promise((resolve, reject) => {
  144. const xhr = new XMLHttpRequest();
  145. xhr.upload.onprogress = (event) => {
  146. if (event.lengthComputable && callbacks?.onProgress) {
  147. const partProgress = Math.round((event.loaded / event.total) * 100);
  148. callbacks.onProgress({
  149. stage: 'uploading',
  150. message: `上传文件片段 ${progressDetails?.partNumber}/${progressDetails?.totalParts} (${partProgress}%)`,
  151. progress: Math.round((
  152. (progressDetails?.partNumber ? (progressDetails.partNumber - 1) * (progressDetails.partSize || 0) : 0) + event.loaded
  153. ) / (progressDetails?.totalSize || 1) * 100),
  154. details: {
  155. ...progressDetails,
  156. loaded: event.loaded,
  157. total: event.total
  158. },
  159. timestamp: Date.now()
  160. });
  161. }
  162. };
  163. xhr.onload = () => {
  164. if (xhr.status >= 200 && xhr.status < 300) {
  165. const etag = xhr.getResponseHeader('ETag')?.replace(/"/g, '') || '';
  166. resolve(etag);
  167. } else {
  168. reject(new Error(`上传片段失败: ${xhr.status} ${xhr.statusText}`));
  169. }
  170. };
  171. xhr.onerror = () => reject(new Error('上传片段失败'));
  172. xhr.open('PUT', uploadUrl);
  173. xhr.send(partBlob);
  174. if (callbacks?.signal) {
  175. if ('addEventListener' in callbacks.signal) {
  176. callbacks.signal.addEventListener('abort', () => {
  177. xhr.abort();
  178. reject(new Error('上传已取消'));
  179. });
  180. }
  181. }
  182. });
  183. }
  184. // 完成分段上传
  185. private static async completeMultipartUpload(
  186. policy: MinioMultipartUploadPolicy,
  187. key: string,
  188. uploadedParts: UploadPart[]
  189. ): Promise<{ fileId: number }> {
  190. const response = await fileClient["multipart-complete"].$post({
  191. json: {
  192. bucket: policy.bucket,
  193. key,
  194. uploadId: policy.uploadId,
  195. parts: uploadedParts.map(part => ({ partNumber: part.PartNumber, etag: part.ETag }))
  196. }
  197. });
  198. if (!response.ok) {
  199. throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`);
  200. }
  201. return response.json();
  202. }
  203. }
  204. export class MinIOXHRUploader {
  205. /**
  206. * 使用XHR上传文件到MinIO(H5环境)
  207. */
  208. static upload(
  209. policy: MinioUploadPolicy,
  210. file: File | Blob,
  211. key: string,
  212. callbacks?: MinioProgressCallbacks
  213. ): Promise<UploadResult> {
  214. const formData = new FormData();
  215. // 添加 MinIO 需要的字段
  216. Object.entries(policy.uploadPolicy).forEach(([k, value]) => {
  217. if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') {
  218. formData.append(k, value);
  219. }
  220. });
  221. formData.append('key', key);
  222. formData.append('file', file);
  223. return new Promise((resolve, reject) => {
  224. const xhr = new XMLHttpRequest();
  225. // 上传进度处理
  226. if (callbacks?.onProgress) {
  227. xhr.upload.onprogress = (event) => {
  228. if (event.lengthComputable) {
  229. callbacks.onProgress?.({
  230. stage: 'uploading',
  231. message: '正在上传文件...',
  232. progress: Math.round((event.loaded * 100) / event.total),
  233. details: {
  234. loaded: event.loaded,
  235. total: event.total
  236. },
  237. timestamp: Date.now()
  238. });
  239. }
  240. };
  241. }
  242. // 完成处理
  243. xhr.onload = () => {
  244. if (xhr.status >= 200 && xhr.status < 300) {
  245. if (callbacks?.onProgress) {
  246. callbacks.onProgress({
  247. stage: 'complete',
  248. message: '文件上传完成',
  249. progress: 100,
  250. timestamp: Date.now()
  251. });
  252. }
  253. callbacks?.onComplete?.();
  254. resolve({
  255. fileUrl: `${policy.uploadPolicy.host}/${key}`,
  256. fileKey: key,
  257. bucketName: policy.uploadPolicy.bucket,
  258. fileId: result.file.id
  259. });
  260. } else {
  261. const error = new Error(`上传失败: ${xhr.status} ${xhr.statusText}`);
  262. callbacks?.onError?.(error);
  263. reject(error);
  264. }
  265. };
  266. // 错误处理
  267. xhr.onerror = () => {
  268. const error = new Error('上传失败');
  269. if (callbacks?.onProgress) {
  270. callbacks.onProgress({
  271. stage: 'error',
  272. message: '文件上传失败',
  273. progress: 0,
  274. timestamp: Date.now()
  275. });
  276. }
  277. callbacks?.onError?.(error);
  278. reject(error);
  279. };
  280. // 根据当前页面协议和 host 配置决定最终的上传地址
  281. const currentProtocol = typeof window !== 'undefined' ? window.location.protocol : 'https:';
  282. const host = policy.uploadPolicy.host?.startsWith('http')
  283. ? policy.uploadPolicy.host
  284. : `${currentProtocol}//${policy.uploadPolicy.host}`;
  285. xhr.open('POST', host);
  286. xhr.send(formData);
  287. // 处理取消
  288. if (callbacks?.signal) {
  289. if ('addEventListener' in callbacks.signal) {
  290. callbacks.signal.addEventListener('abort', () => {
  291. xhr.abort();
  292. reject(new Error('上传已取消'));
  293. });
  294. }
  295. }
  296. });
  297. }
  298. }
  299. // ==================== 小程序实现 ====================
  300. export class TaroMinIOMultipartUploader {
  301. /**
  302. * 使用 Taro 分段上传文件到 MinIO(小程序环境)
  303. */
  304. static async upload(
  305. policy: MinioMultipartUploadPolicy,
  306. filePath: string,
  307. key: string,
  308. callbacks?: MinioProgressCallbacks
  309. ): Promise<UploadResult> {
  310. const partSize = PART_SIZE;
  311. // 获取文件信息
  312. const fileInfo = await getFileInfoPromise(filePath);
  313. const totalSize = fileInfo.size;
  314. const totalParts = Math.ceil(totalSize / partSize);
  315. const uploadedParts: UploadPart[] = [];
  316. callbacks?.onProgress?.({
  317. stage: 'uploading',
  318. message: '准备上传文件...',
  319. progress: 0,
  320. details: {
  321. loaded: 0,
  322. total: totalSize
  323. },
  324. timestamp: Date.now()
  325. });
  326. // 分段上传
  327. for (let i = 0; i < totalParts; i++) {
  328. if (callbacks?.signal && 'aborted' in callbacks.signal && callbacks.signal.aborted) {
  329. throw new Error('上传已取消');
  330. }
  331. const start = i * partSize;
  332. const end = Math.min(start + partSize, totalSize);
  333. const partNumber = i + 1;
  334. try {
  335. // 读取文件片段
  336. const partData = await this.readFileSlice(filePath, start, end);
  337. const etag = await this.uploadPart(
  338. policy.partUrls[i],
  339. partData,
  340. callbacks,
  341. {
  342. partNumber,
  343. totalParts,
  344. partSize: end - start,
  345. totalSize
  346. }
  347. );
  348. uploadedParts.push({
  349. ETag: etag,
  350. PartNumber: partNumber
  351. });
  352. // 更新进度
  353. const progress = Math.round((end / totalSize) * 100);
  354. callbacks?.onProgress?.({
  355. stage: 'uploading',
  356. message: `上传文件片段 ${partNumber}/${totalParts}`,
  357. progress,
  358. details: {
  359. loaded: end,
  360. total: totalSize,
  361. },
  362. timestamp: Date.now()
  363. });
  364. } catch (error) {
  365. callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
  366. throw error;
  367. }
  368. }
  369. // 完成上传
  370. try {
  371. const result = await this.completeMultipartUpload(policy, key, uploadedParts);
  372. callbacks?.onProgress?.({
  373. stage: 'complete',
  374. message: '文件上传完成',
  375. progress: 100,
  376. timestamp: Date.now()
  377. });
  378. callbacks?.onComplete?.();
  379. return {
  380. fileUrl: `${policy.host}/${key}`,
  381. fileKey: key,
  382. bucketName: policy.bucket,
  383. fileId: result.fileId
  384. };
  385. } catch (error) {
  386. callbacks?.onError?.(error instanceof Error ? error : new Error(String(error)));
  387. throw error;
  388. }
  389. }
  390. // 读取文件片段
  391. private static async readFileSlice(filePath: string, start: number, end: number): Promise<ArrayBuffer> {
  392. return new Promise((resolve, reject) => {
  393. try {
  394. const fs = Taro?.getFileSystemManager?.();
  395. if (!fs) {
  396. reject(new Error('小程序文件系统不可用'));
  397. return;
  398. }
  399. const fileData = fs.readFileSync(filePath, undefined, {
  400. position: start,
  401. length: end - start
  402. });
  403. resolve(fileData);
  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. // 准备表单数据
  483. const formData: Record<string, any> = {};
  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?.getFileSystemManager?.()?.readFile({
  492. filePath,
  493. success: (fileData) => {
  494. const formDataObj = new FormData();
  495. Object.entries(formData).forEach(([k, value]) => {
  496. formDataObj.append(k, value);
  497. });
  498. formDataObj.append('file', new Blob([fileData.data]));
  499. Taro?.request?.({
  500. url: policy.uploadPolicy.host,
  501. method: 'POST',
  502. data: formDataObj,
  503. header: {
  504. 'Content-Type': 'multipart/form-data'
  505. },
  506. success: (res: any) => {
  507. if (res.statusCode >= 200 && res.statusCode < 300) {
  508. callbacks?.onProgress?.({
  509. stage: 'complete',
  510. message: '文件上传完成',
  511. progress: 100,
  512. timestamp: Date.now()
  513. });
  514. callbacks?.onComplete?.();
  515. resolve({
  516. fileUrl: `${policy.uploadPolicy.host}/${key}`,
  517. fileKey: key,
  518. bucketName: policy.uploadPolicy.bucket,
  519. fileId: result.file.id
  520. });
  521. } else {
  522. reject(new Error(`上传失败: ${res.statusCode}`));
  523. }
  524. },
  525. fail: (error: any) => {
  526. reject(new Error(`上传失败: ${error.errMsg}`));
  527. }
  528. });
  529. },
  530. fail: (error: any) => {
  531. reject(new Error(`读取文件失败: ${error.errMsg}`));
  532. }
  533. }) || reject(new Error('小程序环境不可用'));
  534. });
  535. }
  536. }
  537. // ==================== 统一 API ====================
  538. /**
  539. * 根据运行环境自动选择合适的上传器
  540. */
  541. export class UniversalMinIOMultipartUploader {
  542. static async upload(
  543. policy: MinioMultipartUploadPolicy,
  544. file: File | Blob | string,
  545. key: string,
  546. callbacks?: MinioProgressCallbacks
  547. ): Promise<UploadResult> {
  548. if (isBrowser && (file instanceof File || file instanceof Blob)) {
  549. return MinIOXHRMultipartUploader.upload(policy, file, key, callbacks);
  550. } else if (isMiniProgram && typeof file === 'string') {
  551. return TaroMinIOMultipartUploader.upload(policy, file, key, callbacks);
  552. } else {
  553. throw new Error('不支持的运行环境或文件类型');
  554. }
  555. }
  556. }
  557. export class UniversalMinIOUploader {
  558. static async upload(
  559. policy: MinioUploadPolicy,
  560. file: File | Blob | string,
  561. key: string,
  562. callbacks?: MinioProgressCallbacks
  563. ): Promise<UploadResult> {
  564. if (isBrowser && (file instanceof File || file instanceof Blob)) {
  565. return MinIOXHRUploader.upload(policy, file, key, callbacks);
  566. } else if (isMiniProgram && typeof file === 'string') {
  567. return TaroMinIOUploader.upload(policy, file, key, callbacks);
  568. } else {
  569. throw new Error('不支持的运行环境或文件类型');
  570. }
  571. }
  572. }
  573. // ==================== 通用函数 ====================
  574. export async function getUploadPolicy(key: string, fileName: string, fileType?: string, fileSize?: number): Promise<MinioUploadPolicy> {
  575. const policyResponse = await fileClient["upload-policy"].$post({
  576. json: {
  577. path: key,
  578. name: fileName,
  579. type: fileType,
  580. size: fileSize
  581. }
  582. });
  583. if (!policyResponse.ok) {
  584. throw new Error('获取上传策略失败');
  585. }
  586. return policyResponse.json();
  587. }
  588. export async function getMultipartUploadPolicy(totalSize: number, fileKey: string, fileType?: string, fileName: string = 'unnamed-file') {
  589. const policyResponse = await fileClient["multipart-policy"].$post({
  590. json: {
  591. totalSize,
  592. partSize: PART_SIZE,
  593. fileKey,
  594. type: fileType,
  595. name: fileName
  596. }
  597. });
  598. if (!policyResponse.ok) {
  599. throw new Error('获取分段上传策略失败');
  600. }
  601. return await policyResponse.json();
  602. }
  603. /**
  604. * 统一的上传函数,自动适应运行环境
  605. */
  606. export async function uploadMinIOWithPolicy(
  607. uploadPath: string,
  608. file: File | Blob | string,
  609. fileKey: string,
  610. callbacks?: MinioProgressCallbacks
  611. ): Promise<UploadResult> {
  612. if(uploadPath === '/') uploadPath = '';
  613. else{
  614. if(!uploadPath.endsWith('/')) uploadPath = `${uploadPath}/`
  615. if(uploadPath.startsWith('/')) uploadPath = uploadPath.replace(/^\//, '');
  616. }
  617. let fileSize: number;
  618. let fileType: string | undefined;
  619. let fileName: string;
  620. if (isBrowser && (file instanceof File || file instanceof Blob)) {
  621. fileSize = file.size;
  622. fileType = (file as File).type || undefined;
  623. fileName = (file as File).name || fileKey;
  624. } else if (isMiniProgram && typeof file === 'string') {
  625. try {
  626. const fileInfo = await getFileInfoPromise(file);
  627. fileSize = fileInfo.size;
  628. fileType = undefined;
  629. fileName = fileKey;
  630. } catch {
  631. fileSize = 0;
  632. fileType = undefined;
  633. fileName = fileKey;
  634. }
  635. } else {
  636. throw new Error('不支持的文件类型');
  637. }
  638. if (fileSize > PART_SIZE) {
  639. if (isBrowser && !(file instanceof File)) {
  640. throw new Error('不支持的文件类型,无法获取文件名');
  641. }
  642. const policy = await getMultipartUploadPolicy(
  643. fileSize,
  644. `${uploadPath}${fileKey}`,
  645. fileType,
  646. fileName
  647. );
  648. if (isBrowser) {
  649. return MinIOXHRMultipartUploader.upload(policy, file as File | Blob, policy.key, callbacks);
  650. } else {
  651. return TaroMinIOMultipartUploader.upload(policy, file as string, policy.key, callbacks);
  652. }
  653. } else {
  654. if (isBrowser && !(file instanceof File)) {
  655. throw new Error('不支持的文件类型,无法获取文件名');
  656. }
  657. const policy = await getUploadPolicy(`${uploadPath}${fileKey}`, fileName, fileType, fileSize);
  658. if (isBrowser) {
  659. return MinIOXHRUploader.upload(policy, file as File | Blob, policy.uploadPolicy.key, callbacks);
  660. } else {
  661. return TaroMinIOUploader.upload(policy, file as string, policy.uploadPolicy.key, callbacks);
  662. }
  663. }
  664. }
  665. // ==================== 小程序工具函数 ====================
  666. /**
  667. * Promise封装的getFileInfo函数
  668. */
  669. async function getFileInfoPromise(filePath: string): Promise<{ size: number }> {
  670. return new Promise((resolve, reject) => {
  671. const fs = Taro?.getFileSystemManager?.();
  672. if (!fs) {
  673. reject(new Error('小程序文件系统不可用'));
  674. return;
  675. }
  676. fs.getFileInfo({
  677. filePath,
  678. success: (res) => {
  679. resolve({ size: res.size });
  680. },
  681. fail: (error) => {
  682. reject(new Error(`获取文件信息失败: ${error.errMsg}`));
  683. }
  684. });
  685. });
  686. }
  687. // ==================== 小程序专用函数 ====================
  688. /**
  689. * 小程序专用:从选择器上传
  690. */
  691. export async function uploadMinIOWithTaroFile(
  692. uploadPath: string,
  693. tempFilePath: string,
  694. fileName: string,
  695. callbacks?: MinioProgressCallbacks
  696. ): Promise<UploadResult> {
  697. if (!isMiniProgram) {
  698. throw new Error('此功能仅支持小程序环境');
  699. }
  700. const fileKey = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${fileName}`;
  701. return uploadMinIOWithPolicy(uploadPath, tempFilePath, fileKey, callbacks);
  702. }
  703. // 新增:自动适应运行环境的文件选择并上传函数
  704. /**
  705. * 自动适应运行环境:选择文件并上传到 MinIO
  706. * 小程序:使用 Taro.chooseImage
  707. * H5:使用 input[type="file"]
  708. */
  709. export async function uploadFromSelect(
  710. uploadPath: string = '',
  711. options: {
  712. sourceType?: ('album' | 'camera')[],
  713. count?: number,
  714. accept?: string,
  715. maxSize?: number,
  716. } = {},
  717. callbacks?: MinioProgressCallbacks
  718. ): Promise<UploadResult> {
  719. const { sourceType = ['album', 'camera'], count = 1, accept = '*', maxSize = 10 * 1024 * 1024 } = options;
  720. if (isMiniProgram) {
  721. return new Promise((resolve, reject) => {
  722. if (typeof Taro === 'undefined') {
  723. reject(new Error('Taro 环境未找到'));
  724. return;
  725. }
  726. Taro.chooseImage({
  727. count,
  728. sourceType: sourceType as any, // 确保类型兼容
  729. success: async (res) => {
  730. const tempFilePath = res.tempFilePaths[0];
  731. const fileName = res.tempFiles[0]?.name || tempFilePath.split('/').pop() || 'unnamed-file';
  732. try {
  733. const result = await uploadMinIOWithPolicy(uploadPath, tempFilePath, fileName, callbacks);
  734. resolve(result);
  735. } catch (error) {
  736. reject(error);
  737. }
  738. },
  739. fail: reject
  740. });
  741. });
  742. } else if (isBrowser) {
  743. return new Promise((resolve, reject) => {
  744. const input = document.createElement('input');
  745. input.type = 'file';
  746. input.accept = accept;
  747. input.multiple = count > 1;
  748. input.onchange = async (event) => {
  749. const files = (event.target as HTMLInputElement).files;
  750. if (!files || files.length === 0) {
  751. reject(new Error('未选择文件'));
  752. return;
  753. }
  754. const file = files[0];
  755. if (file.size > maxSize) {
  756. reject(new Error(`文件大小超过限制: ${maxSize / 1024 / 1024}MB`));
  757. return;
  758. }
  759. const fileName = file.name || 'unnamed-file';
  760. try {
  761. const result = await uploadMinIOWithPolicy(uploadPath, file, fileName, callbacks);
  762. resolve(result);
  763. } catch (error) {
  764. reject(error);
  765. }
  766. };
  767. input.click();
  768. });
  769. } else {
  770. throw new Error('不支持的运行环境');
  771. }
  772. }
  773. // 默认导出
  774. export default {
  775. MinIOXHRMultipartUploader,
  776. MinIOXHRUploader,
  777. TaroMinIOMultipartUploader,
  778. TaroMinIOUploader,
  779. UniversalMinIOMultipartUploader,
  780. UniversalMinIOUploader,
  781. getUploadPolicy,
  782. getMultipartUploadPolicy,
  783. uploadMinIOWithPolicy,
  784. uploadMinIOWithTaroFile,
  785. uploadFromChoose,
  786. uploadFromSelect
  787. };