@cloudbase/storage 3.0.0 → 3.0.1
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/dist/cjs/index.js +48 -404
- package/dist/cjs/storage.d.ts +49 -0
- package/dist/cjs/storage.js +701 -0
- package/dist/cjs/supabase/errors.d.ts +20 -0
- package/dist/cjs/supabase/errors.js +65 -0
- package/dist/cjs/supabase/index.d.ts +145 -0
- package/dist/cjs/supabase/index.js +706 -0
- package/dist/cjs/supabase/types.d.ts +64 -0
- package/dist/cjs/supabase/types.js +3 -0
- package/dist/esm/index.js +47 -403
- package/dist/esm/storage.d.ts +49 -0
- package/dist/esm/storage.js +698 -0
- package/dist/esm/supabase/errors.d.ts +20 -0
- package/dist/esm/supabase/errors.js +61 -0
- package/dist/esm/supabase/index.d.ts +145 -0
- package/dist/esm/supabase/index.js +703 -0
- package/dist/esm/supabase/types.d.ts +64 -0
- package/dist/esm/supabase/types.js +2 -0
- package/package.json +4 -4
- package/src/index.ts +58 -330
- package/src/storage.ts +683 -0
- package/src/supabase/errors.ts +43 -0
- package/src/supabase/index.ts +746 -0
- package/src/supabase/types.ts +132 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { constants } from '@cloudbase/utilities'
|
|
2
|
+
|
|
3
|
+
import { CloudbaseStorage, COMPONENT_NAME, ICloudbaseContext } from '../storage'
|
|
4
|
+
|
|
5
|
+
import { Camelize, FileBody, FileObject, FileObjectV2, FileOptions, TransformOptions } from './types'
|
|
6
|
+
import { isStorageError, StorageError } from './errors'
|
|
7
|
+
|
|
8
|
+
const { ERRORS } = constants
|
|
9
|
+
|
|
10
|
+
export class SupabaseFileAPILikeStorage extends CloudbaseStorage {
|
|
11
|
+
private shouldThrowOnError = false
|
|
12
|
+
private bucketId = ''
|
|
13
|
+
private context: ICloudbaseContext
|
|
14
|
+
|
|
15
|
+
constructor(context?: ICloudbaseContext) {
|
|
16
|
+
super()
|
|
17
|
+
this.context = context
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get config() {
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
return this.context?.config
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get request() {
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
return this.context?.request
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throwOnError(): this {
|
|
31
|
+
this.shouldThrowOnError = true
|
|
32
|
+
return this
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
from(bucket?: string) {
|
|
36
|
+
this.bucketId = bucket || ''
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async upload(
|
|
41
|
+
path: string,
|
|
42
|
+
fileBody: FileBody,
|
|
43
|
+
fileOptions?: FileOptions,
|
|
44
|
+
): Promise<
|
|
45
|
+
{ data: { id: string; path: string; fullPath: string }; error: null } | { data: null; error: StorageError }
|
|
46
|
+
> {
|
|
47
|
+
const options = { upsert: true, ...fileOptions }
|
|
48
|
+
const { cacheControl, contentType, metadata } = options
|
|
49
|
+
try {
|
|
50
|
+
const cloudPath = this._getCloudPath(path)
|
|
51
|
+
const uploadFileParams: Parameters<CloudbaseStorage['uploadFile']>[0] = {
|
|
52
|
+
cloudPath,
|
|
53
|
+
filePath: fileBody as any,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (cacheControl || contentType || metadata) {
|
|
57
|
+
const headers = {}
|
|
58
|
+
if (cacheControl) {
|
|
59
|
+
headers['cache-control'] = cacheControl
|
|
60
|
+
}
|
|
61
|
+
if (contentType) {
|
|
62
|
+
headers['content-type'] = contentType
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (metadata) {
|
|
66
|
+
headers['x-cos-metadata-metadata'] = this.toBase64(JSON.stringify(metadata))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
uploadFileParams.headers = headers
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = await this.uploadFile(uploadFileParams)
|
|
73
|
+
|
|
74
|
+
// IUploadFileRes 没有 code 字段,如果没有 fileID 则表示失败
|
|
75
|
+
if (!result.fileID) {
|
|
76
|
+
throw new Error(JSON.stringify({
|
|
77
|
+
code: ERRORS.OPERATION_FAIL,
|
|
78
|
+
msg: `[${COMPONENT_NAME}.update] no fileID returned`,
|
|
79
|
+
}),)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
data: {
|
|
84
|
+
id: result.fileID,
|
|
85
|
+
path,
|
|
86
|
+
fullPath: path,
|
|
87
|
+
},
|
|
88
|
+
error: null,
|
|
89
|
+
}
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
if (this.shouldThrowOnError) throw error
|
|
92
|
+
if (isStorageError(error)) {
|
|
93
|
+
return {
|
|
94
|
+
data: null,
|
|
95
|
+
error,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async uploadToSignedUrl(
|
|
103
|
+
path: string,
|
|
104
|
+
_token: string,
|
|
105
|
+
fileBody: FileBody,
|
|
106
|
+
fileOptions?: FileOptions,
|
|
107
|
+
): Promise<
|
|
108
|
+
{ data: { id: string; path: string; fullPath: string }; error: null } | { data: null; error: StorageError }
|
|
109
|
+
> {
|
|
110
|
+
return this.upload(path, fileBody, fileOptions)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async createSignedUploadUrl(path: string): Promise<
|
|
114
|
+
| {
|
|
115
|
+
data: {
|
|
116
|
+
signedUrl: string
|
|
117
|
+
token: string
|
|
118
|
+
path: string
|
|
119
|
+
// CloudBase 额外的元数据字段
|
|
120
|
+
authorization?: string
|
|
121
|
+
id?: string
|
|
122
|
+
cosFileId?: string
|
|
123
|
+
downloadUrl?: string
|
|
124
|
+
}
|
|
125
|
+
error: null
|
|
126
|
+
}
|
|
127
|
+
| { data: null; error: StorageError }
|
|
128
|
+
> {
|
|
129
|
+
try {
|
|
130
|
+
const cloudPath = this._getCloudPath(path)
|
|
131
|
+
|
|
132
|
+
const { data: metadata } = await this.getUploadMetadata({ cloudPath })
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
data: {
|
|
136
|
+
signedUrl: metadata.url,
|
|
137
|
+
token: metadata.token,
|
|
138
|
+
path,
|
|
139
|
+
// 返回 CloudBase 的额外元数据,供 uploadToSignedUrl 使用
|
|
140
|
+
authorization: metadata.authorization,
|
|
141
|
+
id: metadata.fileId,
|
|
142
|
+
cosFileId: metadata.cosFileId,
|
|
143
|
+
downloadUrl: metadata.download_url,
|
|
144
|
+
},
|
|
145
|
+
error: null,
|
|
146
|
+
}
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
if (this.shouldThrowOnError) throw error
|
|
149
|
+
return {
|
|
150
|
+
data: null,
|
|
151
|
+
error: error instanceof StorageError ? error : new StorageError(error.message),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async update(
|
|
157
|
+
path: string,
|
|
158
|
+
fileBody: FileBody,
|
|
159
|
+
fileOptions?: FileOptions,
|
|
160
|
+
): Promise<
|
|
161
|
+
{ data: { id: string; path: string; fullPath: string }; error: null } | { data: null; error: StorageError }
|
|
162
|
+
> {
|
|
163
|
+
return this.upload(path, fileBody, { ...fileOptions, upsert: true })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async move(
|
|
167
|
+
fromPath: string,
|
|
168
|
+
toPath: string,
|
|
169
|
+
// options?: DestinationOptions,
|
|
170
|
+
): Promise<{ data: { message: string }; error: null } | { data: null; error: StorageError }> {
|
|
171
|
+
try {
|
|
172
|
+
const result = await this.copyFile({
|
|
173
|
+
fileList: [
|
|
174
|
+
{
|
|
175
|
+
srcPath: this._getCloudPath(fromPath),
|
|
176
|
+
dstPath: this._getCloudPath(toPath),
|
|
177
|
+
overwrite: true,
|
|
178
|
+
removeOriginal: true,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if (result.fileList[0].code && result.fileList[0].code !== 'SUCCESS') {
|
|
184
|
+
throw new StorageError(result.fileList[0].message || 'Move failed')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
data: { message: `File moved from ${fromPath} to ${toPath}` },
|
|
189
|
+
error: null,
|
|
190
|
+
}
|
|
191
|
+
} catch (error: any) {
|
|
192
|
+
if (this.shouldThrowOnError) throw error
|
|
193
|
+
if (isStorageError(error)) {
|
|
194
|
+
return {
|
|
195
|
+
data: null,
|
|
196
|
+
error,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
throw error
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async copy(
|
|
204
|
+
fromPath: string,
|
|
205
|
+
toPath: string,
|
|
206
|
+
// options?: DestinationOptions,
|
|
207
|
+
): Promise<{ data: { path: string }; error: null } | { data: null; error: StorageError }> {
|
|
208
|
+
try {
|
|
209
|
+
const result = await this.copyFile({
|
|
210
|
+
fileList: [
|
|
211
|
+
{
|
|
212
|
+
srcPath: this._getCloudPath(fromPath),
|
|
213
|
+
dstPath: this._getCloudPath(toPath),
|
|
214
|
+
overwrite: true,
|
|
215
|
+
removeOriginal: false,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
if (result.fileList[0].code && result.fileList[0].code !== 'SUCCESS') {
|
|
221
|
+
throw new StorageError(result.fileList[0].message || 'Copy failed')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
data: { path: this._getCloudPath(toPath) },
|
|
226
|
+
error: null,
|
|
227
|
+
}
|
|
228
|
+
} catch (error: any) {
|
|
229
|
+
if (this.shouldThrowOnError) throw error
|
|
230
|
+
if (isStorageError(error)) {
|
|
231
|
+
return { data: null, error }
|
|
232
|
+
}
|
|
233
|
+
throw error
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async createSignedUrl(
|
|
238
|
+
path: string,
|
|
239
|
+
expiresIn: number,
|
|
240
|
+
options?: {
|
|
241
|
+
download?: string | boolean
|
|
242
|
+
transform?: TransformOptions
|
|
243
|
+
},
|
|
244
|
+
): Promise<{ data: { signedUrl: string }; error: null } | { data: null; error: StorageError }> {
|
|
245
|
+
try {
|
|
246
|
+
const cloudPath = this._normalizeCloudId(path)
|
|
247
|
+
const result = await this.getTempFileURL({
|
|
248
|
+
fileList: [
|
|
249
|
+
{
|
|
250
|
+
fileID: cloudPath,
|
|
251
|
+
maxAge: expiresIn,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// IGetFileUrlItem 有 code 字段但没有 message 字段
|
|
257
|
+
if (result.fileList[0].code !== 'SUCCESS') {
|
|
258
|
+
throw new StorageError(`Failed to create signed URL: [${result.fileList[0].code}] ${result.fileList[0].fileID}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let signedUrl = result.fileList[0].download_url
|
|
262
|
+
|
|
263
|
+
// 构建查询参数
|
|
264
|
+
const queryParams: string[] = []
|
|
265
|
+
|
|
266
|
+
// 如果有 download 参数,添加到 URL 中
|
|
267
|
+
if (options?.download !== undefined) {
|
|
268
|
+
if (typeof options.download === 'string') {
|
|
269
|
+
// download 是文件名
|
|
270
|
+
queryParams.push(`download=${encodeURIComponent(options.download)}`)
|
|
271
|
+
} else if (options.download === true) {
|
|
272
|
+
// download 是 true,使用原文件名或默认值
|
|
273
|
+
queryParams.push('download=true')
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 如果有图片转换参数,添加到 URL 中
|
|
278
|
+
if (options?.transform) {
|
|
279
|
+
const transformQuery = this._transformOptsToQueryString(options.transform)
|
|
280
|
+
if (transformQuery) {
|
|
281
|
+
queryParams.push(transformQuery)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 拼接所有查询参数
|
|
286
|
+
if (queryParams.length > 0) {
|
|
287
|
+
const separator = signedUrl.includes('?') ? '&' : '?'
|
|
288
|
+
signedUrl = `${signedUrl}${separator}${queryParams.join('&')}`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
data: { signedUrl },
|
|
293
|
+
error: null,
|
|
294
|
+
}
|
|
295
|
+
} catch (error: any) {
|
|
296
|
+
if (this.shouldThrowOnError) throw error
|
|
297
|
+
if (isStorageError(error)) {
|
|
298
|
+
return {
|
|
299
|
+
data: null,
|
|
300
|
+
error,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
throw error
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async createSignedUrls(
|
|
308
|
+
paths: string[],
|
|
309
|
+
expiresIn: number,
|
|
310
|
+
// options?: {
|
|
311
|
+
// download?: string | boolean
|
|
312
|
+
// },
|
|
313
|
+
): Promise<
|
|
314
|
+
| { data: Array<{ path: string; signedUrl: string; error: string | null }>; error: null }
|
|
315
|
+
| { data: null; error: StorageError }
|
|
316
|
+
> {
|
|
317
|
+
try {
|
|
318
|
+
const fileList = paths.map(p => ({
|
|
319
|
+
fileID: this._normalizeCloudId(p),
|
|
320
|
+
maxAge: expiresIn,
|
|
321
|
+
}))
|
|
322
|
+
|
|
323
|
+
const result = await this.getTempFileURL({ fileList })
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
data: result.fileList.map((item: any, index: number) => ({
|
|
327
|
+
path: paths[index],
|
|
328
|
+
signedUrl: item.tempFileURL || '',
|
|
329
|
+
error: item.code === 'SUCCESS' ? null : item.message,
|
|
330
|
+
})),
|
|
331
|
+
error: null,
|
|
332
|
+
}
|
|
333
|
+
} catch (error: any) {
|
|
334
|
+
if (this.shouldThrowOnError) throw error
|
|
335
|
+
if (isStorageError(error)) {
|
|
336
|
+
return {
|
|
337
|
+
data: null,
|
|
338
|
+
error,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw error
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async download(
|
|
346
|
+
path: string,
|
|
347
|
+
options?: TransformOptions,
|
|
348
|
+
): Promise<{
|
|
349
|
+
data: Blob
|
|
350
|
+
error: StorageError | null
|
|
351
|
+
}> {
|
|
352
|
+
try {
|
|
353
|
+
return {
|
|
354
|
+
data: await (async () => {
|
|
355
|
+
const signedUrlResult = await this.createSignedUrl(path, 600, { transform: options })
|
|
356
|
+
if (signedUrlResult.error) {
|
|
357
|
+
throw signedUrlResult.error
|
|
358
|
+
}
|
|
359
|
+
const tmpUrl = encodeURI(signedUrlResult.data?.signedUrl)
|
|
360
|
+
|
|
361
|
+
const { data } = await (this as any).request.reqClass.get({
|
|
362
|
+
url: tmpUrl,
|
|
363
|
+
headers: {}, // 下载资源请求不经过service,header清空
|
|
364
|
+
responseType: 'blob',
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
if (!data) {
|
|
368
|
+
throw new StorageError('Download failed: no file content')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 将 Buffer 转换为 Uint8Array 以兼容 Blob
|
|
372
|
+
return new Blob([data])
|
|
373
|
+
})(),
|
|
374
|
+
error: null,
|
|
375
|
+
}
|
|
376
|
+
} catch (error: any) {
|
|
377
|
+
if (this.shouldThrowOnError) throw error
|
|
378
|
+
if (isStorageError(error)) {
|
|
379
|
+
return {
|
|
380
|
+
data: null,
|
|
381
|
+
error,
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
throw error
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 获取文件信息
|
|
390
|
+
*
|
|
391
|
+
* @param pathOrFileId - 相对路径(如 'images/photo.jpg')或 CloudBase fileID(以 'cloud://' 开头)
|
|
392
|
+
* @returns 文件信息对象
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```typescript
|
|
396
|
+
* // 使用相对路径
|
|
397
|
+
* const { data } = await bucket.info('images/photo.jpg')
|
|
398
|
+
*
|
|
399
|
+
* // 使用 CloudBase fileID
|
|
400
|
+
* const { data } = await bucket.info('cloud://env-id.xxxx-xxxx/images/photo.jpg')
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
async info(pathOrFileId: string,): Promise<{ data: Camelize<FileObjectV2>; error: null } | { data: null; error: StorageError }> {
|
|
404
|
+
try {
|
|
405
|
+
// 判断是 fileID 还是相对路径
|
|
406
|
+
const isFileId = pathOrFileId.startsWith('cloud://')
|
|
407
|
+
const displayName = isFileId ? this._extractPathFromFileId(pathOrFileId) : pathOrFileId
|
|
408
|
+
const bucketId = isFileId ? this._extractBucketFromFileId(pathOrFileId) : this.bucketId
|
|
409
|
+
|
|
410
|
+
const fileInfo = await this.getFileInfo({
|
|
411
|
+
fileList: [this._normalizeCloudId(pathOrFileId)],
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
const item = fileInfo.fileList[0]
|
|
415
|
+
|
|
416
|
+
if (item.code !== 'SUCCESS') {
|
|
417
|
+
throw new StorageError(item.message)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const now = new Date().toISOString()
|
|
421
|
+
const lastModified = (item.lastModified ? new Date(item.lastModified) : new Date()).toISOString()
|
|
422
|
+
return {
|
|
423
|
+
data: {
|
|
424
|
+
id: item.fileID,
|
|
425
|
+
version: '1',
|
|
426
|
+
name: displayName,
|
|
427
|
+
bucketId,
|
|
428
|
+
updatedAt: lastModified,
|
|
429
|
+
createdAt: lastModified,
|
|
430
|
+
lastAccessedAt: now,
|
|
431
|
+
size: item.size,
|
|
432
|
+
cacheControl: item.cacheControl,
|
|
433
|
+
contentType: item.contentType,
|
|
434
|
+
etag: item.etag,
|
|
435
|
+
lastModified,
|
|
436
|
+
metadata: {},
|
|
437
|
+
},
|
|
438
|
+
error: null,
|
|
439
|
+
}
|
|
440
|
+
} catch (error: any) {
|
|
441
|
+
if (this.shouldThrowOnError) throw error
|
|
442
|
+
return {
|
|
443
|
+
data: null,
|
|
444
|
+
error: error instanceof StorageError ? error : new StorageError(error.message),
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async exists(pathOrFileId: string): Promise<{ data: boolean; error: null } | { data: null; error: StorageError }> {
|
|
450
|
+
try {
|
|
451
|
+
// 判断是 fileID 还是相对路径
|
|
452
|
+
const fileInfo = await this.getFileInfo({
|
|
453
|
+
fileList: [this._normalizeCloudId(pathOrFileId)],
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
const item = fileInfo.fileList[0]
|
|
457
|
+
|
|
458
|
+
if (item.code === 'FILE_NOT_FOUND') {
|
|
459
|
+
return {
|
|
460
|
+
data: false,
|
|
461
|
+
error: null,
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (item.code !== 'SUCCESS') {
|
|
466
|
+
throw new StorageError(item.message)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return { data: true, error: null }
|
|
470
|
+
} catch (error: any) {
|
|
471
|
+
if (this.shouldThrowOnError) throw error
|
|
472
|
+
throw error
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async getPublicUrl(
|
|
477
|
+
path: string,
|
|
478
|
+
options?: {
|
|
479
|
+
download?: string | boolean
|
|
480
|
+
transform?: TransformOptions
|
|
481
|
+
},
|
|
482
|
+
): Promise<
|
|
483
|
+
| {
|
|
484
|
+
data: { publicUrl: string }
|
|
485
|
+
}
|
|
486
|
+
| { data: null; error: StorageError }
|
|
487
|
+
> {
|
|
488
|
+
const res = await this.createSignedUrl(path, 600, options)
|
|
489
|
+
|
|
490
|
+
if (res.data) {
|
|
491
|
+
return {
|
|
492
|
+
data: { publicUrl: res.data.signedUrl },
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return { data: null, error: res.error as StorageError }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async remove(paths: string[]): Promise<{ data: FileObject[]; error: null } | { data: null; error: StorageError }> {
|
|
500
|
+
try {
|
|
501
|
+
// 分组获取文件信息,每组最多10个
|
|
502
|
+
const chunkSize = 10
|
|
503
|
+
const pathChunks: string[][] = []
|
|
504
|
+
for (let i = 0; i < paths.length; i += chunkSize) {
|
|
505
|
+
pathChunks.push(paths.slice(i, i + chunkSize))
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 并行获取所有分组的文件信息
|
|
509
|
+
const fileInfoResults = await Promise.all(pathChunks.map(chunk => Promise.all(chunk.map(path => this.info(path)))),)
|
|
510
|
+
|
|
511
|
+
// 合并所有文件信息并构建映射表(path -> fileInfo)
|
|
512
|
+
const fileInfoMap = new Map<string, Camelize<FileObjectV2>>()
|
|
513
|
+
fileInfoResults.flat().forEach((result, index) => {
|
|
514
|
+
if (result.data) {
|
|
515
|
+
fileInfoMap.set(paths[Math.floor(index / chunkSize) * chunkSize + (index % chunkSize)], result.data)
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
// 执行删除操作
|
|
520
|
+
const fileList = paths.map(p => this._normalizeCloudId(p))
|
|
521
|
+
const result = await this.deleteFile({ fileList })
|
|
522
|
+
|
|
523
|
+
// IDeleteFileRes 的 fileList 数组中每个项有 code 字段
|
|
524
|
+
const failedFiles = result.fileList.filter(item => item.code !== 'SUCCESS')
|
|
525
|
+
if (failedFiles.length > 0) {
|
|
526
|
+
throw new StorageError(`Delete failed for ${failedFiles.length} file(s)`)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const now = new Date().toISOString()
|
|
530
|
+
|
|
531
|
+
// 使用获取到的文件信息构建返回数据
|
|
532
|
+
return {
|
|
533
|
+
data: paths.map((p) => {
|
|
534
|
+
const info = fileInfoMap.get(p)
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
name: info?.name,
|
|
538
|
+
id: info?.id,
|
|
539
|
+
bucket_id: info?.bucketId,
|
|
540
|
+
owner: undefined, // 无法计算owner
|
|
541
|
+
updated_at: info?.updatedAt || now,
|
|
542
|
+
created_at: info?.createdAt,
|
|
543
|
+
last_accessed_at: info?.lastAccessedAt || now,
|
|
544
|
+
metadata: info?.metadata || {},
|
|
545
|
+
/**
|
|
546
|
+
* TODO: 获取补全 Bucket 信息
|
|
547
|
+
*/
|
|
548
|
+
buckets: {
|
|
549
|
+
id: info?.bucketId,
|
|
550
|
+
name: info?.bucketId,
|
|
551
|
+
owner: undefined, // 无法计算owner
|
|
552
|
+
public: false, // 未知
|
|
553
|
+
created_at: '', // 未知
|
|
554
|
+
updated_at: now, // 未知
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
}),
|
|
558
|
+
error: null,
|
|
559
|
+
}
|
|
560
|
+
} catch (error: any) {
|
|
561
|
+
if (this.shouldThrowOnError) throw error
|
|
562
|
+
if (isStorageError(error)) {
|
|
563
|
+
return {
|
|
564
|
+
data: null,
|
|
565
|
+
error,
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
throw error
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async list() {
|
|
573
|
+
throw new StorageError('Not implemented')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private _getCloudPath(path: string): string {
|
|
577
|
+
// 清理路径:移除首尾斜杠,合并多个斜杠
|
|
578
|
+
const cleanPath = path.replace(/^\/|\/$/g, '').replace(/\/+/g, '/')
|
|
579
|
+
|
|
580
|
+
// 否则返回清理后的相对路径
|
|
581
|
+
return cleanPath
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private _normalizeCloudId(path: string) {
|
|
585
|
+
// 如果已经是 cloud:// 格式,直接返回
|
|
586
|
+
if (/^cloud:\/\//.test(path)) {
|
|
587
|
+
return path
|
|
588
|
+
}
|
|
589
|
+
const cleanPath = this._getCloudPath(path)
|
|
590
|
+
|
|
591
|
+
// 如果设置了 bucketId,构建完整的 cloud:// 路径
|
|
592
|
+
if (this.bucketId) {
|
|
593
|
+
// 获取环境 ID
|
|
594
|
+
const envId = this.config?.env || ''
|
|
595
|
+
if (envId) {
|
|
596
|
+
return `cloud://${envId}.${this.bucketId}/${cleanPath}`
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
throw new StorageError('bucketId is not set')
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private toBase64(data: string) {
|
|
604
|
+
if (typeof Buffer !== 'undefined') {
|
|
605
|
+
return Buffer.from(data).toString('base64')
|
|
606
|
+
}
|
|
607
|
+
return btoa(data)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* 将 TransformOptions 转换为腾讯云数据万象的 imageMogr2 查询字符串
|
|
612
|
+
*
|
|
613
|
+
* 腾讯云数据万象使用 imageMogr2 接口进行图片处理,支持以下参数:
|
|
614
|
+
* - /thumbnail/<Width>x<Height> - 缩放
|
|
615
|
+
* - /format/<Format> - 格式转换
|
|
616
|
+
* - /quality/<Quality> - 质量调整
|
|
617
|
+
* - /rquality/<Quality> - 相对质量
|
|
618
|
+
*
|
|
619
|
+
* @param transform - Supabase TransformOptions
|
|
620
|
+
* @returns 腾讯云数据万象的查询字符串
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```typescript
|
|
624
|
+
* _transformOptsToQueryString({ width: 300, height: 200, quality: 80, format: 'origin' })
|
|
625
|
+
* // 返回: 'imageMogr2/thumbnail/300x200/quality/80'
|
|
626
|
+
* ```
|
|
627
|
+
*/
|
|
628
|
+
private _transformOptsToQueryString(transform: TransformOptions): string {
|
|
629
|
+
const params: string[] = ['imageMogr2']
|
|
630
|
+
|
|
631
|
+
// 处理缩放参数
|
|
632
|
+
if (transform.width || transform.height) {
|
|
633
|
+
const width = transform.width || ''
|
|
634
|
+
const height = transform.height || ''
|
|
635
|
+
|
|
636
|
+
// 根据 resize 模式选择不同的缩放方式
|
|
637
|
+
if (transform.resize === 'fill') {
|
|
638
|
+
// fill: 强制缩放到指定尺寸,可能变形
|
|
639
|
+
params.push(`thumbnail/${width}x${height}!`)
|
|
640
|
+
} else if (transform.resize === 'contain') {
|
|
641
|
+
// contain: 等比缩放,完整显示在指定尺寸内
|
|
642
|
+
params.push(`thumbnail/${width}x${height}`)
|
|
643
|
+
} else {
|
|
644
|
+
// cover (默认): 等比缩放,填充指定尺寸,可能裁剪
|
|
645
|
+
// 使用 /thumbnail/<Width>x<Height>^ 表示填充模式
|
|
646
|
+
params.push(`thumbnail/${width}x${height}^`)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 处理格式转换
|
|
651
|
+
// format: 'origin' 表示保持原格式,不传此参数
|
|
652
|
+
if (transform.format && transform.format !== 'origin') {
|
|
653
|
+
params.push(`format/${transform.format}`)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 处理质量参数
|
|
657
|
+
if (transform.quality !== undefined) {
|
|
658
|
+
// 腾讯云数据万象的 quality 参数范围是 1-100
|
|
659
|
+
const quality = Math.max(1, Math.min(100, transform.quality))
|
|
660
|
+
params.push(`quality/${quality}`)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return params.join('/')
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* 从 CloudBase fileID 中提取文件路径
|
|
668
|
+
*
|
|
669
|
+
* @param fileId - CloudBase fileID (格式: cloud://env-id.bucket/path/to/file.jpg)
|
|
670
|
+
* @returns 提取的文件路径
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* ```typescript
|
|
674
|
+
* _extractPathFromFileId('cloud://test-env.test-bucket/images/photo.jpg')
|
|
675
|
+
* // 返回: 'images/photo.jpg'
|
|
676
|
+
*
|
|
677
|
+
* _extractPathFromFileId('cloud://test-env.test-bucket/file.txt')
|
|
678
|
+
* // 返回: 'file.txt'
|
|
679
|
+
* ```
|
|
680
|
+
*/
|
|
681
|
+
private _extractPathFromFileId(fileId: string): string {
|
|
682
|
+
// fileID 格式: cloud://env-id.bucket/path/to/file.jpg
|
|
683
|
+
// 需要提取第一个 / 后面的所有内容(包括 bucket 和路径)
|
|
684
|
+
// 然后再提取 bucket 后面的路径部分
|
|
685
|
+
|
|
686
|
+
// 移除 cloud:// 前缀
|
|
687
|
+
const withoutProtocol = fileId.replace(/^cloud:\/\//, '')
|
|
688
|
+
|
|
689
|
+
// 分割为 [env-id.bucket, path, to, file.jpg]
|
|
690
|
+
const parts = withoutProtocol.split('/')
|
|
691
|
+
|
|
692
|
+
if (parts.length < 2) {
|
|
693
|
+
// 如果无法解析,返回完整的 fileID
|
|
694
|
+
return fileId
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// 移除第一部分(env-id.bucket),返回剩余路径
|
|
698
|
+
return parts.slice(1).join('/')
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* 从 CloudBase fileID 中提取 bucket 名称
|
|
703
|
+
*
|
|
704
|
+
* @param fileId - CloudBase fileID (格式: cloud://env-id.bucket/path/to/file.jpg)
|
|
705
|
+
* @returns 提取的 bucket 名称
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* ```typescript
|
|
709
|
+
* _extractBucketFromFileId('cloud://test-env.test-bucket/images/photo.jpg')
|
|
710
|
+
* // 返回: 'test-bucket'
|
|
711
|
+
*
|
|
712
|
+
* _extractBucketFromFileId('cloud://prod-env.my-bucket/file.txt')
|
|
713
|
+
* // 返回: 'my-bucket'
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
private _extractBucketFromFileId(fileId: string): string {
|
|
717
|
+
// fileID 格式: cloud://env-id.bucket/path/to/file.jpg
|
|
718
|
+
// 需要提取 env-id.bucket 中的 bucket 部分
|
|
719
|
+
|
|
720
|
+
// 移除 cloud:// 前缀
|
|
721
|
+
const withoutProtocol = fileId.replace(/^cloud:\/\//, '')
|
|
722
|
+
|
|
723
|
+
// 分割为 [env-id.bucket, path, to, file.jpg]
|
|
724
|
+
const parts = withoutProtocol.split('/')
|
|
725
|
+
|
|
726
|
+
if (parts.length < 1) {
|
|
727
|
+
// 如果无法解析,返回默认 bucket
|
|
728
|
+
return ''
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 第一部分是 env-id.bucket,提取 bucket 部分
|
|
732
|
+
// 格式: env-id.bucket 或 env-id.bucket-name
|
|
733
|
+
const envAndBucket = parts[0]
|
|
734
|
+
|
|
735
|
+
// 查找第一个点的位置
|
|
736
|
+
const dotIndex = envAndBucket.indexOf('.')
|
|
737
|
+
|
|
738
|
+
if (dotIndex === -1) {
|
|
739
|
+
// 如果没有点,返回整个字符串
|
|
740
|
+
return envAndBucket
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// 返回点后面的部分(bucket 名称)
|
|
744
|
+
return envAndBucket.substring(dotIndex + 1)
|
|
745
|
+
}
|
|
746
|
+
}
|