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

supabase / storage-js / 14871076232

06 May 2025 10:37PM UTC coverage: 85.311% (+1.0%) from 84.272%
14871076232

push

github

web-flow
Merge pull request #226 from supabase/fix/resolve-regression-from-upload-error-change

fix: resolve regression with uploading files introduced by #216

172 of 209 branches covered (82.3%)

Branch coverage included in aggregate %.

12 of 13 new or added lines in 4 files covered. (92.31%)

24 existing lines in 1 file now uncovered.

281 of 322 relevant lines covered (87.27%)

16.2 hits per line

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

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

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

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

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

42
export default class StorageFileApi {
4✔
43
  protected url: string
44
  protected headers: { [key: string]: string }
45
  protected bucketId?: string
46
  protected fetch: Fetch
47

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

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

90
      const metadata = options.metadata
28✔
91

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

110
        if (metadata) {
27✔
111
          headers['x-metadata'] = this.toBase64(this.encodeMetadata(metadata))
1✔
112
        }
113
      }
114

115
      if (fileOptions?.headers) {
28!
116
        headers = { ...headers, ...fileOptions.headers }
×
117
      }
118

119
      const cleanPath = this._removeEmptyFolders(path)
28✔
120
      const _path = this._getFinalPath(cleanPath)
28✔
121
      const data = await (method == 'PUT' ? put : post)(
28✔
122
        this.fetch,
123
        `${this.url}/object/${_path}`,
124
        body as object,
125
        { headers, ...(options?.duplex ? { duplex: options.duplex } : {}) }
112!
126
      )
127

128
      return {
26✔
129
        data: { path: cleanPath, id: data.Id, fullPath: data.Key },
130
        error: null,
131
      }
132
    } catch (error) {
133
      if (isStorageError(error)) {
2✔
134
        return { data: null, error }
2✔
135
      }
136

UNCOV
137
      throw error
×
138
    }
139
  }
140

141
  /**
142
   * Uploads a file to an existing bucket.
143
   *
144
   * @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.
145
   * @param fileBody The body of the file to be stored in the bucket.
146
   */
147
  async upload(
148
    path: string,
149
    fileBody: FileBody,
150
    fileOptions?: FileOptions
151
  ): Promise<
152
    | {
153
        data: { id: string; path: string; fullPath: string }
154
        error: null
155
      }
156
    | {
157
        data: null
158
        error: StorageError
159
      }
160
  > {
161
    return this.uploadOrUpdate('POST', path, fileBody, fileOptions)
27✔
162
  }
163

164
  /**
165
   * Upload a file with a token generated from `createSignedUploadUrl`.
166
   * @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.
167
   * @param token The token generated from `createSignedUploadUrl`
168
   * @param fileBody The body of the file to be stored in the bucket.
169
   */
170
  async uploadToSignedUrl(
171
    path: string,
172
    token: string,
173
    fileBody: FileBody,
174
    fileOptions?: FileOptions
175
  ) {
176
    const cleanPath = this._removeEmptyFolders(path)
4✔
177
    const _path = this._getFinalPath(cleanPath)
4✔
178

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

182
    try {
4✔
183
      let body
184
      const options = { upsert: DEFAULT_FILE_OPTIONS.upsert, ...fileOptions }
4✔
185
      const headers: Record<string, string> = {
4✔
186
        ...this.headers,
187
        ...{ 'x-upsert': String(options.upsert as boolean) },
188
      }
189

190
      if (typeof Blob !== 'undefined' && fileBody instanceof Blob) {
4!
UNCOV
191
        body = new FormData()
×
UNCOV
192
        body.append('cacheControl', options.cacheControl as string)
×
UNCOV
193
        body.append('', fileBody)
×
194
      } else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) {
4!
UNCOV
195
        body = fileBody
×
UNCOV
196
        body.append('cacheControl', options.cacheControl as string)
×
197
      } else {
198
        body = fileBody
4✔
199
        headers['cache-control'] = `max-age=${options.cacheControl}`
4✔
200
        headers['content-type'] = options.contentType as string
4✔
201
      }
202

203
      const data = await put(this.fetch, url.toString(), body as object, { headers })
4✔
204

205
      return {
3✔
206
        data: { path: cleanPath, fullPath: data.Key },
207
        error: null,
208
      }
209
    } catch (error) {
210
      if (isStorageError(error)) {
1✔
211
        return { data: null, error }
1✔
212
      }
213

UNCOV
214
      throw error
×
215
    }
216
  }
217

218
  /**
219
   * Creates a signed upload URL.
220
   * Signed upload URLs can be used to upload files to the bucket without further authentication.
221
   * They are valid for 2 hours.
222
   * @param path The file path, including the current file name. For example `folder/image.png`.
223
   * @param options.upsert If set to true, allows the file to be overwritten if it already exists.
224
   */
225
  async createSignedUploadUrl(
226
    path: string,
227
    options?: { upsert: boolean }
228
  ): Promise<
229
    | {
230
        data: { signedUrl: string; token: string; path: string }
231
        error: null
232
      }
233
    | {
234
        data: null
235
        error: StorageError
236
      }
237
  > {
238
    try {
4✔
239
      let _path = this._getFinalPath(path)
4✔
240

241
      const headers = { ...this.headers }
4✔
242

243
      if (options?.upsert) {
4✔
244
        headers['x-upsert'] = 'true'
1✔
245
      }
246

247
      const data = await post(
4✔
248
        this.fetch,
249
        `${this.url}/object/upload/sign/${_path}`,
250
        {},
251
        { headers }
252
      )
253

254
      const url = new URL(this.url + data.url)
4✔
255

256
      const token = url.searchParams.get('token')
4✔
257

258
      if (!token) {
4!
UNCOV
259
        throw new StorageError('No token returned by API')
×
260
      }
261

262
      return { data: { signedUrl: url.toString(), path, token }, error: null }
4✔
263
    } catch (error) {
UNCOV
264
      if (isStorageError(error)) {
×
UNCOV
265
        return { data: null, error }
×
266
      }
267

UNCOV
268
      throw error
×
269
    }
270
  }
271

272
  /**
273
   * Replaces an existing file at the specified path with a new one.
274
   *
275
   * @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to update.
276
   * @param fileBody The body of the file to be stored in the bucket.
277
   */
278
  async update(
279
    path: string,
280
    fileBody:
281
      | ArrayBuffer
282
      | ArrayBufferView
283
      | Blob
284
      | Buffer
285
      | File
286
      | FormData
287
      | NodeJS.ReadableStream
288
      | ReadableStream<Uint8Array>
289
      | URLSearchParams
290
      | string,
291
    fileOptions?: FileOptions
292
  ): Promise<
293
    | {
294
        data: { id: string; path: string; fullPath: string }
295
        error: null
296
      }
297
    | {
298
        data: null
299
        error: StorageError
300
      }
301
  > {
302
    return this.uploadOrUpdate('PUT', path, fileBody, fileOptions)
1✔
303
  }
304

305
  /**
306
   * Moves an existing file to a new path in the same bucket.
307
   *
308
   * @param fromPath The original file path, including the current file name. For example `folder/image.png`.
309
   * @param toPath The new file path, including the new file name. For example `folder/image-new.png`.
310
   * @param options The destination options.
311
   */
312
  async move(
313
    fromPath: string,
314
    toPath: string,
315
    options?: DestinationOptions
316
  ): Promise<
317
    | {
318
        data: { message: string }
319
        error: null
320
      }
321
    | {
322
        data: null
323
        error: StorageError
324
      }
325
  > {
326
    try {
5✔
327
      const data = await post(
5✔
328
        this.fetch,
329
        `${this.url}/object/move`,
330
        {
331
          bucketId: this.bucketId,
332
          sourceKey: fromPath,
333
          destinationKey: toPath,
334
          destinationBucket: options?.destinationBucket,
15✔
335
        },
336
        { headers: this.headers }
337
      )
338
      return { data, error: null }
2✔
339
    } catch (error) {
340
      if (isStorageError(error)) {
3✔
341
        return { data: null, error }
2✔
342
      }
343

344
      throw error
1✔
345
    }
346
  }
347

348
  /**
349
   * Copies an existing file to a new path in the same bucket.
350
   *
351
   * @param fromPath The original file path, including the current file name. For example `folder/image.png`.
352
   * @param toPath The new file path, including the new file name. For example `folder/image-copy.png`.
353
   * @param options The destination options.
354
   */
355
  async copy(
356
    fromPath: string,
357
    toPath: string,
358
    options?: DestinationOptions
359
  ): Promise<
360
    | {
361
        data: { path: string }
362
        error: null
363
      }
364
    | {
365
        data: null
366
        error: StorageError
367
      }
368
  > {
369
    try {
5✔
370
      const data = await post(
5✔
371
        this.fetch,
372
        `${this.url}/object/copy`,
373
        {
374
          bucketId: this.bucketId,
375
          sourceKey: fromPath,
376
          destinationKey: toPath,
377
          destinationBucket: options?.destinationBucket,
15✔
378
        },
379
        { headers: this.headers }
380
      )
381
      return { data: { path: data.Key }, error: null }
2✔
382
    } catch (error) {
383
      if (isStorageError(error)) {
3✔
384
        return { data: null, error }
2✔
385
      }
386

387
      throw error
1✔
388
    }
389
  }
390

391
  /**
392
   * Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.
393
   *
394
   * @param path The file path, including the current file name. For example `folder/image.png`.
395
   * @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
396
   * @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.
397
   * @param options.transform Transform the asset before serving it to the client.
398
   */
399
  async createSignedUrl(
400
    path: string,
401
    expiresIn: number,
402
    options?: { download?: string | boolean; transform?: TransformOptions }
403
  ): Promise<
404
    | {
405
        data: { signedUrl: string }
406
        error: null
407
      }
408
    | {
409
        data: null
410
        error: StorageError
411
      }
412
  > {
413
    try {
8✔
414
      let _path = this._getFinalPath(path)
8✔
415

416
      let data = await post(
8✔
417
        this.fetch,
418
        `${this.url}/object/sign/${_path}`,
419
        { expiresIn, ...(options?.transform ? { transform: options.transform } : {}) },
32✔
420
        { headers: this.headers }
421
      )
422
      const downloadQueryParam = options?.download
5✔
423
        ? `&download=${options.download === true ? '' : options.download}`
3✔
424
        : ''
425
      const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`)
5✔
426
      data = { signedUrl }
5✔
427
      return { data, error: null }
5✔
428
    } catch (error) {
429
      if (isStorageError(error)) {
3✔
430
        return { data: null, error }
2✔
431
      }
432

433
      throw error
1✔
434
    }
435
  }
436

437
  /**
438
   * Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time.
439
   *
440
   * @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`.
441
   * @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
442
   * @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.
443
   */
444
  async createSignedUrls(
445
    paths: string[],
446
    expiresIn: number,
447
    options?: { download: string | boolean }
448
  ): Promise<
449
    | {
450
        data: { error: string | null; path: string | null; signedUrl: string }[]
451
        error: null
452
      }
453
    | {
454
        data: null
455
        error: StorageError
456
      }
457
  > {
UNCOV
458
    try {
×
UNCOV
459
      const data = await post(
×
460
        this.fetch,
461
        `${this.url}/object/sign/${this.bucketId}`,
462
        { expiresIn, paths },
463
        { headers: this.headers }
464
      )
465

UNCOV
466
      const downloadQueryParam = options?.download
×
467
        ? `&download=${options.download === true ? '' : options.download}`
×
468
        : ''
UNCOV
469
      return {
×
UNCOV
470
        data: data.map((datum: { signedURL: string }) => ({
×
471
          ...datum,
472
          signedUrl: datum.signedURL
×
473
            ? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`)
474
            : null,
475
        })),
476
        error: null,
477
      }
478
    } catch (error) {
479
      if (isStorageError(error)) {
×
UNCOV
480
        return { data: null, error }
×
481
      }
482

483
      throw error
×
484
    }
485
  }
486

487
  /**
488
   * Downloads a file from a private bucket. For public buckets, make a request to the URL returned from `getPublicUrl` instead.
489
   *
490
   * @param path The full path and file name of the file to be downloaded. For example `folder/image.png`.
491
   * @param options.transform Transform the asset before serving it to the client.
492
   */
493
  async download(
494
    path: string,
495
    options?: { transform?: TransformOptions }
496
  ): Promise<
497
    | {
498
        data: Blob
499
        error: null
500
      }
501
    | {
502
        data: null
503
        error: StorageError
504
      }
505
  > {
506
    const wantsTransformation = typeof options?.transform !== 'undefined'
6✔
507
    const renderPath = wantsTransformation ? 'render/image/authenticated' : 'object'
6✔
508
    const transformationQuery = this.transformOptsToQueryString(options?.transform || {})
6✔
509
    const queryString = transformationQuery ? `?${transformationQuery}` : ''
6✔
510

511
    try {
6✔
512
      const _path = this._getFinalPath(path)
6✔
513
      const res = await get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString}`, {
6✔
514
        headers: this.headers,
515
        noResolveJson: true,
516
      })
517
      const data = await res.blob()
3✔
518
      return { data, error: null }
3✔
519
    } catch (error) {
520
      if (isStorageError(error)) {
3✔
521
        return { data: null, error }
2✔
522
      }
523

524
      throw error
1✔
525
    }
526
  }
527

528
  /**
529
   * Retrieves the details of an existing file.
530
   * @param path
531
   */
532
  async info(
533
    path: string
534
  ): Promise<
535
    | {
536
        data: Camelize<FileObjectV2>
537
        error: null
538
      }
539
    | {
540
        data: null
541
        error: StorageError
542
      }
543
  > {
544
    const _path = this._getFinalPath(path)
2✔
545

546
    try {
2✔
547
      const data = await get(this.fetch, `${this.url}/object/info/${_path}`, {
2✔
548
        headers: this.headers,
549
      })
550

551
      return { data: recursiveToCamel(data) as Camelize<FileObjectV2>, error: null }
2✔
552
    } catch (error) {
UNCOV
553
      if (isStorageError(error)) {
×
UNCOV
554
        return { data: null, error }
×
555
      }
556

UNCOV
557
      throw error
×
558
    }
559
  }
560

561
  /**
562
   * Checks the existence of a file.
563
   * @param path
564
   */
565
  async exists(
566
    path: string
567
  ): Promise<
568
    | {
569
        data: boolean
570
        error: null
571
      }
572
    | {
573
        data: boolean
574
        error: StorageError
575
      }
576
  > {
577
    const _path = this._getFinalPath(path)
2✔
578

579
    try {
2✔
580
      await head(this.fetch, `${this.url}/object/${_path}`, {
2✔
581
        headers: this.headers,
582
      })
583

584
      return { data: true, error: null }
1✔
585
    } catch (error) {
586
      if (isStorageError(error) && error instanceof StorageUnknownError) {
1✔
587
        const originalError = (error.originalError as unknown) as { status: number }
1✔
588

589
        if ([400, 404].includes(originalError?.status)) {
1!
590
          return { data: false, error }
1✔
591
        }
592
      }
593

UNCOV
594
      throw error
×
595
    }
596
  }
597

598
  /**
599
   * 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.
600
   * 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.
601
   *
602
   * @param path The path and name of the file to generate the public URL for. For example `folder/image.png`.
603
   * @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.
604
   * @param options.transform Transform the asset before serving it to the client.
605
   */
606
  getPublicUrl(
607
    path: string,
608
    options?: { download?: string | boolean; transform?: TransformOptions }
609
  ): { data: { publicUrl: string } } {
610
    const _path = this._getFinalPath(path)
4✔
611
    const _queryString = []
4✔
612

613
    const downloadQueryParam = options?.download
4✔
614
      ? `download=${options.download === true ? '' : options.download}`
2✔
615
      : ''
616

617
    if (downloadQueryParam !== '') {
4✔
618
      _queryString.push(downloadQueryParam)
2✔
619
    }
620

621
    const wantsTransformation = typeof options?.transform !== 'undefined'
4✔
622
    const renderPath = wantsTransformation ? 'render/image' : 'object'
4✔
623
    const transformationQuery = this.transformOptsToQueryString(options?.transform || {})
4✔
624

625
    if (transformationQuery !== '') {
4✔
626
      _queryString.push(transformationQuery)
1✔
627
    }
628

629
    let queryString = _queryString.join('&')
4✔
630
    if (queryString !== '') {
4✔
631
      queryString = `?${queryString}`
3✔
632
    }
633

634
    return {
4✔
635
      data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) },
636
    }
637
  }
638

639
  /**
640
   * Deletes files within the same bucket
641
   *
642
   * @param paths An array of files to delete, including the path and file name. For example [`'folder/image.png'`].
643
   */
644
  async remove(
645
    paths: string[]
646
  ): Promise<
647
    | {
648
        data: FileObject[]
649
        error: null
650
      }
651
    | {
652
        data: null
653
        error: StorageError
654
      }
655
  > {
656
    try {
4✔
657
      const data = await remove(
4✔
658
        this.fetch,
659
        `${this.url}/object/${this.bucketId}`,
660
        { prefixes: paths },
661
        { headers: this.headers }
662
      )
663
      return { data, error: null }
1✔
664
    } catch (error) {
665
      if (isStorageError(error)) {
3✔
666
        return { data: null, error }
2✔
667
      }
668

669
      throw error
1✔
670
    }
671
  }
672

673
  /**
674
   * Get file metadata
675
   * @param id the file id to retrieve metadata
676
   */
677
  // async getMetadata(
678
  //   id: string
679
  // ): Promise<
680
  //   | {
681
  //       data: Metadata
682
  //       error: null
683
  //     }
684
  //   | {
685
  //       data: null
686
  //       error: StorageError
687
  //     }
688
  // > {
689
  //   try {
690
  //     const data = await get(this.fetch, `${this.url}/metadata/${id}`, { headers: this.headers })
691
  //     return { data, error: null }
692
  //   } catch (error) {
693
  //     if (isStorageError(error)) {
694
  //       return { data: null, error }
695
  //     }
696

697
  //     throw error
698
  //   }
699
  // }
700

701
  /**
702
   * Update file metadata
703
   * @param id the file id to update metadata
704
   * @param meta the new file metadata
705
   */
706
  // async updateMetadata(
707
  //   id: string,
708
  //   meta: Metadata
709
  // ): Promise<
710
  //   | {
711
  //       data: Metadata
712
  //       error: null
713
  //     }
714
  //   | {
715
  //       data: null
716
  //       error: StorageError
717
  //     }
718
  // > {
719
  //   try {
720
  //     const data = await post(
721
  //       this.fetch,
722
  //       `${this.url}/metadata/${id}`,
723
  //       { ...meta },
724
  //       { headers: this.headers }
725
  //     )
726
  //     return { data, error: null }
727
  //   } catch (error) {
728
  //     if (isStorageError(error)) {
729
  //       return { data: null, error }
730
  //     }
731

732
  //     throw error
733
  //   }
734
  // }
735

736
  /**
737
   * Lists all the files within a bucket.
738
   * @param path The folder path.
739
   */
740
  async list(
741
    path?: string,
742
    options?: SearchOptions,
743
    parameters?: FetchParameters
744
  ): Promise<
745
    | {
746
        data: FileObject[]
747
        error: null
748
      }
749
    | {
750
        data: null
751
        error: StorageError
752
      }
753
  > {
754
    try {
7✔
755
      const body = { ...DEFAULT_SEARCH_OPTIONS, ...options, prefix: path || '' }
7✔
756
      const data = await post(
7✔
757
        this.fetch,
758
        `${this.url}/object/list/${this.bucketId}`,
759
        body,
760
        { headers: this.headers },
761
        parameters
762
      )
763
      return { data, error: null }
1✔
764
    } catch (error) {
765
      if (isStorageError(error)) {
6✔
766
        return { data: null, error }
5✔
767
      }
768

769
      throw error
1✔
770
    }
771
  }
772

773
  protected encodeMetadata(metadata: Record<string, any>) {
774
    return JSON.stringify(metadata)
1✔
775
  }
776

777
  toBase64(data: string) {
778
    if (typeof Buffer !== 'undefined') {
1✔
779
      return Buffer.from(data).toString('base64')
1✔
780
    }
UNCOV
781
    return btoa(data)
×
782
  }
783

784
  private _getFinalPath(path: string) {
785
    return `${this.bucketId}/${path}`
58✔
786
  }
787

788
  private _removeEmptyFolders(path: string) {
789
    return path.replace(/^\/|\/$/g, '').replace(/\/+/g, '/')
32✔
790
  }
791

792
  private transformOptsToQueryString(transform: TransformOptions) {
793
    const params = []
10✔
794
    if (transform.width) {
10✔
795
      params.push(`width=${transform.width}`)
2✔
796
    }
797

798
    if (transform.height) {
10✔
799
      params.push(`height=${transform.height}`)
2✔
800
    }
801

802
    if (transform.resize) {
10!
UNCOV
803
      params.push(`resize=${transform.resize}`)
×
804
    }
805

806
    if (transform.format) {
10!
UNCOV
807
      params.push(`format=${transform.format}`)
×
808
    }
809

810
    if (transform.quality) {
10✔
811
      params.push(`quality=${transform.quality}`)
1✔
812
    }
813

814
    return params.join('&')
10✔
815
  }
816
}
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