@cloudbase/storage 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,7 +1,685 @@
1
1
  import { ICloudbase } from '@cloudbase/types'
2
2
  import { ICloudbaseComponent } from '@cloudbase/types/component'
3
- import { CloudbaseStorage, COMPONENT_NAME, ICloudbaseContext } from './storage'
4
- import { SupabaseFileAPILikeStorage } from './supabase'
3
+ import { Camelize, FileBody, FileObject, FileObjectV2, FileOptions, TransformOptions } from './supabase/types'
4
+ import { isStorageError, StorageError } from './supabase/errors'
5
+ import { constants, utils, helpers } from '@cloudbase/utilities'
6
+ import {
7
+ ICloudbaseFileMetaDataRes,
8
+ ICloudbaseFileInfo,
9
+ ICloudbaseUploadFileParams,
10
+ ICloudbaseUploadFileResult,
11
+ ICloudbaseGetUploadMetadataParams,
12
+ ICloudbaseDeleteFileParams,
13
+ ICloudbaseDeleteFileResult,
14
+ ICloudbaseGetTempFileURLResult,
15
+ ICloudbaseGetTempFileURLParams,
16
+ ICloudbaseDownloadFileResult,
17
+ ICloudbaseDownloadFileParams,
18
+ ICloudbaseCopyFileParams,
19
+ ICloudbaseCopyFileResult,
20
+ } from '@cloudbase/types/storage'
21
+
22
+ enum EUploadMethod {
23
+ put = 'put',
24
+ post = 'post',
25
+ }
26
+
27
+ interface IUploadMetadata {
28
+ url: string
29
+ token: string
30
+ authorization: string
31
+ fileId: string
32
+ cosFileId: string
33
+ download_url?: string
34
+ }
35
+
36
+ const { getSdkName, ERRORS, COMMUNITY_SITE_URL } = constants
37
+ const { isArray, isString, isPalinObject, execCallback } = utils
38
+ /** catchErrorsDecorator 装饰器要放在index入口文件,意面有的编译找不到其sourcemap,例如HBuilderX */
39
+ const { catchErrorsDecorator } = helpers
40
+
41
+ export const COMPONENT_NAME = 'storage'
42
+
43
+ const storageGateWay = {
44
+ getUploadInfo: async (
45
+ request,
46
+ params: { path: string; headers?: Record<string, string> },
47
+ customReqOpts: ICloudbaseUploadFileParams['customReqOpts'],
48
+ ) => {
49
+ let res = await request.gateWay(
50
+ {
51
+ path: 'storages',
52
+ name: 'get-objects-upload-info',
53
+ data: [
54
+ {
55
+ objectId: params.path,
56
+ ...(params.headers ? { signedHeader: params.headers } : {}),
57
+ },
58
+ ],
59
+ },
60
+ customReqOpts,
61
+ )
62
+ const data = res.data?.[0] || {}
63
+
64
+ res = {
65
+ ...res,
66
+ data: {
67
+ ...(data.code ? { ...data } : {}),
68
+ authorization: data.authorization,
69
+ token: data.token,
70
+ url: data.uploadUrl,
71
+ fileId: data.cloudObjectId,
72
+ cosFileId: data.cloudObjectMeta,
73
+ download_url: data.downloadUrl,
74
+ },
75
+ }
76
+
77
+ return res
78
+ },
79
+ getDownLoadInfo: async (
80
+ request,
81
+ params: { convertedFileList: Array<{ fileid: string }> },
82
+ customReqOpts: ICloudbaseUploadFileParams['customReqOpts'],
83
+ ) => {
84
+ let res = await request.gateWay(
85
+ {
86
+ path: 'storages',
87
+ name: 'get-objects-download-info',
88
+ data: params.convertedFileList.map((v: any) => ({ cloudObjectId: v.fileid })),
89
+ },
90
+ customReqOpts,
91
+ )
92
+ res = {
93
+ ...res,
94
+ data: {
95
+ download_list: res.data?.map(v => ({
96
+ code: v.code || 'SUCCESS',
97
+ message: v.message,
98
+ fileid: v.cloudObjectId,
99
+ download_url: v.downloadUrl,
100
+ fileID: v.cloudObjectId,
101
+ tempFileURL: v.downloadUrl,
102
+ })),
103
+ },
104
+ }
105
+
106
+ return res
107
+ },
108
+ delete: async (
109
+ request,
110
+ params: { fileList: Array<string> },
111
+ customReqOpts: ICloudbaseUploadFileParams['customReqOpts'],
112
+ ) => {
113
+ let res = await request.gateWay(
114
+ {
115
+ path: 'storages',
116
+ name: 'delete-objects',
117
+ data: params.fileList.map(v => ({ cloudObjectId: v })),
118
+ },
119
+ customReqOpts,
120
+ )
121
+ res = {
122
+ ...res,
123
+ data: {
124
+ delete_list: res.data?.map(v => ({
125
+ code: v.code || 'SUCCESS',
126
+ fileID: v.cloudObjectId,
127
+ message: v.message,
128
+ })),
129
+ },
130
+ }
131
+
132
+ return res
133
+ },
134
+ copyFile: async (
135
+ request,
136
+ params: {
137
+ convertedFileList: Array<{ src_path: string; dst_path: string; overwrite: boolean; remove_original: boolean }>
138
+ },
139
+ customReqOpts: ICloudbaseUploadFileParams['customReqOpts'],
140
+ ) => {
141
+ let res = await request.gateWay(
142
+ {
143
+ path: 'storages',
144
+ name: 'copy-objects',
145
+ data: params.convertedFileList.map((v: any) => ({
146
+ srcPath: v.src_path,
147
+ dstPath: v.dst_path,
148
+ overwrite: v.overwrite,
149
+ removeOriginal: v.remove_original,
150
+ })),
151
+ },
152
+ customReqOpts,
153
+ )
154
+ res = {
155
+ ...res,
156
+ data: {
157
+ copy_list: res.data?.map(v => ({
158
+ code: v.code || 'SUCCESS',
159
+ fileID: v.cloudObjectId,
160
+ message: v.message,
161
+ })),
162
+ },
163
+ }
164
+
165
+ return res
166
+ },
167
+ }
168
+
169
+ export interface ICloudbaseContext {
170
+ config: any
171
+ request: any
172
+ }
173
+
174
+ export class CloudbaseStorage {
175
+ public isGateWay() {
176
+ // @ts-ignore
177
+ const { config } = this
178
+ const endPointMode = config.endPointMode || 'CLOUD_API'
179
+
180
+ return endPointMode === 'GATEWAY'
181
+ }
182
+
183
+ @catchErrorsDecorator({
184
+ customInfo: {
185
+ className: 'Cloudbase',
186
+ methodName: 'uploadFile',
187
+ },
188
+ title: '上传文件失败',
189
+ messages: [
190
+ '请确认以下各项:',
191
+ ' 1 - 调用 uploadFile() 的语法或参数是否正确',
192
+ ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety',
193
+ ' 3 - 云存储安全规则是否限制了当前登录状态访问',
194
+ `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
195
+ ],
196
+ })
197
+ public async uploadFile(
198
+ params: Omit<ICloudbaseUploadFileParams, 'filePath'> & { filePath?: string },
199
+ callback?: Function,
200
+ ): Promise<ICloudbaseUploadFileResult> {
201
+ const { cloudPath, filePath, onUploadProgress, method = 'put', headers = {}, fileContent } = params
202
+ if (!isString(cloudPath) || (!filePath && !fileContent)) {
203
+ throw new Error(JSON.stringify({
204
+ code: ERRORS.INVALID_PARAMS,
205
+ msg: `[${COMPONENT_NAME}.uploadFile] invalid params`,
206
+ }),)
207
+ }
208
+ const uploadMethod = { put: EUploadMethod.put, post: EUploadMethod.post }[method.toLocaleLowerCase()] || EUploadMethod.put
209
+
210
+ // 调用 getUploadMetadata 获取上传元数据
211
+ const metadataResult = await this.getUploadMetadata({
212
+ cloudPath,
213
+ method: uploadMethod,
214
+ headers: uploadMethod === EUploadMethod.put ? headers : undefined,
215
+ customReqOpts: params.customReqOpts,
216
+ })
217
+
218
+ const { data: metadata, requestId } = metadataResult
219
+ const { url, authorization, token, fileId, cosFileId, download_url: downloadUrl } = metadata
220
+
221
+ const commonParams = {
222
+ url,
223
+ file: filePath,
224
+ name: cloudPath,
225
+ onUploadProgress,
226
+ fileContent,
227
+ fileId,
228
+ requestId,
229
+ }
230
+
231
+ const putParams = {
232
+ ...commonParams,
233
+ method: EUploadMethod.put,
234
+ headers: {
235
+ ...headers,
236
+ authorization,
237
+ 'x-cos-meta-fileid': cosFileId,
238
+ 'x-cos-security-token': token,
239
+ },
240
+ }
241
+
242
+ const postParams = {
243
+ ...commonParams,
244
+ method: EUploadMethod.post,
245
+ data: {
246
+ key: cloudPath,
247
+ signature: authorization,
248
+ 'x-cos-meta-fileid': cosFileId,
249
+ success_action_status: '201',
250
+ 'x-cos-security-token': token,
251
+ },
252
+ }
253
+
254
+ const uploadConfig = {
255
+ [EUploadMethod.put]: {
256
+ params: putParams,
257
+ isSuccess: (code: number) => code >= 200 && code < 300,
258
+ },
259
+ [EUploadMethod.post]: {
260
+ params: postParams,
261
+ isSuccess: (code: number) => code === 201,
262
+ },
263
+ }
264
+
265
+ // @ts-ignore
266
+ const res = await this.request.upload(uploadConfig[uploadMethod].params)
267
+
268
+ if (uploadConfig[uploadMethod].isSuccess(res.statusCode)) {
269
+ return execCallback(callback, null, {
270
+ fileID: fileId,
271
+ download_url: downloadUrl,
272
+ requestId,
273
+ })
274
+ }
275
+ return execCallback(
276
+ callback,
277
+ new Error(`[${getSdkName()}][${ERRORS.OPERATION_FAIL}][${COMPONENT_NAME}]:${res.data}`),
278
+ )
279
+ }
280
+ @catchErrorsDecorator({
281
+ customInfo: {
282
+ className: 'Cloudbase',
283
+ methodName: 'getUploadMetadata',
284
+ },
285
+ title: '获取上传元信息失败',
286
+ messages: [
287
+ '请确认以下各项:',
288
+ ' 1 - 调用 getUploadMetadata() 的语法或参数是否正确',
289
+ ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety',
290
+ ' 3 - 云存储安全规则是否限制了当前登录状态访问',
291
+ `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
292
+ ],
293
+ })
294
+ public async getUploadMetadata(
295
+ params: ICloudbaseGetUploadMetadataParams & {
296
+ method?: EUploadMethod
297
+ headers?: Record<string, string>
298
+ },
299
+ callback?: Function,
300
+ ): Promise<{ data: IUploadMetadata; requestId: string }> {
301
+ const { cloudPath, method, headers } = params
302
+ if (!isString(cloudPath)) {
303
+ throw new Error(JSON.stringify({
304
+ code: ERRORS.INVALID_PARAMS,
305
+ msg: `[${COMPONENT_NAME}.getUploadMetadata] invalid cloudPath`,
306
+ }),)
307
+ }
308
+ // @ts-ignore
309
+ const { request } = this
310
+ const action = 'storage.getUploadMetadata'
311
+
312
+ try {
313
+ let metaData: ICloudbaseFileMetaDataRes
314
+
315
+ const metaDataParam: {
316
+ path: string
317
+ method?: EUploadMethod
318
+ headers?: Record<string, string>
319
+ } = { path: cloudPath }
320
+
321
+ if (method) {
322
+ metaDataParam.method = method
323
+ }
324
+ if (method === EUploadMethod.put && headers) {
325
+ metaDataParam.headers = headers
326
+ }
327
+
328
+ if (this.isGateWay()) {
329
+ metaData = await storageGateWay.getUploadInfo(request, metaDataParam, params.customReqOpts)
330
+ } else {
331
+ metaData = await request.send(action, metaDataParam, params.customReqOpts)
332
+ }
333
+ return execCallback(callback, null, metaData)
334
+ } catch (err) {
335
+ return execCallback(callback, err)
336
+ }
337
+ }
338
+ @catchErrorsDecorator({
339
+ customInfo: {
340
+ className: 'Cloudbase',
341
+ methodName: 'deleteFile',
342
+ },
343
+ title: '删除文件失败',
344
+ messages: [
345
+ '请确认以下各项:',
346
+ ' 1 - 调用 deleteFile() 的语法或参数是否正确',
347
+ ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety',
348
+ ' 3 - 云存储安全规则是否限制了当前登录状态访问',
349
+ `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
350
+ ],
351
+ })
352
+ public async deleteFile(
353
+ params: ICloudbaseDeleteFileParams,
354
+ callback?: Function,
355
+ ): Promise<ICloudbaseDeleteFileResult> {
356
+ const { fileList } = params
357
+
358
+ if (!fileList || !isArray(fileList) || fileList.length === 0) {
359
+ throw new Error(JSON.stringify({
360
+ code: ERRORS.INVALID_PARAMS,
361
+ msg: `[${COMPONENT_NAME}.deleteFile] fileList must not be empty`,
362
+ }),)
363
+ }
364
+
365
+ for (const fileId of fileList) {
366
+ if (!fileId || !isString(fileId)) {
367
+ throw new Error(JSON.stringify({
368
+ code: ERRORS.INVALID_PARAMS,
369
+ msg: `[${COMPONENT_NAME}.deleteFile] fileID must be string`,
370
+ }),)
371
+ }
372
+ }
373
+
374
+ const action = 'storage.batchDeleteFile'
375
+ // @ts-ignore
376
+ const { request } = this
377
+ let res: any = {}
378
+
379
+ if (this.isGateWay()) {
380
+ res = await storageGateWay.delete(request, { fileList }, params.customReqOpts)
381
+ } else {
382
+ res = await request.send(
383
+ action,
384
+ {
385
+ fileid_list: fileList,
386
+ },
387
+ params.customReqOpts,
388
+ )
389
+ }
390
+
391
+ if (res.code) {
392
+ return execCallback(callback, null, res)
393
+ }
394
+ const data = {
395
+ fileList: res.data.delete_list,
396
+ requestId: res.requestId,
397
+ }
398
+ return execCallback(callback, null, data)
399
+ }
400
+ @catchErrorsDecorator({
401
+ customInfo: {
402
+ className: 'Cloudbase',
403
+ methodName: 'getTempFileURL',
404
+ },
405
+ title: '获取文件下载链接',
406
+ messages: [
407
+ '请确认以下各项:',
408
+ ' 1 - 调用 getTempFileURL() 的语法或参数是否正确',
409
+ ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety',
410
+ ' 3 - 云存储安全规则是否限制了当前登录状态访问',
411
+ `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
412
+ ],
413
+ })
414
+ public async getTempFileURL(
415
+ params: ICloudbaseGetTempFileURLParams,
416
+ callback?: Function,
417
+ ): Promise<ICloudbaseGetTempFileURLResult> {
418
+ const { fileList } = params
419
+
420
+ if (!fileList || !isArray(fileList) || fileList.length === 0) {
421
+ throw new Error(JSON.stringify({
422
+ code: ERRORS.INVALID_PARAMS,
423
+ msg: `[${COMPONENT_NAME}.getTempFileURL] fileList must not be empty`,
424
+ }),)
425
+ }
426
+
427
+ const convertedFileList = []
428
+ for (const file of fileList) {
429
+ if (isPalinObject(file)) {
430
+ if (!Object.prototype.hasOwnProperty.call(file, 'fileID')) {
431
+ throw new Error(JSON.stringify({
432
+ code: ERRORS.INVALID_PARAMS,
433
+ msg: `[${COMPONENT_NAME}.getTempFileURL] file info must include fileID`,
434
+ }),)
435
+ }
436
+
437
+ convertedFileList.push({
438
+ fileid: (file as ICloudbaseFileInfo).fileID,
439
+ max_age: (file as ICloudbaseFileInfo).maxAge || 7200,
440
+ })
441
+ } else if (isString(file)) {
442
+ convertedFileList.push({
443
+ fileid: file,
444
+ })
445
+ } else {
446
+ throw new Error(JSON.stringify({
447
+ code: ERRORS.INVALID_PARAMS,
448
+ msg: `[${COMPONENT_NAME}.getTempFileURL] invalid fileList`,
449
+ }),)
450
+ }
451
+ }
452
+
453
+ const action = 'storage.batchGetDownloadUrl'
454
+ // @ts-ignore
455
+ const { request } = this
456
+ let res: any = {}
457
+
458
+ if (this.isGateWay()) {
459
+ res = await storageGateWay.getDownLoadInfo(request, { convertedFileList }, params.customReqOpts)
460
+ } else {
461
+ res = await request.send(action, { file_list: convertedFileList }, params.customReqOpts)
462
+ }
463
+
464
+ if (res.code) {
465
+ return execCallback(callback, null, res)
466
+ }
467
+
468
+ return execCallback(callback, null, {
469
+ fileList: res.data.download_list,
470
+ requestId: res.requestId,
471
+ })
472
+ }
473
+ @catchErrorsDecorator({
474
+ customInfo: {
475
+ className: 'Cloudbase',
476
+ methodName: 'downloadFile',
477
+ },
478
+ title: '下载文件失败',
479
+ messages: [
480
+ '请确认以下各项:',
481
+ ' 1 - 调用 downloadFile() 的语法或参数是否正确',
482
+ ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety',
483
+ ' 3 - 云存储安全规则是否限制了当前登录状态访问',
484
+ `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
485
+ ],
486
+ })
487
+ public async downloadFile(
488
+ params: ICloudbaseDownloadFileParams,
489
+ callback?: Function,
490
+ ): Promise<ICloudbaseDownloadFileResult> {
491
+ const { fileID } = params
492
+ if (!isString(fileID)) {
493
+ throw new Error(JSON.stringify({
494
+ code: ERRORS.INVALID_PARAMS,
495
+ msg: `[${COMPONENT_NAME}.getTempFileURL] fileID must be string`,
496
+ }),)
497
+ }
498
+
499
+ const tmpUrlRes = await this.getTempFileURL.call(this, {
500
+ fileList: [
501
+ {
502
+ fileID,
503
+ maxAge: 600,
504
+ },
505
+ ],
506
+ customReqOpts: params.customReqOpts,
507
+ })
508
+
509
+ const res = tmpUrlRes.fileList[0]
510
+
511
+ if (res.code !== 'SUCCESS') {
512
+ return execCallback(callback, res)
513
+ }
514
+ // @ts-ignore
515
+ const { request } = this
516
+
517
+ const tmpUrl = encodeURI(res.download_url)
518
+
519
+ const result = await request.download({ url: tmpUrl, tempFilePath: params.tempFilePath })
520
+ return execCallback(callback, null, result)
521
+ }
522
+ @catchErrorsDecorator({
523
+ customInfo: {
524
+ className: 'Cloudbase',
525
+ methodName: 'copyFile',
526
+ },
527
+ title: '批量复制文件',
528
+ messages: [
529
+ '请确认以下各项:',
530
+ ' 1 - 调用 copyFile() 的语法或参数是否正确',
531
+ ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety',
532
+ ' 3 - 云存储安全规则是否限制了当前登录状态访问',
533
+ `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
534
+ ],
535
+ })
536
+ public async copyFile(params: ICloudbaseCopyFileParams, callback?: Function): Promise<ICloudbaseCopyFileResult> {
537
+ const { fileList } = params
538
+
539
+ if (!fileList || !isArray(fileList) || fileList.length === 0) {
540
+ throw new Error(JSON.stringify({
541
+ code: ERRORS.INVALID_PARAMS,
542
+ msg: `[${COMPONENT_NAME}.copyFile] fileList must not be empty`,
543
+ }),)
544
+ }
545
+
546
+ const convertedFileList = []
547
+
548
+ for (const file of fileList) {
549
+ const { srcPath, dstPath } = file
550
+ if (!srcPath || !dstPath || typeof srcPath !== 'string' || typeof dstPath !== 'string') {
551
+ throw new Error(JSON.stringify({
552
+ code: ERRORS.INVALID_PARAMS,
553
+ msg: `[${COMPONENT_NAME}.copyFile] srcPath and dstPath may not be empty`,
554
+ }),)
555
+ }
556
+ if (srcPath === dstPath) {
557
+ throw new Error(JSON.stringify({
558
+ code: ERRORS.INVALID_PARAMS,
559
+ msg: `[${COMPONENT_NAME}.copyFile] srcPath and dstPath can not be the same`,
560
+ }),)
561
+ }
562
+ // if (basename(srcPath) !== basename(dstPath)) {
563
+ // throw new Error(JSON.stringify({
564
+ // code: ERRORS.INVALID_PARAMS,
565
+ // msg: `[${COMPONENT_NAME}.copyFile] srcPath and dstPath file name must be the same`,
566
+ // }),)
567
+ // }
568
+ convertedFileList.push({
569
+ src_path: srcPath,
570
+ dst_path: dstPath,
571
+ overwrite: file.overwrite,
572
+ remove_original: file.removeOriginal,
573
+ })
574
+ }
575
+
576
+ // @ts-ignore
577
+ const { request } = this
578
+ let res: any = {}
579
+
580
+ if (this.isGateWay()) {
581
+ res = await storageGateWay.copyFile(request, { convertedFileList }, params.customReqOpts)
582
+ } else {
583
+ const action = 'storage.batchCopyFile'
584
+ res = await request.send(action, { file_list: convertedFileList }, params.customReqOpts)
585
+ }
586
+
587
+ if (res.code) {
588
+ return execCallback(callback, null, res)
589
+ }
590
+
591
+ return execCallback(callback, null, {
592
+ fileList: res.data.copy_list,
593
+ requestId: res.requestId,
594
+ })
595
+ }
596
+
597
+ public async getFileInfo(params: ICloudbaseGetTempFileURLParams): Promise<{
598
+ fileList: (Pick<ICloudbaseGetTempFileURLResult['fileList'][0], 'code' | 'fileID' | 'tempFileURL' | 'message'> & {
599
+ fileName?: string
600
+ cloudId?: ICloudbaseGetTempFileURLResult['fileList'][0]['fileID']
601
+ contentType?: string
602
+ mime?: string
603
+ size?: number
604
+ cacheControl?: string
605
+ lastModified?: string
606
+ etag?: string
607
+ })[]
608
+ requestId: string
609
+ }> {
610
+ const fileInfo = await this.getTempFileURL(params)
611
+
612
+ if (fileInfo?.fileList && fileInfo?.fileList?.length > 0) {
613
+ const fileList = await Promise.all(fileInfo.fileList.map(async (item: ICloudbaseGetTempFileURLResult['fileList'][0]) => {
614
+ if (item.code !== 'SUCCESS') {
615
+ return {
616
+ code: item.code,
617
+ fileID: item.fileID,
618
+ tempFileURL: item.tempFileURL,
619
+ message: item.message,
620
+ }
621
+ }
622
+
623
+ try {
624
+ // @ts-ignore
625
+ const { request } = this
626
+ const res = await request.fetch({ url: item.tempFileURL, method: 'HEAD' })
627
+ let { header } = res
628
+
629
+ if (Headers && header instanceof Headers) {
630
+ header = Object.fromEntries(res.header.entries())
631
+ }
632
+ // eslint-disable-next-line radix
633
+ const fileSize = parseInt(header['content-length']) || 0
634
+ const contentType = header['content-type'] || ''
635
+
636
+ if ([400, 404].includes(Number(res.statusCode))) {
637
+ return {
638
+ code: 'FILE_NOT_FOUND',
639
+ fileID: item.fileID,
640
+ tempFileURL: item.tempFileURL,
641
+ message: 'file not found',
642
+ }
643
+ }
644
+
645
+ const fileInfo = {
646
+ code: item.code,
647
+ fileID: item.fileID,
648
+ tempFileURL: item.tempFileURL,
649
+ cloudId: item.fileID,
650
+ fileName: item.fileID.split('/').pop(),
651
+ contentType,
652
+ mime: contentType.split(';')[0].trim(),
653
+ size: fileSize,
654
+ etag: header.etag || '',
655
+ lastModified: header['last-modified'] || '',
656
+ cacheControl: header['cache-control'] || '',
657
+ }
658
+
659
+ return fileInfo
660
+ } catch (e) {
661
+ return {
662
+ code: 'FETCH_FILE_INFO_ERROR',
663
+ fileID: item.fileID,
664
+ tempFileURL: item.tempFileURL,
665
+ message: e.message,
666
+ }
667
+ }
668
+ }),)
669
+
670
+ return {
671
+ fileList,
672
+ requestId: fileInfo.requestId,
673
+ }
674
+ }
675
+
676
+ return {
677
+ fileList: [],
678
+ requestId: fileInfo.requestId,
679
+ }
680
+ }
681
+ }
682
+
5
683
 
6
684
  declare const cloudbase: ICloudbase
7
685
 
@@ -20,6 +698,744 @@ const component: ICloudbaseComponent = {
20
698
  },
21
699
  }
22
700
 
701
+ export class SupabaseFileAPILikeStorage extends CloudbaseStorage {
702
+ private shouldThrowOnError = false
703
+ private bucketId = ''
704
+ private context: ICloudbaseContext
705
+
706
+ constructor(context?: ICloudbaseContext) {
707
+ super()
708
+ this.context = context
709
+ }
710
+
711
+ get config() {
712
+ // @ts-ignore
713
+ return this.context?.config
714
+ }
715
+
716
+ get request() {
717
+ // @ts-ignore
718
+ return this.context?.request
719
+ }
720
+
721
+ throwOnError(): this {
722
+ this.shouldThrowOnError = true
723
+ return this
724
+ }
725
+
726
+ from(bucket?: string) {
727
+ this.bucketId = bucket || ''
728
+ return this
729
+ }
730
+
731
+ async upload(
732
+ path: string,
733
+ fileBody: FileBody,
734
+ fileOptions?: FileOptions,
735
+ ): Promise<
736
+ { data: { id: string; path: string; fullPath: string }; error: null } | { data: null; error: StorageError }
737
+ > {
738
+ const options = { upsert: true, ...fileOptions }
739
+ const { cacheControl, contentType, metadata } = options
740
+ try {
741
+ const cloudPath = this._getCloudPath(path)
742
+ const uploadFileParams: Parameters<CloudbaseStorage['uploadFile']>[0] = {
743
+ cloudPath,
744
+ filePath: fileBody as any,
745
+ }
746
+
747
+ if (cacheControl || contentType || metadata) {
748
+ const headers = {}
749
+ if (cacheControl) {
750
+ headers['cache-control'] = cacheControl
751
+ }
752
+ if (contentType) {
753
+ headers['content-type'] = contentType
754
+ }
755
+
756
+ if (metadata) {
757
+ headers['x-cos-metadata-metadata'] = this.toBase64(JSON.stringify(metadata))
758
+ }
759
+
760
+ uploadFileParams.headers = headers
761
+ }
762
+
763
+ const result = await this.uploadFile(uploadFileParams)
764
+
765
+ // IUploadFileRes 没有 code 字段,如果没有 fileID 则表示失败
766
+ if (!result.fileID) {
767
+ throw new Error(JSON.stringify({
768
+ code: ERRORS.OPERATION_FAIL,
769
+ msg: `[${COMPONENT_NAME}.update] no fileID returned`,
770
+ }),)
771
+ }
772
+
773
+ return {
774
+ data: {
775
+ id: result.fileID,
776
+ path,
777
+ fullPath: path,
778
+ },
779
+ error: null,
780
+ }
781
+ } catch (error: any) {
782
+ if (this.shouldThrowOnError) throw error
783
+ if (isStorageError(error)) {
784
+ return {
785
+ data: null,
786
+ error,
787
+ }
788
+ }
789
+ throw error
790
+ }
791
+ }
792
+
793
+ async uploadToSignedUrl(
794
+ path: string,
795
+ _token: string,
796
+ fileBody: FileBody,
797
+ fileOptions?: FileOptions,
798
+ ): Promise<
799
+ { data: { id: string; path: string; fullPath: string }; error: null } | { data: null; error: StorageError }
800
+ > {
801
+ return this.upload(path, fileBody, fileOptions)
802
+ }
803
+
804
+ async createSignedUploadUrl(path: string): Promise<
805
+ | {
806
+ data: {
807
+ signedUrl: string
808
+ token: string
809
+ path: string
810
+ // CloudBase 额外的元数据字段
811
+ authorization?: string
812
+ id?: string
813
+ cosFileId?: string
814
+ downloadUrl?: string
815
+ }
816
+ error: null
817
+ }
818
+ | { data: null; error: StorageError }
819
+ > {
820
+ try {
821
+ const cloudPath = this._getCloudPath(path)
822
+
823
+ const { data: metadata } = await this.getUploadMetadata({ cloudPath })
824
+
825
+ return {
826
+ data: {
827
+ signedUrl: metadata.url,
828
+ token: metadata.token,
829
+ path,
830
+ // 返回 CloudBase 的额外元数据,供 uploadToSignedUrl 使用
831
+ authorization: metadata.authorization,
832
+ id: metadata.fileId,
833
+ cosFileId: metadata.cosFileId,
834
+ downloadUrl: metadata.download_url,
835
+ },
836
+ error: null,
837
+ }
838
+ } catch (error: any) {
839
+ if (this.shouldThrowOnError) throw error
840
+ return {
841
+ data: null,
842
+ error: error instanceof StorageError ? error : new StorageError(error.message),
843
+ }
844
+ }
845
+ }
846
+
847
+ async update(
848
+ path: string,
849
+ fileBody: FileBody,
850
+ fileOptions?: FileOptions,
851
+ ): Promise<
852
+ { data: { id: string; path: string; fullPath: string }; error: null } | { data: null; error: StorageError }
853
+ > {
854
+ return this.upload(path, fileBody, { ...fileOptions, upsert: true })
855
+ }
856
+
857
+ async move(
858
+ fromPath: string,
859
+ toPath: string,
860
+ // options?: DestinationOptions,
861
+ ): Promise<{ data: { message: string }; error: null } | { data: null; error: StorageError }> {
862
+ try {
863
+ const result = await this.copyFile({
864
+ fileList: [
865
+ {
866
+ srcPath: this._getCloudPath(fromPath),
867
+ dstPath: this._getCloudPath(toPath),
868
+ overwrite: true,
869
+ removeOriginal: true,
870
+ },
871
+ ],
872
+ })
873
+
874
+ if (result.fileList[0].code && result.fileList[0].code !== 'SUCCESS') {
875
+ throw new StorageError(result.fileList[0].message || 'Move failed')
876
+ }
877
+
878
+ return {
879
+ data: { message: `File moved from ${fromPath} to ${toPath}` },
880
+ error: null,
881
+ }
882
+ } catch (error: any) {
883
+ if (this.shouldThrowOnError) throw error
884
+ if (isStorageError(error)) {
885
+ return {
886
+ data: null,
887
+ error,
888
+ }
889
+ }
890
+ throw error
891
+ }
892
+ }
893
+
894
+ async copy(
895
+ fromPath: string,
896
+ toPath: string,
897
+ // options?: DestinationOptions,
898
+ ): Promise<{ data: { path: string }; error: null } | { data: null; error: StorageError }> {
899
+ try {
900
+ const result = await this.copyFile({
901
+ fileList: [
902
+ {
903
+ srcPath: this._getCloudPath(fromPath),
904
+ dstPath: this._getCloudPath(toPath),
905
+ overwrite: true,
906
+ removeOriginal: false,
907
+ },
908
+ ],
909
+ })
910
+
911
+ if (result.fileList[0].code && result.fileList[0].code !== 'SUCCESS') {
912
+ throw new StorageError(result.fileList[0].message || 'Copy failed')
913
+ }
914
+
915
+ return {
916
+ data: { path: this._getCloudPath(toPath) },
917
+ error: null,
918
+ }
919
+ } catch (error: any) {
920
+ if (this.shouldThrowOnError) throw error
921
+ if (isStorageError(error)) {
922
+ return { data: null, error }
923
+ }
924
+ throw error
925
+ }
926
+ }
927
+
928
+ async createSignedUrl(
929
+ path: string,
930
+ expiresIn: number,
931
+ options?: {
932
+ download?: string | boolean
933
+ transform?: TransformOptions
934
+ },
935
+ ): Promise<{ data: { signedUrl: string }; error: null } | { data: null; error: StorageError }> {
936
+ try {
937
+ const cloudPath = this._normalizeCloudId(path)
938
+ const result = await this.getTempFileURL({
939
+ fileList: [
940
+ {
941
+ fileID: cloudPath,
942
+ maxAge: expiresIn,
943
+ },
944
+ ],
945
+ })
946
+
947
+ // IGetFileUrlItem 有 code 字段但没有 message 字段
948
+ if (result.fileList[0].code !== 'SUCCESS') {
949
+ throw new StorageError(`Failed to create signed URL: [${result.fileList[0].code}] ${result.fileList[0].fileID}`)
950
+ }
951
+
952
+ let signedUrl = result.fileList[0].download_url
953
+
954
+ // 构建查询参数
955
+ const queryParams: string[] = []
956
+
957
+ // 如果有 download 参数,添加到 URL 中
958
+ if (options?.download !== undefined) {
959
+ if (typeof options.download === 'string') {
960
+ // download 是文件名
961
+ queryParams.push(`download=${encodeURIComponent(options.download)}`)
962
+ } else if (options.download === true) {
963
+ // download 是 true,使用原文件名或默认值
964
+ queryParams.push('download=true')
965
+ }
966
+ }
967
+
968
+ // 如果有图片转换参数,添加到 URL 中
969
+ if (options?.transform) {
970
+ const transformQuery = this._transformOptsToQueryString(options.transform)
971
+ if (transformQuery) {
972
+ queryParams.push(transformQuery)
973
+ }
974
+ }
975
+
976
+ // 拼接所有查询参数
977
+ if (queryParams.length > 0) {
978
+ const separator = signedUrl.includes('?') ? '&' : '?'
979
+ signedUrl = `${signedUrl}${separator}${queryParams.join('&')}`
980
+ }
981
+
982
+ return {
983
+ data: { signedUrl },
984
+ error: null,
985
+ }
986
+ } catch (error: any) {
987
+ if (this.shouldThrowOnError) throw error
988
+ if (isStorageError(error)) {
989
+ return {
990
+ data: null,
991
+ error,
992
+ }
993
+ }
994
+ throw error
995
+ }
996
+ }
997
+
998
+ async createSignedUrls(
999
+ paths: string[],
1000
+ expiresIn: number,
1001
+ // options?: {
1002
+ // download?: string | boolean
1003
+ // },
1004
+ ): Promise<
1005
+ | { data: Array<{ path: string; signedUrl: string; error: string | null }>; error: null }
1006
+ | { data: null; error: StorageError }
1007
+ > {
1008
+ try {
1009
+ const fileList = paths.map(p => ({
1010
+ fileID: this._normalizeCloudId(p),
1011
+ maxAge: expiresIn,
1012
+ }))
1013
+
1014
+ const result = await this.getTempFileURL({ fileList })
1015
+
1016
+ return {
1017
+ data: result.fileList.map((item: any, index: number) => ({
1018
+ path: paths[index],
1019
+ signedUrl: item.tempFileURL || '',
1020
+ error: item.code === 'SUCCESS' ? null : item.message,
1021
+ })),
1022
+ error: null,
1023
+ }
1024
+ } catch (error: any) {
1025
+ if (this.shouldThrowOnError) throw error
1026
+ if (isStorageError(error)) {
1027
+ return {
1028
+ data: null,
1029
+ error,
1030
+ }
1031
+ }
1032
+ throw error
1033
+ }
1034
+ }
1035
+
1036
+ async download(
1037
+ path: string,
1038
+ options?: TransformOptions,
1039
+ ): Promise<{
1040
+ data: Blob
1041
+ error: StorageError | null
1042
+ }> {
1043
+ try {
1044
+ return {
1045
+ data: await (async () => {
1046
+ const signedUrlResult = await this.createSignedUrl(path, 600, { transform: options })
1047
+ if (signedUrlResult.error) {
1048
+ throw signedUrlResult.error
1049
+ }
1050
+ const tmpUrl = encodeURI(signedUrlResult.data?.signedUrl)
1051
+
1052
+ const { data } = await (this as any).request.reqClass.get({
1053
+ url: tmpUrl,
1054
+ headers: {}, // 下载资源请求不经过service,header清空
1055
+ responseType: 'blob',
1056
+ })
1057
+
1058
+ if (!data) {
1059
+ throw new StorageError('Download failed: no file content')
1060
+ }
1061
+
1062
+ // 将 Buffer 转换为 Uint8Array 以兼容 Blob
1063
+ return new Blob([data])
1064
+ })(),
1065
+ error: null,
1066
+ }
1067
+ } catch (error: any) {
1068
+ if (this.shouldThrowOnError) throw error
1069
+ if (isStorageError(error)) {
1070
+ return {
1071
+ data: null,
1072
+ error,
1073
+ }
1074
+ }
1075
+ throw error
1076
+ }
1077
+ }
1078
+
1079
+ /**
1080
+ * 获取文件信息
1081
+ *
1082
+ * @param pathOrFileId - 相对路径(如 'images/photo.jpg')或 CloudBase fileID(以 'cloud://' 开头)
1083
+ * @returns 文件信息对象
1084
+ *
1085
+ * @example
1086
+ * ```typescript
1087
+ * // 使用相对路径
1088
+ * const { data } = await bucket.info('images/photo.jpg')
1089
+ *
1090
+ * // 使用 CloudBase fileID
1091
+ * const { data } = await bucket.info('cloud://env-id.xxxx-xxxx/images/photo.jpg')
1092
+ * ```
1093
+ */
1094
+ async info(pathOrFileId: string,): Promise<{ data: Camelize<FileObjectV2>; error: null } | { data: null; error: StorageError }> {
1095
+ try {
1096
+ // 判断是 fileID 还是相对路径
1097
+ const isFileId = pathOrFileId.startsWith('cloud://')
1098
+ const displayName = isFileId ? this._extractPathFromFileId(pathOrFileId) : pathOrFileId
1099
+ const bucketId = isFileId ? this._extractBucketFromFileId(pathOrFileId) : this.bucketId
1100
+
1101
+ const fileInfo = await this.getFileInfo({
1102
+ fileList: [this._normalizeCloudId(pathOrFileId)],
1103
+ })
1104
+
1105
+ const item = fileInfo.fileList[0]
1106
+
1107
+ if (item.code !== 'SUCCESS') {
1108
+ throw new StorageError(item.message)
1109
+ }
1110
+
1111
+ const now = new Date().toISOString()
1112
+ const lastModified = (item.lastModified ? new Date(item.lastModified) : new Date()).toISOString()
1113
+ return {
1114
+ data: {
1115
+ id: item.fileID,
1116
+ version: '1',
1117
+ name: displayName,
1118
+ bucketId,
1119
+ updatedAt: lastModified,
1120
+ createdAt: lastModified,
1121
+ lastAccessedAt: now,
1122
+ size: item.size,
1123
+ cacheControl: item.cacheControl,
1124
+ contentType: item.contentType,
1125
+ etag: item.etag,
1126
+ lastModified,
1127
+ metadata: {},
1128
+ },
1129
+ error: null,
1130
+ }
1131
+ } catch (error: any) {
1132
+ if (this.shouldThrowOnError) throw error
1133
+ return {
1134
+ data: null,
1135
+ error: error instanceof StorageError ? error : new StorageError(error.message),
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ async exists(pathOrFileId: string): Promise<{ data: boolean; error: null } | { data: null; error: StorageError }> {
1141
+ try {
1142
+ // 判断是 fileID 还是相对路径
1143
+ const fileInfo = await this.getFileInfo({
1144
+ fileList: [this._normalizeCloudId(pathOrFileId)],
1145
+ })
1146
+
1147
+ const item = fileInfo.fileList[0]
1148
+
1149
+ if (item.code === 'FILE_NOT_FOUND') {
1150
+ return {
1151
+ data: false,
1152
+ error: null,
1153
+ }
1154
+ }
1155
+
1156
+ if (item.code !== 'SUCCESS') {
1157
+ throw new StorageError(item.message)
1158
+ }
1159
+
1160
+ return { data: true, error: null }
1161
+ } catch (error: any) {
1162
+ if (this.shouldThrowOnError) throw error
1163
+ throw error
1164
+ }
1165
+ }
1166
+
1167
+ async getPublicUrl(
1168
+ path: string,
1169
+ options?: {
1170
+ download?: string | boolean
1171
+ transform?: TransformOptions
1172
+ },
1173
+ ): Promise<
1174
+ | {
1175
+ data: { publicUrl: string }
1176
+ }
1177
+ | { data: null; error: StorageError }
1178
+ > {
1179
+ const res = await this.createSignedUrl(path, 600, options)
1180
+
1181
+ if (res.data) {
1182
+ return {
1183
+ data: { publicUrl: res.data.signedUrl },
1184
+ }
1185
+ }
1186
+
1187
+ return { data: null, error: res.error as StorageError }
1188
+ }
1189
+
1190
+ async remove(paths: string[]): Promise<{ data: FileObject[]; error: null } | { data: null; error: StorageError }> {
1191
+ try {
1192
+ // 分组获取文件信息,每组最多10个
1193
+ const chunkSize = 10
1194
+ const pathChunks: string[][] = []
1195
+ for (let i = 0; i < paths.length; i += chunkSize) {
1196
+ pathChunks.push(paths.slice(i, i + chunkSize))
1197
+ }
1198
+
1199
+ // 并行获取所有分组的文件信息
1200
+ const fileInfoResults = await Promise.all(pathChunks.map(chunk => Promise.all(chunk.map(path => this.info(path)))),)
1201
+
1202
+ // 合并所有文件信息并构建映射表(path -> fileInfo)
1203
+ const fileInfoMap = new Map<string, Camelize<FileObjectV2>>()
1204
+ fileInfoResults.flat().forEach((result, index) => {
1205
+ if (result.data) {
1206
+ fileInfoMap.set(paths[Math.floor(index / chunkSize) * chunkSize + (index % chunkSize)], result.data)
1207
+ }
1208
+ })
1209
+
1210
+ // 执行删除操作
1211
+ const fileList = paths.map(p => this._normalizeCloudId(p))
1212
+ const result = await this.deleteFile({ fileList })
1213
+
1214
+ // IDeleteFileRes 的 fileList 数组中每个项有 code 字段
1215
+ const failedFiles = result.fileList.filter(item => item.code !== 'SUCCESS')
1216
+ if (failedFiles.length > 0) {
1217
+ throw new StorageError(`Delete failed for ${failedFiles.length} file(s)`)
1218
+ }
1219
+
1220
+ const now = new Date().toISOString()
1221
+
1222
+ // 使用获取到的文件信息构建返回数据
1223
+ return {
1224
+ data: paths.map((p) => {
1225
+ const info = fileInfoMap.get(p)
1226
+
1227
+ return {
1228
+ name: info?.name,
1229
+ id: info?.id,
1230
+ bucket_id: info?.bucketId,
1231
+ owner: undefined, // 无法计算owner
1232
+ updated_at: info?.updatedAt || now,
1233
+ created_at: info?.createdAt,
1234
+ last_accessed_at: info?.lastAccessedAt || now,
1235
+ metadata: info?.metadata || {},
1236
+ /**
1237
+ * TODO: 获取补全 Bucket 信息
1238
+ */
1239
+ buckets: {
1240
+ id: info?.bucketId,
1241
+ name: info?.bucketId,
1242
+ owner: undefined, // 无法计算owner
1243
+ public: false, // 未知
1244
+ created_at: '', // 未知
1245
+ updated_at: now, // 未知
1246
+ },
1247
+ }
1248
+ }),
1249
+ error: null,
1250
+ }
1251
+ } catch (error: any) {
1252
+ if (this.shouldThrowOnError) throw error
1253
+ if (isStorageError(error)) {
1254
+ return {
1255
+ data: null,
1256
+ error,
1257
+ }
1258
+ }
1259
+ throw error
1260
+ }
1261
+ }
1262
+
1263
+ async list() {
1264
+ throw new StorageError('Not implemented')
1265
+ }
1266
+
1267
+ private _getCloudPath(path: string): string {
1268
+ // 清理路径:移除首尾斜杠,合并多个斜杠
1269
+ const cleanPath = path.replace(/^\/|\/$/g, '').replace(/\/+/g, '/')
1270
+
1271
+ // 否则返回清理后的相对路径
1272
+ return cleanPath
1273
+ }
1274
+
1275
+ private _normalizeCloudId(path: string) {
1276
+ // 如果已经是 cloud:// 格式,直接返回
1277
+ if (/^cloud:\/\//.test(path)) {
1278
+ return path
1279
+ }
1280
+ const cleanPath = this._getCloudPath(path)
1281
+
1282
+ // 如果设置了 bucketId,构建完整的 cloud:// 路径
1283
+ if (this.bucketId) {
1284
+ // 获取环境 ID
1285
+ const envId = this.config?.env || ''
1286
+ if (envId) {
1287
+ return `cloud://${envId}.${this.bucketId}/${cleanPath}`
1288
+ }
1289
+ } else {
1290
+ throw new StorageError('bucketId is not set')
1291
+ }
1292
+ }
1293
+
1294
+ private toBase64(data: string) {
1295
+ if (typeof Buffer !== 'undefined') {
1296
+ return Buffer.from(data).toString('base64')
1297
+ }
1298
+ return btoa(data)
1299
+ }
1300
+
1301
+ /**
1302
+ * 将 TransformOptions 转换为腾讯云数据万象的 imageMogr2 查询字符串
1303
+ *
1304
+ * 腾讯云数据万象使用 imageMogr2 接口进行图片处理,支持以下参数:
1305
+ * - /thumbnail/<Width>x<Height> - 缩放
1306
+ * - /format/<Format> - 格式转换
1307
+ * - /quality/<Quality> - 质量调整
1308
+ * - /rquality/<Quality> - 相对质量
1309
+ *
1310
+ * @param transform - Supabase TransformOptions
1311
+ * @returns 腾讯云数据万象的查询字符串
1312
+ *
1313
+ * @example
1314
+ * ```typescript
1315
+ * _transformOptsToQueryString({ width: 300, height: 200, quality: 80, format: 'origin' })
1316
+ * // 返回: 'imageMogr2/thumbnail/300x200/quality/80'
1317
+ * ```
1318
+ */
1319
+ private _transformOptsToQueryString(transform: TransformOptions): string {
1320
+ const params: string[] = ['imageMogr2']
1321
+
1322
+ // 处理缩放参数
1323
+ if (transform.width || transform.height) {
1324
+ const width = transform.width || ''
1325
+ const height = transform.height || ''
1326
+
1327
+ // 根据 resize 模式选择不同的缩放方式
1328
+ if (transform.resize === 'fill') {
1329
+ // fill: 强制缩放到指定尺寸,可能变形
1330
+ params.push(`thumbnail/${width}x${height}!`)
1331
+ } else if (transform.resize === 'contain') {
1332
+ // contain: 等比缩放,完整显示在指定尺寸内
1333
+ params.push(`thumbnail/${width}x${height}`)
1334
+ } else {
1335
+ // cover (默认): 等比缩放,填充指定尺寸,可能裁剪
1336
+ // 使用 /thumbnail/<Width>x<Height>^ 表示填充模式
1337
+ params.push(`thumbnail/${width}x${height}^`)
1338
+ }
1339
+ }
1340
+
1341
+ // 处理格式转换
1342
+ // format: 'origin' 表示保持原格式,不传此参数
1343
+ if (transform.format && transform.format !== 'origin') {
1344
+ params.push(`format/${transform.format}`)
1345
+ }
1346
+
1347
+ // 处理质量参数
1348
+ if (transform.quality !== undefined) {
1349
+ // 腾讯云数据万象的 quality 参数范围是 1-100
1350
+ const quality = Math.max(1, Math.min(100, transform.quality))
1351
+ params.push(`quality/${quality}`)
1352
+ }
1353
+
1354
+ return params.join('/')
1355
+ }
1356
+
1357
+ /**
1358
+ * 从 CloudBase fileID 中提取文件路径
1359
+ *
1360
+ * @param fileId - CloudBase fileID (格式: cloud://env-id.bucket/path/to/file.jpg)
1361
+ * @returns 提取的文件路径
1362
+ *
1363
+ * @example
1364
+ * ```typescript
1365
+ * _extractPathFromFileId('cloud://test-env.test-bucket/images/photo.jpg')
1366
+ * // 返回: 'images/photo.jpg'
1367
+ *
1368
+ * _extractPathFromFileId('cloud://test-env.test-bucket/file.txt')
1369
+ * // 返回: 'file.txt'
1370
+ * ```
1371
+ */
1372
+ private _extractPathFromFileId(fileId: string): string {
1373
+ // fileID 格式: cloud://env-id.bucket/path/to/file.jpg
1374
+ // 需要提取第一个 / 后面的所有内容(包括 bucket 和路径)
1375
+ // 然后再提取 bucket 后面的路径部分
1376
+
1377
+ // 移除 cloud:// 前缀
1378
+ const withoutProtocol = fileId.replace(/^cloud:\/\//, '')
1379
+
1380
+ // 分割为 [env-id.bucket, path, to, file.jpg]
1381
+ const parts = withoutProtocol.split('/')
1382
+
1383
+ if (parts.length < 2) {
1384
+ // 如果无法解析,返回完整的 fileID
1385
+ return fileId
1386
+ }
1387
+
1388
+ // 移除第一部分(env-id.bucket),返回剩余路径
1389
+ return parts.slice(1).join('/')
1390
+ }
1391
+
1392
+ /**
1393
+ * 从 CloudBase fileID 中提取 bucket 名称
1394
+ *
1395
+ * @param fileId - CloudBase fileID (格式: cloud://env-id.bucket/path/to/file.jpg)
1396
+ * @returns 提取的 bucket 名称
1397
+ *
1398
+ * @example
1399
+ * ```typescript
1400
+ * _extractBucketFromFileId('cloud://test-env.test-bucket/images/photo.jpg')
1401
+ * // 返回: 'test-bucket'
1402
+ *
1403
+ * _extractBucketFromFileId('cloud://prod-env.my-bucket/file.txt')
1404
+ * // 返回: 'my-bucket'
1405
+ * ```
1406
+ */
1407
+ private _extractBucketFromFileId(fileId: string): string {
1408
+ // fileID 格式: cloud://env-id.bucket/path/to/file.jpg
1409
+ // 需要提取 env-id.bucket 中的 bucket 部分
1410
+
1411
+ // 移除 cloud:// 前缀
1412
+ const withoutProtocol = fileId.replace(/^cloud:\/\//, '')
1413
+
1414
+ // 分割为 [env-id.bucket, path, to, file.jpg]
1415
+ const parts = withoutProtocol.split('/')
1416
+
1417
+ if (parts.length < 1) {
1418
+ // 如果无法解析,返回默认 bucket
1419
+ return ''
1420
+ }
1421
+
1422
+ // 第一部分是 env-id.bucket,提取 bucket 部分
1423
+ // 格式: env-id.bucket 或 env-id.bucket-name
1424
+ const envAndBucket = parts[0]
1425
+
1426
+ // 查找第一个点的位置
1427
+ const dotIndex = envAndBucket.indexOf('.')
1428
+
1429
+ if (dotIndex === -1) {
1430
+ // 如果没有点,返回整个字符串
1431
+ return envAndBucket
1432
+ }
1433
+
1434
+ // 返回点后面的部分(bucket 名称)
1435
+ return envAndBucket.substring(dotIndex + 1)
1436
+ }
1437
+ }
1438
+
23
1439
  const supabaseComponent: ICloudbaseComponent = {
24
1440
  name: `${COMPONENT_NAME}/supabase`,
25
1441
  IIFE: true,