• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / storage-js / 13458494462

21 Feb 2025 02:07PM UTC coverage: 74.857% (-0.5%) from 75.34%
13458494462

Pull #219

github

web-flow
Merge 3c3adbc61 into 6bf02b9da
Pull Request #219: feat: list-v2 endpoint

146 of 197 branches covered (74.11%)

Branch coverage included in aggregate %.

4 of 7 new or added lines in 1 file covered. (57.14%)

2 existing lines in 1 file now uncovered.

247 of 328 relevant lines covered (75.3%)

9.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.7
/src/packages/StorageFileApi.ts
1
import { isStorageError, StorageError, StorageUnknownError } from '../lib/errors'
2✔
2
import { Fetch, get, head, post, remove } from '../lib/fetch'
2✔
3
import { recursiveToCamel, resolveFetch } from '../lib/helpers'
2✔
4
import {
5
  FileObject,
6
  FileOptions,
7
  SearchOptions,
8
  FetchParameters,
9
  TransformOptions,
10
  DestinationOptions,
11
  FileObjectV2,
12
  Camelize,
13
  SearchV2Options,
14
  SearchV2Result,
15
} from '../lib/types'
16

17
const DEFAULT_SEARCH_OPTIONS = {
2✔
18
  limit: 100,
19
  offset: 0,
20
  sortBy: {
21
    column: 'name',
22
    order: 'asc',
23
  },
24
}
25

26
const DEFAULT_FILE_OPTIONS: FileOptions = {
2✔
27
  cacheControl: '3600',
28
  contentType: 'text/plain;charset=UTF-8',
29
  upsert: false,
30
}
31

32
type FileBody =
33
  | ArrayBuffer
34
  | ArrayBufferView
35
  | Blob
36
  | Buffer
37
  | File
38
  | FormData
39
  | NodeJS.ReadableStream
40
  | ReadableStream<Uint8Array>
41
  | URLSearchParams
42
  | string
43

44
export default class StorageFileApi {
2✔
45
  protected url: string
46
  protected headers: { [key: string]: string }
47
  protected bucketId?: string
48
  protected fetch: Fetch
49

50
  constructor(
51
    url: string,
52
    headers: { [key: string]: string } = {},
×
53
    bucketId?: string,
54
    fetch?: Fetch
55
  ) {
56
    this.url = url
64✔
57
    this.headers = headers
64✔
58
    this.bucketId = bucketId
64✔
59
    this.fetch = resolveFetch(fetch)
64✔
60
  }
61

62
  /**
63
   * Uploads a file to an existing bucket or replaces an existing file at the specified path with a new one.
64
   *
65
   * @param method HTTP method.
66
   * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
67
   * @param fileBody The body of the file to be stored in the bucket.
68
   */
69
  private async uploadOrUpdate(
70
    method: 'POST' | 'PUT',
71
    path: string,
72
    fileBody: FileBody,
73
    fileOptions?: FileOptions
74
  ): Promise<
75
    | {
76
        data: { id: string; path: string; fullPath: string }
77
        error: null
78
      }
79
    | {
80
        data: null
81
        error: StorageError
82
      }
83
  > {
84
    try {
31✔
85
      let body
86
      const options = { ...DEFAULT_FILE_OPTIONS, ...fileOptions }
31✔
87
      let headers: Record<string, string> = {
31✔
88
        ...this.headers,
89
        ...(method === 'POST' && { 'x-upsert': String(options.upsert as boolean) }),
61✔
90
      }
91

92
      const metadata = options.metadata
31✔
93

94
      if (typeof Blob !== 'undefined' && fileBody instanceof Blob) {
31✔
95
        body = new FormData()
1✔
96
        body.append('cacheControl', options.cacheControl as string)
1✔
97
        if (metadata) {
1!
98
          body.append('metadata', this.encodeMetadata(metadata))
×
99
        }
100
        body.append('', fileBody)
1✔
101
      } else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) {
30!
102
        body = fileBody
×
103
        body.append('cacheControl', options.cacheControl as string)
×
104
        if (metadata) {
×
105
          body.append('metadata', this.encodeMetadata(metadata))
×
106
        }
107
      } else {
108
        body = fileBody
30✔
109
        headers['cache-control'] = `max-age=${options.cacheControl}`
30✔
110
        headers['content-type'] = options.contentType as string
30✔
111

112
        if (metadata) {
30✔
113
          headers['x-metadata'] = this.toBase64(this.encodeMetadata(metadata))
1✔
114
        }
115
      }
116

117
      if (fileOptions?.headers) {
31!
118
        headers = { ...headers, ...fileOptions.headers }
×
119
      }
120

121
      const cleanPath = this._removeEmptyFolders(path)
31✔
122
      const _path = this._getFinalPath(cleanPath)
31✔
123
      const res = await this.fetch(`${this.url}/object/${_path}`, {
31✔
124
        method,
125
        body: body as BodyInit,
126
        headers,
127
        ...(options?.duplex ? { duplex: options.duplex } : {}),
124!
128
      })
129

130
      const data = await res.json()
31✔
131

132
      if (res.ok) {
31✔
133
        return {
29✔
134
          data: { path: cleanPath, id: data.Id, fullPath: data.Key },
135
          error: null,
136
        }
137
      } else {
138
        const error = data
2✔
139
        return { data: null, error }
2✔
140
      }
141
    } catch (error) {
142
      if (isStorageError(error)) {
×
143
        return { data: null, error }
×
144
      }
145

146
      throw error
×
147
    }
148
  }
149

150
  /**
151
   * Uploads a file to an existing bucket.
152
   *
153
   * @param path The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
154
   * @param fileBody The body of the file to be stored in the bucket.
155
   */
156
  async upload(
157
    path: string,
158
    fileBody: FileBody,
159
    fileOptions?: FileOptions
160
  ): Promise<
161
    | {
162
        data: { id: string; path: string; fullPath: string }
163
        error: null
164
      }
165
    | {
166
        data: null
167
        error: StorageError
168
      }
169
  > {
170
    return this.uploadOrUpdate('POST', path, fileBody, fileOptions)
30✔
171
  }
172

173
  /**
174
   * Upload a file with a token generated from `createSignedUploadUrl`.
175
   * @param path The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
176
   * @param token The token generated from `createSignedUploadUrl`
177
   * @param fileBody The body of the file to be stored in the bucket.
178
   */
179
  async uploadToSignedUrl(
180
    path: string,
181
    token: string,
182
    fileBody: FileBody,
183
    fileOptions?: FileOptions
184
  ) {
185
    const cleanPath = this._removeEmptyFolders(path)
4✔
186
    const _path = this._getFinalPath(cleanPath)
4✔
187

188
    const url = new URL(this.url + `/object/upload/sign/${_path}`)
4✔
189
    url.searchParams.set('token', token)
4✔
190

191
    try {
4✔
192
      let body
193
      const options = { upsert: DEFAULT_FILE_OPTIONS.upsert, ...fileOptions }
4✔
194
      const headers: Record<string, string> = {
4✔
195
        ...this.headers,
196
        ...{ 'x-upsert': String(options.upsert as boolean) },
197
      }
198

199
      if (typeof Blob !== 'undefined' && fileBody instanceof Blob) {
4!
200
        body = new FormData()
×
201
        body.append('cacheControl', options.cacheControl as string)
×
202
        body.append('', fileBody)
×
203
      } else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) {
4!
204
        body = fileBody
×
205
        body.append('cacheControl', options.cacheControl as string)
×
206
      } else {
207
        body = fileBody
4✔
208
        headers['cache-control'] = `max-age=${options.cacheControl}`
4✔
209
        headers['content-type'] = options.contentType as string
4✔
210
      }
211

212
      const res = await this.fetch(url.toString(), {
4✔
213
        method: 'PUT',
214
        body: body as BodyInit,
215
        headers,
216
      })
217

218
      const data = await res.json()
4✔
219

220
      if (res.ok) {
4✔
221
        return {
3✔
222
          data: { path: cleanPath, fullPath: data.Key },
223
          error: null,
224
        }
225
      } else {
226
        const error = data
1✔
227
        return { data: null, error }
1✔
228
      }
229
    } catch (error) {
230
      if (isStorageError(error)) {
×
231
        return { data: null, error }
×
232
      }
233

234
      throw error
×
235
    }
236
  }
237

238
  /**
239
   * Creates a signed upload URL.
240
   * Signed upload URLs can be used to upload files to the bucket without further authentication.
241
   * They are valid for 2 hours.
242
   * @param path The file path, including the current file name. For example `folder/image.png`.
243
   * @param options.upsert If set to true, allows the file to be overwritten if it already exists.
244
   */
245
  async createSignedUploadUrl(
246
    path: string,
247
    options?: { upsert: boolean }
248
  ): Promise<
249
    | {
250
        data: { signedUrl: string; token: string; path: string }
251
        error: null
252
      }
253
    | {
254
        data: null
255
        error: StorageError
256
      }
257
  > {
258
    try {
4✔
259
      let _path = this._getFinalPath(path)
4✔
260

261
      const headers = { ...this.headers }
4✔
262

263
      if (options?.upsert) {
4✔
264
        headers['x-upsert'] = 'true'
1✔
265
      }
266

267
      const data = await post(
4✔
268
        this.fetch,
269
        `${this.url}/object/upload/sign/${_path}`,
270
        {},
271
        { headers }
272
      )
273

274
      const url = new URL(this.url + data.url)
4✔
275

276
      const token = url.searchParams.get('token')
4✔
277

278
      if (!token) {
4!
279
        throw new StorageError('No token returned by API')
×
280
      }
281

282
      return { data: { signedUrl: url.toString(), path, token }, error: null }
4✔
283
    } catch (error) {
284
      if (isStorageError(error)) {
×
285
        return { data: null, error }
×
286
      }
287

288
      throw error
×
289
    }
290
  }
291

292
  /**
293
   * Replaces an existing file at the specified path with a new one.
294
   *
295
   * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to update.
296
   * @param fileBody The body of the file to be stored in the bucket.
297
   */
298
  async update(
299
    path: string,
300
    fileBody:
301
      | ArrayBuffer
302
      | ArrayBufferView
303
      | Blob
304
      | Buffer
305
      | File
306
      | FormData
307
      | NodeJS.ReadableStream
308
      | ReadableStream<Uint8Array>
309
      | URLSearchParams
310
      | string,
311
    fileOptions?: FileOptions
312
  ): Promise<
313
    | {
314
        data: { id: string; path: string; fullPath: string }
315
        error: null
316
      }
317
    | {
318
        data: null
319
        error: StorageError
320
      }
321
  > {
322
    return this.uploadOrUpdate('PUT', path, fileBody, fileOptions)
1✔
323
  }
324

325
  /**
326
   * Moves an existing file to a new path in the same bucket.
327
   *
328
   * @param fromPath The original file path, including the current file name. For example `folder/image.png`.
329
   * @param toPath The new file path, including the new file name. For example `folder/image-new.png`.
330
   * @param options The destination options.
331
   */
332
  async move(
333
    fromPath: string,
334
    toPath: string,
335
    options?: DestinationOptions
336
  ): Promise<
337
    | {
338
        data: { message: string }
339
        error: null
340
      }
341
    | {
342
        data: null
343
        error: StorageError
344
      }
345
  > {
346
    try {
2✔
347
      const data = await post(
2✔
348
        this.fetch,
349
        `${this.url}/object/move`,
350
        {
351
          bucketId: this.bucketId,
352
          sourceKey: fromPath,
353
          destinationKey: toPath,
354
          destinationBucket: options?.destinationBucket,
6✔
355
        },
356
        { headers: this.headers }
357
      )
358
      return { data, error: null }
2✔
359
    } catch (error) {
360
      if (isStorageError(error)) {
×
361
        return { data: null, error }
×
362
      }
363

364
      throw error
×
365
    }
366
  }
367

368
  /**
369
   * Copies an existing file to a new path in the same bucket.
370
   *
371
   * @param fromPath The original file path, including the current file name. For example `folder/image.png`.
372
   * @param toPath The new file path, including the new file name. For example `folder/image-copy.png`.
373
   * @param options The destination options.
374
   */
375
  async copy(
376
    fromPath: string,
377
    toPath: string,
378
    options?: DestinationOptions
379
  ): Promise<
380
    | {
381
        data: { path: string }
382
        error: null
383
      }
384
    | {
385
        data: null
386
        error: StorageError
387
      }
388
  > {
389
    try {
2✔
390
      const data = await post(
2✔
391
        this.fetch,
392
        `${this.url}/object/copy`,
393
        {
394
          bucketId: this.bucketId,
395
          sourceKey: fromPath,
396
          destinationKey: toPath,
397
          destinationBucket: options?.destinationBucket,
6✔
398
        },
399
        { headers: this.headers }
400
      )
401
      return { data: { path: data.Key }, error: null }
2✔
402
    } catch (error) {
403
      if (isStorageError(error)) {
×
404
        return { data: null, error }
×
405
      }
406

407
      throw error
×
408
    }
409
  }
410

411
  /**
412
   * Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.
413
   *
414
   * @param path The file path, including the current file name. For example `folder/image.png`.
415
   * @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
416
   * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
417
   * @param options.transform Transform the asset before serving it to the client.
418
   */
419
  async createSignedUrl(
420
    path: string,
421
    expiresIn: number,
422
    options?: { download?: string | boolean; transform?: TransformOptions }
423
  ): Promise<
424
    | {
425
        data: { signedUrl: string }
426
        error: null
427
      }
428
    | {
429
        data: null
430
        error: StorageError
431
      }
432
  > {
433
    try {
5✔
434
      let _path = this._getFinalPath(path)
5✔
435

436
      let data = await post(
5✔
437
        this.fetch,
438
        `${this.url}/object/sign/${_path}`,
439
        { expiresIn, ...(options?.transform ? { transform: options.transform } : {}) },
20✔
440
        { headers: this.headers }
441
      )
442
      const downloadQueryParam = options?.download
5✔
443
        ? `&download=${options.download === true ? '' : options.download}`
3✔
444
        : ''
445
      const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`)
5✔
446
      data = { signedUrl }
5✔
447
      return { data, error: null }
5✔
448
    } catch (error) {
449
      if (isStorageError(error)) {
×
450
        return { data: null, error }
×
451
      }
452

453
      throw error
×
454
    }
455
  }
456

457
  /**
458
   * Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time.
459
   *
460
   * @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`.
461
   * @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
462
   * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
463
   */
464
  async createSignedUrls(
465
    paths: string[],
466
    expiresIn: number,
467
    options?: { download: string | boolean }
468
  ): Promise<
469
    | {
470
        data: { error: string | null; path: string | null; signedUrl: string }[]
471
        error: null
472
      }
473
    | {
474
        data: null
475
        error: StorageError
476
      }
477
  > {
478
    try {
×
479
      const data = await post(
×
480
        this.fetch,
481
        `${this.url}/object/sign/${this.bucketId}`,
482
        { expiresIn, paths },
483
        { headers: this.headers }
484
      )
485

486
      const downloadQueryParam = options?.download
×
487
        ? `&download=${options.download === true ? '' : options.download}`
×
488
        : ''
489
      return {
×
490
        data: data.map((datum: { signedURL: string }) => ({
×
491
          ...datum,
492
          signedUrl: datum.signedURL
×
493
            ? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`)
494
            : null,
495
        })),
496
        error: null,
497
      }
498
    } catch (error) {
499
      if (isStorageError(error)) {
×
500
        return { data: null, error }
×
501
      }
502

503
      throw error
×
504
    }
505
  }
506

507
  /**
508
   * Downloads a file from a private bucket. For public buckets, make a request to the URL returned from `getPublicUrl` instead.
509
   *
510
   * @param path The full path and file name of the file to be downloaded. For example `folder/image.png`.
511
   * @param options.transform Transform the asset before serving it to the client.
512
   */
513
  async download(
514
    path: string,
515
    options?: { transform?: TransformOptions }
516
  ): Promise<
517
    | {
518
        data: Blob
519
        error: null
520
      }
521
    | {
522
        data: null
523
        error: StorageError
524
      }
525
  > {
526
    const wantsTransformation = typeof options?.transform !== 'undefined'
5✔
527
    const renderPath = wantsTransformation ? 'render/image/authenticated' : 'object'
5✔
528
    const transformationQuery = this.transformOptsToQueryString(options?.transform || {})
5✔
529
    const queryString = transformationQuery ? `?${transformationQuery}` : ''
5✔
530

531
    try {
5✔
532
      const _path = this._getFinalPath(path)
5✔
533
      const res = await get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString}`, {
5✔
534
        headers: this.headers,
535
        noResolveJson: true,
536
      })
537
      const data = await res.blob()
5✔
538
      return { data, error: null }
5✔
539
    } catch (error) {
540
      if (isStorageError(error)) {
×
541
        return { data: null, error }
×
542
      }
543

544
      throw error
×
545
    }
546
  }
547

548
  /**
549
   * Retrieves the details of an existing file.
550
   * @param path
551
   */
552
  async info(
553
    path: string
554
  ): Promise<
555
    | {
556
        data: Camelize<FileObjectV2>
557
        error: null
558
      }
559
    | {
560
        data: null
561
        error: StorageError
562
      }
563
  > {
564
    const _path = this._getFinalPath(path)
2✔
565

566
    try {
2✔
567
      const data = await get(this.fetch, `${this.url}/object/info/${_path}`, {
2✔
568
        headers: this.headers,
569
      })
570

571
      return { data: recursiveToCamel(data) as Camelize<FileObjectV2>, error: null }
2✔
572
    } catch (error) {
573
      if (isStorageError(error)) {
×
574
        return { data: null, error }
×
575
      }
576

577
      throw error
×
578
    }
579
  }
580

581
  /**
582
   * Checks the existence of a file.
583
   * @param path
584
   */
585
  async exists(
586
    path: string
587
  ): Promise<
588
    | {
589
        data: boolean
590
        error: null
591
      }
592
    | {
593
        data: boolean
594
        error: StorageError
595
      }
596
  > {
597
    const _path = this._getFinalPath(path)
2✔
598

599
    try {
2✔
600
      await head(this.fetch, `${this.url}/object/${_path}`, {
2✔
601
        headers: this.headers,
602
      })
603

604
      return { data: true, error: null }
1✔
605
    } catch (error) {
606
      if (isStorageError(error) && error instanceof StorageUnknownError) {
1✔
607
        const originalError = (error.originalError as unknown) as { status: number }
1✔
608

609
        if ([400, 404].includes(originalError?.status)) {
1!
610
          return { data: false, error }
1✔
611
        }
612
      }
613

614
      throw error
×
615
    }
616
  }
617

618
  /**
619
   * A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset.
620
   * This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset.
621
   *
622
   * @param path The path and name of the file to generate the public URL for. For example `folder/image.png`.
623
   * @param options.download Triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
624
   * @param options.transform Transform the asset before serving it to the client.
625
   */
626
  getPublicUrl(
627
    path: string,
628
    options?: { download?: string | boolean; transform?: TransformOptions }
629
  ): { data: { publicUrl: string } } {
630
    const _path = this._getFinalPath(path)
4✔
631
    const _queryString = []
4✔
632

633
    const downloadQueryParam = options?.download
4✔
634
      ? `download=${options.download === true ? '' : options.download}`
2✔
635
      : ''
636

637
    if (downloadQueryParam !== '') {
4✔
638
      _queryString.push(downloadQueryParam)
2✔
639
    }
640

641
    const wantsTransformation = typeof options?.transform !== 'undefined'
4✔
642
    const renderPath = wantsTransformation ? 'render/image' : 'object'
4✔
643
    const transformationQuery = this.transformOptsToQueryString(options?.transform || {})
4✔
644

645
    if (transformationQuery !== '') {
4✔
646
      _queryString.push(transformationQuery)
1✔
647
    }
648

649
    let queryString = _queryString.join('&')
4✔
650
    if (queryString !== '') {
4✔
651
      queryString = `?${queryString}`
3✔
652
    }
653

654
    return {
4✔
655
      data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) },
656
    }
657
  }
658

659
  /**
660
   * Deletes files within the same bucket
661
   *
662
   * @param paths An array of files to delete, including the path and file name. For example [`'folder/image.png'`].
663
   */
664
  async remove(
665
    paths: string[]
666
  ): Promise<
667
    | {
668
        data: FileObject[]
669
        error: null
670
      }
671
    | {
672
        data: null
673
        error: StorageError
674
      }
675
  > {
676
    try {
1✔
677
      const data = await remove(
1✔
678
        this.fetch,
679
        `${this.url}/object/${this.bucketId}`,
680
        { prefixes: paths },
681
        { headers: this.headers }
682
      )
683
      return { data, error: null }
1✔
684
    } catch (error) {
685
      if (isStorageError(error)) {
×
686
        return { data: null, error }
×
687
      }
688

689
      throw error
×
690
    }
691
  }
692

693
  /**
694
   * Get file metadata
695
   * @param id the file id to retrieve metadata
696
   */
697
  // async getMetadata(
698
  //   id: string
699
  // ): Promise<
700
  //   | {
701
  //       data: Metadata
702
  //       error: null
703
  //     }
704
  //   | {
705
  //       data: null
706
  //       error: StorageError
707
  //     }
708
  // > {
709
  //   try {
710
  //     const data = await get(this.fetch, `${this.url}/metadata/${id}`, { headers: this.headers })
711
  //     return { data, error: null }
712
  //   } catch (error) {
713
  //     if (isStorageError(error)) {
714
  //       return { data: null, error }
715
  //     }
716

717
  //     throw error
718
  //   }
719
  // }
720

721
  /**
722
   * Update file metadata
723
   * @param id the file id to update metadata
724
   * @param meta the new file metadata
725
   */
726
  // async updateMetadata(
727
  //   id: string,
728
  //   meta: Metadata
729
  // ): Promise<
730
  //   | {
731
  //       data: Metadata
732
  //       error: null
733
  //     }
734
  //   | {
735
  //       data: null
736
  //       error: StorageError
737
  //     }
738
  // > {
739
  //   try {
740
  //     const data = await post(
741
  //       this.fetch,
742
  //       `${this.url}/metadata/${id}`,
743
  //       { ...meta },
744
  //       { headers: this.headers }
745
  //     )
746
  //     return { data, error: null }
747
  //   } catch (error) {
748
  //     if (isStorageError(error)) {
749
  //       return { data: null, error }
750
  //     }
751

752
  //     throw error
753
  //   }
754
  // }
755

756
  /**
757
   * Lists all the files within a bucket.
758
   * @param path The folder path.
759
   */
760
  async list(
761
    path?: string,
762
    options?: SearchOptions,
763
    parameters?: FetchParameters
764
  ): Promise<
765
    | {
766
        data: FileObject[]
767
        error: null
768
      }
769
    | {
770
        data: null
771
        error: StorageError
772
      }
773
  > {
774
    try {
1✔
775
      const body = { ...DEFAULT_SEARCH_OPTIONS, ...options, prefix: path || '' }
1!
776
      const data = await post(
1✔
777
        this.fetch,
778
        `${this.url}/object/list/${this.bucketId}`,
779
        body,
780
        { headers: this.headers },
781
        parameters
782
      )
783
      return { data, error: null }
1✔
784
    } catch (error) {
785
      if (isStorageError(error)) {
×
786
        return { data: null, error }
×
787
      }
788

789
      throw error
×
790
    }
791
  }
792

793
  async listV2(
794
    path?: string,
795
    options?: SearchV2Options,
796
    parameters?: FetchParameters
797
  ): Promise<
798
    | {
799
        data: SearchV2Result[]
800
        error: null
801
      }
802
    | {
803
        data: null
804
        error: StorageError
805
      }
806
  > {
807
    try {
1✔
808
      const body = { ...options, prefix: path || '' }
1!
809
      const data = await post(
1✔
810
        this.fetch,
811
        `${this.url}/object/list-v2/${this.bucketId}`,
812
        body,
813
        { headers: this.headers },
814
        parameters
815
      )
816
      return { data, error: null }
1✔
817
    } catch (error) {
NEW
818
      if (isStorageError(error)) {
×
NEW
819
        return { data: null, error }
×
820
      }
821

NEW
822
      throw error
×
823
    }
824
  }
825

826
  protected encodeMetadata(metadata: Record<string, any>) {
827
    return JSON.stringify(metadata)
1✔
828
  }
829

830
  toBase64(data: string) {
831
    if (typeof Buffer !== 'undefined') {
1✔
832
      return Buffer.from(data).toString('base64')
1✔
833
    }
UNCOV
834
    return btoa(data)
×
835
  }
836

837
  private _getFinalPath(path: string) {
838
    return `${this.bucketId}/${path}`
57✔
839
  }
840

841
  private _removeEmptyFolders(path: string) {
842
    return path.replace(/^\/|\/$/g, '').replace(/\/+/g, '/')
35✔
843
  }
844

845
  private transformOptsToQueryString(transform: TransformOptions) {
846
    const params = []
9✔
847
    if (transform.width) {
9✔
848
      params.push(`width=${transform.width}`)
4✔
849
    }
850

851
    if (transform.height) {
9✔
852
      params.push(`height=${transform.height}`)
4✔
853
    }
854

855
    if (transform.resize) {
9!
UNCOV
856
      params.push(`resize=${transform.resize}`)
×
857
    }
858

859
    if (transform.format) {
9✔
860
      params.push(`format=${transform.format}`)
1✔
861
    }
862

863
    if (transform.quality) {
9✔
864
      params.push(`quality=${transform.quality}`)
1✔
865
    }
866

867
    return params.join('&')
9✔
868
  }
869
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc