import { DEFAULT_HEADERS } from '../lib/constants' import { StorageError } from '../lib/common/errors' import { Fetch, get, post, put, remove } from '../lib/common/fetch' import { encodeStoragePath } from '../lib/common/helpers' import BaseApiClient from '../lib/common/BaseApiClient' import { Bucket, BucketType, FetchParameters, ListBucketOptions, PurgeCacheOptions, } from '../lib/types' import { StorageClientOptions } from '../StorageClient' export default class StorageBucketApi extends BaseApiClient { constructor( url: string, headers: { [key: string]: string } = {}, fetch?: Fetch, opts?: StorageClientOptions ) { const baseUrl = new URL(url) // if legacy uri is used, replace with new storage host (disables request buffering to allow > 50GB uploads) // "project-ref.supabase.co" becomes "project-ref.storage.supabase.co" if (opts?.useNewHostname) { const isSupabaseHost = /supabase\.(co|in|red)$/.test(baseUrl.hostname) if (isSupabaseHost && !baseUrl.hostname.includes('storage.supabase.')) { baseUrl.hostname = baseUrl.hostname.replace('supabase.', 'storage.supabase.') } } const finalUrl = baseUrl.href.replace(/\/$/, '') const finalHeaders = { ...DEFAULT_HEADERS, ...headers } super(finalUrl, finalHeaders, fetch, 'storage') } /** * Retrieves the details of all Storage buckets within an existing project. * * @category Storage * @subcategory File Buckets * @param options Query parameters for listing buckets * @param options.limit Maximum number of buckets to return * @param options.offset Number of buckets to skip * @param options.sortColumn Column to sort by ('id', 'name', 'created_at', 'updated_at') * @param options.sortOrder Sort order ('asc' or 'desc') * @param options.search Search term to filter bucket names * @returns Promise with response containing array of buckets or error * * @example List buckets * ```js * const { data, error } = await supabase * .storage * .listBuckets() * ``` * * @example List buckets with options * ```js * const { data, error } = await supabase * .storage * .listBuckets({ * limit: 10, * offset: 0, * sortColumn: 'created_at', * sortOrder: 'desc', * search: 'prod' * }) * ``` * * @remarks * - RLS policy permissions required: * - `buckets` table permissions: `select` * - `objects` table permissions: none * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works */ async listBuckets(options?: ListBucketOptions): Promise< | { data: Bucket[] error: null } | { data: null error: StorageError } > { return this.handleOperation(async () => { const queryString = this.listBucketOptionsToQueryString(options) return await get(this.fetch, `${this.url}/bucket${queryString}`, { headers: this.headers, }) }) } /** * Retrieves the details of an existing Storage bucket. * * @category Storage * @subcategory File Buckets * @param id The unique identifier of the bucket you would like to retrieve. * @returns Promise with response containing bucket details or error * * @example Get bucket * ```js * const { data, error } = await supabase * .storage * .getBucket('avatars') * ``` * * Response: * ```json * { * "data": { * "id": "avatars", * "name": "avatars", * "owner": "", * "public": false, * "file_size_limit": 1024, * "allowed_mime_types": [ * "image/png" * ], * "created_at": "2024-05-22T22:26:05.100Z", * "updated_at": "2024-05-22T22:26:05.100Z" * }, * "error": null * } * ``` * * @remarks * - RLS policy permissions required: * - `buckets` table permissions: `select` * - `objects` table permissions: none * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works */ async getBucket(id: string): Promise< | { data: Bucket error: null } | { data: null error: StorageError } > { return this.handleOperation(async () => { return await get(this.fetch, `${this.url}/bucket/${id}`, { headers: this.headers }) }) } /** * Creates a new Storage bucket * * @category Storage * @subcategory File Buckets * @param id A unique identifier for the bucket you are creating. * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. By default, buckets are private. * @param options.fileSizeLimit specifies the max file size in bytes that can be uploaded to this bucket. * The global file size limit takes precedence over this value. * The default value is null, which doesn't set a per bucket file size limit. * @param options.allowedMimeTypes specifies the allowed mime types that this bucket can accept during upload. * The default value is null, which allows files with all mime types to be uploaded. * Each mime type specified can be a wildcard, e.g. image/*, or a specific mime type, e.g. image/png. * @param options.type (private-beta) specifies the bucket type. see `BucketType` for more details. * - default bucket type is `STANDARD` * @returns Promise with response containing newly created bucket name or error * * @example Create bucket * ```js * const { data, error } = await supabase * .storage * .createBucket('avatars', { * public: false, * allowedMimeTypes: ['image/png'], * fileSizeLimit: 1024 * }) * ``` * * Response: * ```json * { * "data": { * "name": "avatars" * }, * "error": null * } * ``` * * @remarks * - RLS policy permissions required: * - `buckets` table permissions: `insert` * - `objects` table permissions: none * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works */ async createBucket( id: string, options: { public: boolean fileSizeLimit?: number | string | null allowedMimeTypes?: string[] | null type?: BucketType } = { public: false, } ): Promise< | { data: Pick error: null } | { data: null error: StorageError } > { return this.handleOperation(async () => { return await post( this.fetch, `${this.url}/bucket`, { id, name: id, type: options.type, public: options.public, file_size_limit: options.fileSizeLimit, allowed_mime_types: options.allowedMimeTypes, }, { headers: this.headers } ) }) } /** * Updates a Storage bucket * * @category Storage * @subcategory File Buckets * @param id A unique identifier for the bucket you are updating. * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. * @param options.fileSizeLimit specifies the max file size in bytes that can be uploaded to this bucket. * The global file size limit takes precedence over this value. * The default value is null, which doesn't set a per bucket file size limit. * @param options.allowedMimeTypes specifies the allowed mime types that this bucket can accept during upload. * The default value is null, which allows files with all mime types to be uploaded. * Each mime type specified can be a wildcard, e.g. image/*, or a specific mime type, e.g. image/png. * @returns Promise with response containing success message or error * * @example Update bucket * ```js * const { data, error } = await supabase * .storage * .updateBucket('avatars', { * public: false, * allowedMimeTypes: ['image/png'], * fileSizeLimit: 1024 * }) * ``` * * Response: * ```json * { * "data": { * "message": "Successfully updated" * }, * "error": null * } * ``` * * @remarks * - RLS policy permissions required: * - `buckets` table permissions: `select` and `update` * - `objects` table permissions: none * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works */ async updateBucket( id: string, options: { public: boolean fileSizeLimit?: number | string | null allowedMimeTypes?: string[] | null } ): Promise< | { data: { message: string } error: null } | { data: null error: StorageError } > { return this.handleOperation(async () => { return await put( this.fetch, `${this.url}/bucket/${id}`, { id, name: id, public: options.public, file_size_limit: options.fileSizeLimit, allowed_mime_types: options.allowedMimeTypes, }, { headers: this.headers } ) }) } /** * Removes all objects inside a single bucket. * * @category Storage * @subcategory File Buckets * @param id The unique identifier of the bucket you would like to empty. * @returns Promise with success message or error * * @example Empty bucket * ```js * const { data, error } = await supabase * .storage * .emptyBucket('avatars') * ``` * * Response: * ```json * { * "data": { * "message": "Successfully emptied" * }, * "error": null * } * ``` * * @remarks * - RLS policy permissions required: * - `buckets` table permissions: `select` * - `objects` table permissions: `select` and `delete` * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works */ async emptyBucket(id: string): Promise< | { data: { message: string } error: null } | { data: null error: StorageError } > { return this.handleOperation(async () => { return await post(this.fetch, `${this.url}/bucket/${id}/empty`, {}, { headers: this.headers }) }) } /** * Deletes an existing bucket. A bucket can't be deleted with existing objects inside it. * You must first `empty()` the bucket. * * @category Storage * @subcategory File Buckets * @param id The unique identifier of the bucket you would like to delete. * @returns Promise with success message or error * * @example Delete bucket * ```js * const { data, error } = await supabase * .storage * .deleteBucket('avatars') * ``` * * Response: * ```json * { * "data": { * "message": "Successfully deleted" * }, * "error": null * } * ``` * * @remarks * - RLS policy permissions required: * - `buckets` table permissions: `select` and `delete` * - `objects` table permissions: none * - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works */ async deleteBucket(id: string): Promise< | { data: { message: string } error: null } | { data: null error: StorageError } > { return this.handleOperation(async () => { return await remove(this.fetch, `${this.url}/bucket/${id}`, {}, { headers: this.headers }) }) } /** * Purges the CDN cache for an entire bucket. * * Maps to `DELETE /cdn/{bucket}` on the Storage API. The server * issues a CDN invalidation for the bucket and returns `{ message: 'success' }`. * * **Requires the `service_role` key.** The underlying endpoint enforces * `service_role` JWT — calls made with the anon key or a user JWT will be * rejected by the server. * * **Hosted CDN feature.** On self-hosted Supabase, the Storage service must * have `CDN_PURGE_ENDPOINT_URL` configured and the `purgeCache` tenant * feature enabled, otherwise the server returns an error. * * @category Storage * @subcategory File Buckets * @param id The unique identifier of the bucket you would like to purge from cache. * @param options Optional purge cache options. * @param options.transformations If true, purges only transformations (resized/formatted variants), leaving original cached files intact. * @param parameters Optional fetch parameters such as an `AbortController` signal. * @returns Promise with `{ data: { message }, error: null }` on success or `{ data: null, error }` on failure. * * @example Purge cache for an entire bucket * ```js * const { data, error } = await supabase * .storage * .purgeBucketCache('avatars') * ``` * * @example Purge only transformations for an entire bucket * ```js * const { data, error } = await supabase * .storage * .purgeBucketCache('avatars', { transformations: true }) * ``` */ async purgeBucketCache( id: string, options?: PurgeCacheOptions, parameters?: FetchParameters ): Promise< | { data: { message: string } error: null } | { data: null error: StorageError } > { return this.handleOperation(async () => { const query = new URLSearchParams() if (options?.transformations) { query.set('transformations', 'true') } const queryString = query.toString() return await remove( this.fetch, `${this.url}/cdn/${encodeStoragePath(id)}${queryString ? `?${queryString}` : ''}`, {}, { headers: this.headers }, parameters ) }) } private listBucketOptionsToQueryString(options?: ListBucketOptions): string { const params: Record = {} if (options) { if ('limit' in options) { params.limit = String(options.limit) } if ('offset' in options) { params.offset = String(options.offset) } if (options.search) { params.search = options.search } if (options.sortColumn) { params.sortColumn = options.sortColumn } if (options.sortOrder) { params.sortOrder = options.sortOrder } } return Object.keys(params).length > 0 ? '?' + new URLSearchParams(params).toString() : '' } }