Venta.De.Boletos.De.Un.Conc.../node_modules/@supabase/storage-js/src/packages/StorageFileApi.ts

835 lines
22 KiB
TypeScript

import { isStorageError, StorageError, StorageUnknownError } from '../lib/errors'
import { Fetch, get, head, post, remove } from '../lib/fetch'
import { recursiveToCamel, resolveFetch } from '../lib/helpers'
import {
FileObject,
FileOptions,
SearchOptions,
FetchParameters,
TransformOptions,
DestinationOptions,
FileObjectV2,
Camelize,
} from '../lib/types'
const DEFAULT_SEARCH_OPTIONS = {
limit: 100,
offset: 0,
sortBy: {
column: 'name',
order: 'asc',
},
}
const DEFAULT_FILE_OPTIONS: FileOptions = {
cacheControl: '3600',
contentType: 'text/plain;charset=UTF-8',
upsert: false,
}
type FileBody =
| ArrayBuffer
| ArrayBufferView
| Blob
| Buffer
| File
| FormData
| NodeJS.ReadableStream
| ReadableStream<Uint8Array>
| URLSearchParams
| string
export default class StorageFileApi {
protected url: string
protected headers: { [key: string]: string }
protected bucketId?: string
protected fetch: Fetch
constructor(
url: string,
headers: { [key: string]: string } = {},
bucketId?: string,
fetch?: Fetch
) {
this.url = url
this.headers = headers
this.bucketId = bucketId
this.fetch = resolveFetch(fetch)
}
/**
* Uploads a file to an existing bucket or replaces an existing file at the specified path with a new one.
*
* @param method HTTP method.
* @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
* @param fileBody The body of the file to be stored in the bucket.
*/
private async uploadOrUpdate(
method: 'POST' | 'PUT',
path: string,
fileBody: FileBody,
fileOptions?: FileOptions
): Promise<
| {
data: { id: string; path: string; fullPath: string }
error: null
}
| {
data: null
error: StorageError
}
> {
try {
let body
const options = { ...DEFAULT_FILE_OPTIONS, ...fileOptions }
let headers: Record<string, string> = {
...this.headers,
...(method === 'POST' && { 'x-upsert': String(options.upsert as boolean) }),
}
const metadata = options.metadata
if (typeof Blob !== 'undefined' && fileBody instanceof Blob) {
body = new FormData()
body.append('cacheControl', options.cacheControl as string)
if (metadata) {
body.append('metadata', this.encodeMetadata(metadata))
}
body.append('', fileBody)
} else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) {
body = fileBody
body.append('cacheControl', options.cacheControl as string)
if (metadata) {
body.append('metadata', this.encodeMetadata(metadata))
}
} else {
body = fileBody
headers['cache-control'] = `max-age=${options.cacheControl}`
headers['content-type'] = options.contentType as string
if (metadata) {
headers['x-metadata'] = this.toBase64(this.encodeMetadata(metadata))
}
}
if (fileOptions?.headers) {
headers = { ...headers, ...fileOptions.headers }
}
const cleanPath = this._removeEmptyFolders(path)
const _path = this._getFinalPath(cleanPath)
const res = await this.fetch(`${this.url}/object/${_path}`, {
method,
body: body as BodyInit,
headers,
...(options?.duplex ? { duplex: options.duplex } : {}),
})
const data = await res.json()
if (res.ok) {
return {
data: { path: cleanPath, id: data.Id, fullPath: data.Key },
error: null,
}
} else {
const error = data
return { data: null, error }
}
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Uploads a file to an existing bucket.
*
* @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.
* @param fileBody The body of the file to be stored in the bucket.
*/
async upload(
path: string,
fileBody: FileBody,
fileOptions?: FileOptions
): Promise<
| {
data: { id: string; path: string; fullPath: string }
error: null
}
| {
data: null
error: StorageError
}
> {
return this.uploadOrUpdate('POST', path, fileBody, fileOptions)
}
/**
* Upload a file with a token generated from `createSignedUploadUrl`.
* @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.
* @param token The token generated from `createSignedUploadUrl`
* @param fileBody The body of the file to be stored in the bucket.
*/
async uploadToSignedUrl(
path: string,
token: string,
fileBody: FileBody,
fileOptions?: FileOptions
) {
const cleanPath = this._removeEmptyFolders(path)
const _path = this._getFinalPath(cleanPath)
const url = new URL(this.url + `/object/upload/sign/${_path}`)
url.searchParams.set('token', token)
try {
let body
const options = { upsert: DEFAULT_FILE_OPTIONS.upsert, ...fileOptions }
const headers: Record<string, string> = {
...this.headers,
...{ 'x-upsert': String(options.upsert as boolean) },
}
if (typeof Blob !== 'undefined' && fileBody instanceof Blob) {
body = new FormData()
body.append('cacheControl', options.cacheControl as string)
body.append('', fileBody)
} else if (typeof FormData !== 'undefined' && fileBody instanceof FormData) {
body = fileBody
body.append('cacheControl', options.cacheControl as string)
} else {
body = fileBody
headers['cache-control'] = `max-age=${options.cacheControl}`
headers['content-type'] = options.contentType as string
}
const res = await this.fetch(url.toString(), {
method: 'PUT',
body: body as BodyInit,
headers,
})
const data = await res.json()
if (res.ok) {
return {
data: { path: cleanPath, fullPath: data.Key },
error: null,
}
} else {
const error = data
return { data: null, error }
}
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Creates a signed upload URL.
* Signed upload URLs can be used to upload files to the bucket without further authentication.
* They are valid for 2 hours.
* @param path The file path, including the current file name. For example `folder/image.png`.
* @param options.upsert If set to true, allows the file to be overwritten if it already exists.
*/
async createSignedUploadUrl(
path: string,
options?: { upsert: boolean }
): Promise<
| {
data: { signedUrl: string; token: string; path: string }
error: null
}
| {
data: null
error: StorageError
}
> {
try {
let _path = this._getFinalPath(path)
const headers = { ...this.headers }
if (options?.upsert) {
headers['x-upsert'] = 'true'
}
const data = await post(
this.fetch,
`${this.url}/object/upload/sign/${_path}`,
{},
{ headers }
)
const url = new URL(this.url + data.url)
const token = url.searchParams.get('token')
if (!token) {
throw new StorageError('No token returned by API')
}
return { data: { signedUrl: url.toString(), path, token }, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Replaces an existing file at the specified path with a new one.
*
* @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to update.
* @param fileBody The body of the file to be stored in the bucket.
*/
async update(
path: string,
fileBody:
| ArrayBuffer
| ArrayBufferView
| Blob
| Buffer
| File
| FormData
| NodeJS.ReadableStream
| ReadableStream<Uint8Array>
| URLSearchParams
| string,
fileOptions?: FileOptions
): Promise<
| {
data: { id: string; path: string; fullPath: string }
error: null
}
| {
data: null
error: StorageError
}
> {
return this.uploadOrUpdate('PUT', path, fileBody, fileOptions)
}
/**
* Moves an existing file to a new path in the same bucket.
*
* @param fromPath The original file path, including the current file name. For example `folder/image.png`.
* @param toPath The new file path, including the new file name. For example `folder/image-new.png`.
* @param options The destination options.
*/
async move(
fromPath: string,
toPath: string,
options?: DestinationOptions
): Promise<
| {
data: { message: string }
error: null
}
| {
data: null
error: StorageError
}
> {
try {
const data = await post(
this.fetch,
`${this.url}/object/move`,
{
bucketId: this.bucketId,
sourceKey: fromPath,
destinationKey: toPath,
destinationBucket: options?.destinationBucket,
},
{ headers: this.headers }
)
return { data, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Copies an existing file to a new path in the same bucket.
*
* @param fromPath The original file path, including the current file name. For example `folder/image.png`.
* @param toPath The new file path, including the new file name. For example `folder/image-copy.png`.
* @param options The destination options.
*/
async copy(
fromPath: string,
toPath: string,
options?: DestinationOptions
): Promise<
| {
data: { path: string }
error: null
}
| {
data: null
error: StorageError
}
> {
try {
const data = await post(
this.fetch,
`${this.url}/object/copy`,
{
bucketId: this.bucketId,
sourceKey: fromPath,
destinationKey: toPath,
destinationBucket: options?.destinationBucket,
},
{ headers: this.headers }
)
return { data: { path: data.Key }, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.
*
* @param path The file path, including the current file name. For example `folder/image.png`.
* @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
* @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.
* @param options.transform Transform the asset before serving it to the client.
*/
async createSignedUrl(
path: string,
expiresIn: number,
options?: { download?: string | boolean; transform?: TransformOptions }
): Promise<
| {
data: { signedUrl: string }
error: null
}
| {
data: null
error: StorageError
}
> {
try {
let _path = this._getFinalPath(path)
let data = await post(
this.fetch,
`${this.url}/object/sign/${_path}`,
{ expiresIn, ...(options?.transform ? { transform: options.transform } : {}) },
{ headers: this.headers }
)
const downloadQueryParam = options?.download
? `&download=${options.download === true ? '' : options.download}`
: ''
const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`)
data = { signedUrl }
return { data, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time.
*
* @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`.
* @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
* @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.
*/
async createSignedUrls(
paths: string[],
expiresIn: number,
options?: { download: string | boolean }
): Promise<
| {
data: { error: string | null; path: string | null; signedUrl: string }[]
error: null
}
| {
data: null
error: StorageError
}
> {
try {
const data = await post(
this.fetch,
`${this.url}/object/sign/${this.bucketId}`,
{ expiresIn, paths },
{ headers: this.headers }
)
const downloadQueryParam = options?.download
? `&download=${options.download === true ? '' : options.download}`
: ''
return {
data: data.map((datum: { signedURL: string }) => ({
...datum,
signedUrl: datum.signedURL
? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`)
: null,
})),
error: null,
}
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Downloads a file from a private bucket. For public buckets, make a request to the URL returned from `getPublicUrl` instead.
*
* @param path The full path and file name of the file to be downloaded. For example `folder/image.png`.
* @param options.transform Transform the asset before serving it to the client.
*/
async download(
path: string,
options?: { transform?: TransformOptions }
): Promise<
| {
data: Blob
error: null
}
| {
data: null
error: StorageError
}
> {
const wantsTransformation = typeof options?.transform !== 'undefined'
const renderPath = wantsTransformation ? 'render/image/authenticated' : 'object'
const transformationQuery = this.transformOptsToQueryString(options?.transform || {})
const queryString = transformationQuery ? `?${transformationQuery}` : ''
try {
const _path = this._getFinalPath(path)
const res = await get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString}`, {
headers: this.headers,
noResolveJson: true,
})
const data = await res.blob()
return { data, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Retrieves the details of an existing file.
* @param path
*/
async info(
path: string
): Promise<
| {
data: Camelize<FileObjectV2>
error: null
}
| {
data: null
error: StorageError
}
> {
const _path = this._getFinalPath(path)
try {
const data = await get(this.fetch, `${this.url}/object/info/${_path}`, {
headers: this.headers,
})
return { data: recursiveToCamel(data) as Camelize<FileObjectV2>, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Checks the existence of a file.
* @param path
*/
async exists(
path: string
): Promise<
| {
data: boolean
error: null
}
| {
data: boolean
error: StorageError
}
> {
const _path = this._getFinalPath(path)
try {
await head(this.fetch, `${this.url}/object/${_path}`, {
headers: this.headers,
})
return { data: true, error: null }
} catch (error) {
if (isStorageError(error) && error instanceof StorageUnknownError) {
const originalError = (error.originalError as unknown) as { status: number }
if ([400, 404].includes(originalError?.status)) {
return { data: false, error }
}
}
throw error
}
}
/**
* 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.
* 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.
*
* @param path The path and name of the file to generate the public URL for. For example `folder/image.png`.
* @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.
* @param options.transform Transform the asset before serving it to the client.
*/
getPublicUrl(
path: string,
options?: { download?: string | boolean; transform?: TransformOptions }
): { data: { publicUrl: string } } {
const _path = this._getFinalPath(path)
const _queryString = []
const downloadQueryParam = options?.download
? `download=${options.download === true ? '' : options.download}`
: ''
if (downloadQueryParam !== '') {
_queryString.push(downloadQueryParam)
}
const wantsTransformation = typeof options?.transform !== 'undefined'
const renderPath = wantsTransformation ? 'render/image' : 'object'
const transformationQuery = this.transformOptsToQueryString(options?.transform || {})
if (transformationQuery !== '') {
_queryString.push(transformationQuery)
}
let queryString = _queryString.join('&')
if (queryString !== '') {
queryString = `?${queryString}`
}
return {
data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) },
}
}
/**
* Deletes files within the same bucket
*
* @param paths An array of files to delete, including the path and file name. For example [`'folder/image.png'`].
*/
async remove(
paths: string[]
): Promise<
| {
data: FileObject[]
error: null
}
| {
data: null
error: StorageError
}
> {
try {
const data = await remove(
this.fetch,
`${this.url}/object/${this.bucketId}`,
{ prefixes: paths },
{ headers: this.headers }
)
return { data, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Get file metadata
* @param id the file id to retrieve metadata
*/
// async getMetadata(
// id: string
// ): Promise<
// | {
// data: Metadata
// error: null
// }
// | {
// data: null
// error: StorageError
// }
// > {
// try {
// const data = await get(this.fetch, `${this.url}/metadata/${id}`, { headers: this.headers })
// return { data, error: null }
// } catch (error) {
// if (isStorageError(error)) {
// return { data: null, error }
// }
// throw error
// }
// }
/**
* Update file metadata
* @param id the file id to update metadata
* @param meta the new file metadata
*/
// async updateMetadata(
// id: string,
// meta: Metadata
// ): Promise<
// | {
// data: Metadata
// error: null
// }
// | {
// data: null
// error: StorageError
// }
// > {
// try {
// const data = await post(
// this.fetch,
// `${this.url}/metadata/${id}`,
// { ...meta },
// { headers: this.headers }
// )
// return { data, error: null }
// } catch (error) {
// if (isStorageError(error)) {
// return { data: null, error }
// }
// throw error
// }
// }
/**
* Lists all the files within a bucket.
* @param path The folder path.
*/
async list(
path?: string,
options?: SearchOptions,
parameters?: FetchParameters
): Promise<
| {
data: FileObject[]
error: null
}
| {
data: null
error: StorageError
}
> {
try {
const body = { ...DEFAULT_SEARCH_OPTIONS, ...options, prefix: path || '' }
const data = await post(
this.fetch,
`${this.url}/object/list/${this.bucketId}`,
body,
{ headers: this.headers },
parameters
)
return { data, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
protected encodeMetadata(metadata: Record<string, any>) {
return JSON.stringify(metadata)
}
toBase64(data: string) {
if (typeof Buffer !== 'undefined') {
return Buffer.from(data).toString('base64')
}
return btoa(data)
}
private _getFinalPath(path: string) {
return `${this.bucketId}/${path}`
}
private _removeEmptyFolders(path: string) {
return path.replace(/^\/|\/$/g, '').replace(/\/+/g, '/')
}
private transformOptsToQueryString(transform: TransformOptions) {
const params = []
if (transform.width) {
params.push(`width=${transform.width}`)
}
if (transform.height) {
params.push(`height=${transform.height}`)
}
if (transform.resize) {
params.push(`resize=${transform.resize}`)
}
if (transform.format) {
params.push(`format=${transform.format}`)
}
if (transform.quality) {
params.push(`quality=${transform.quality}`)
}
return params.join('&')
}
}