|
1 | | -import { isStorageError, StorageError } from '../lib/errors' |
2 | | -import { Fetch, get, post, remove } from '../lib/fetch' |
3 | | -import { resolveFetch } from '../lib/helpers' |
| 1 | +import { isStorageError, StorageError, StorageUnknownError } from '../lib/errors' |
| 2 | +import { Fetch, get, head, post, remove } from '../lib/fetch' |
| 3 | +import { recursiveToCamel, resolveFetch } from '../lib/helpers' |
4 | 4 | import { |
5 | 5 | FileObject, |
6 | 6 | FileOptions, |
7 | 7 | SearchOptions, |
8 | 8 | FetchParameters, |
9 | 9 | TransformOptions, |
10 | 10 | DestinationOptions, |
| 11 | + FileObjectV2, |
| 12 | + Camelize, |
11 | 13 | } from '../lib/types' |
12 | 14 |
|
13 | 15 | const DEFAULT_SEARCH_OPTIONS = { |
@@ -80,22 +82,39 @@ export default class StorageFileApi { |
80 | 82 | try { |
81 | 83 | let body |
82 | 84 | const options = { ...DEFAULT_FILE_OPTIONS, ...fileOptions } |
83 | | - const headers: Record<string, string> = { |
| 85 | + let headers: Record<string, string> = { |
84 | 86 | ...this.headers, |
85 | 87 | ...(method === 'POST' && { 'x-upsert': String(options.upsert as boolean) }), |
86 | 88 | } |
87 | 89 |
|
| 90 | + const metadata = options.metadata |
| 91 | + |
88 | 92 | if (typeof Blob !== 'undefined' && fileBody instanceof Blob) { |
89 | 93 | body = new FormData() |
90 | 94 | body.append('cacheControl', options.cacheControl as string) |
91 | 95 | body.append('', fileBody) |
| 96 | + |
| 97 | + if (metadata) { |
| 98 | + body.append('metadata', this.encodeMetadata(metadata)) |
| 99 | + } |
92 | 100 | } else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) { |
93 | 101 | body = fileBody |
94 | 102 | body.append('cacheControl', options.cacheControl as string) |
| 103 | + if (metadata) { |
| 104 | + body.append('metadata', this.encodeMetadata(metadata)) |
| 105 | + } |
95 | 106 | } else { |
96 | 107 | body = fileBody |
97 | 108 | headers['cache-control'] = `max-age=${options.cacheControl}` |
98 | 109 | headers['content-type'] = options.contentType as string |
| 110 | + |
| 111 | + if (metadata) { |
| 112 | + headers['x-metadata'] = this.toBase64(this.encodeMetadata(metadata)) |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + if (fileOptions?.headers) { |
| 117 | + headers = { ...headers, ...fileOptions.headers } |
99 | 118 | } |
100 | 119 |
|
101 | 120 | const cleanPath = this._removeEmptyFolders(path) |
@@ -525,6 +544,76 @@ export default class StorageFileApi { |
525 | 544 | } |
526 | 545 | } |
527 | 546 |
|
| 547 | + /** |
| 548 | + * Retrieves the details of an existing file. |
| 549 | + * @param path |
| 550 | + */ |
| 551 | + async info( |
| 552 | + path: string |
| 553 | + ): Promise< |
| 554 | + | { |
| 555 | + data: Camelize<FileObjectV2> |
| 556 | + error: null |
| 557 | + } |
| 558 | + | { |
| 559 | + data: null |
| 560 | + error: StorageError |
| 561 | + } |
| 562 | + > { |
| 563 | + const _path = this._getFinalPath(path) |
| 564 | + |
| 565 | + try { |
| 566 | + const data = await get(this.fetch, `${this.url}/object/info/${_path}`, { |
| 567 | + headers: this.headers, |
| 568 | + }) |
| 569 | + |
| 570 | + return { data: recursiveToCamel(data) as Camelize<FileObjectV2>, error: null } |
| 571 | + } catch (error) { |
| 572 | + if (isStorageError(error)) { |
| 573 | + return { data: null, error } |
| 574 | + } |
| 575 | + |
| 576 | + throw error |
| 577 | + } |
| 578 | + } |
| 579 | + |
| 580 | + /** |
| 581 | + * Checks the existence of a file. |
| 582 | + * @param path |
| 583 | + */ |
| 584 | + async exists( |
| 585 | + path: string |
| 586 | + ): Promise< |
| 587 | + | { |
| 588 | + data: boolean |
| 589 | + error: null |
| 590 | + } |
| 591 | + | { |
| 592 | + data: boolean |
| 593 | + error: StorageError |
| 594 | + } |
| 595 | + > { |
| 596 | + const _path = this._getFinalPath(path) |
| 597 | + |
| 598 | + try { |
| 599 | + await head(this.fetch, `${this.url}/object/${_path}`, { |
| 600 | + headers: this.headers, |
| 601 | + }) |
| 602 | + |
| 603 | + return { data: true, error: null } |
| 604 | + } catch (error) { |
| 605 | + if (isStorageError(error) && error instanceof StorageUnknownError) { |
| 606 | + const originalError = (error.originalError as unknown) as { status: number } |
| 607 | + |
| 608 | + if ([400, 404].includes(originalError?.status)) { |
| 609 | + return { data: false, error } |
| 610 | + } |
| 611 | + } |
| 612 | + |
| 613 | + throw error |
| 614 | + } |
| 615 | + } |
| 616 | + |
528 | 617 | /** |
529 | 618 | * 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. |
530 | 619 | * 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. |
@@ -700,6 +789,17 @@ export default class StorageFileApi { |
700 | 789 | } |
701 | 790 | } |
702 | 791 |
|
| 792 | + protected encodeMetadata(metadata: Record<string, any>) { |
| 793 | + return JSON.stringify(metadata) |
| 794 | + } |
| 795 | + |
| 796 | + toBase64(data: string) { |
| 797 | + if (typeof Buffer !== 'undefined') { |
| 798 | + return Buffer.from(data).toString('base64') |
| 799 | + } |
| 800 | + return btoa(data) |
| 801 | + } |
| 802 | + |
703 | 803 | private _getFinalPath(path: string) { |
704 | 804 | return `${this.bucketId}/${path}` |
705 | 805 | } |
|
0 commit comments