@uppy/transloadit
Version:
The Transloadit plugin can be used to upload files to Transloadit for all kinds of processing, such as transcoding video, resizing images, zipping/unzipping, and more
264 lines (230 loc) • 6.71 kB
text/typescript
import type {
Body,
Meta,
RateLimitedQueue,
UppyFile,
WrapPromiseFunctionType,
} from '@uppy/utils'
import { fetchWithNetworkError } from '@uppy/utils'
import {
type AssemblyResponse,
getAssemblyUrlSsl,
type OptionsWithRestructuredFields,
} from './index.js'
const ASSEMBLIES_ENDPOINT = '/assemblies'
type Opts = {
client?: string
service: string
rateLimitedQueue: RateLimitedQueue
errorReporting: boolean
}
export class AssemblyError extends Error {
details: string | undefined
assembly: AssemblyResponse
constructor(
message: string,
details: string | undefined,
assembly: AssemblyResponse,
) {
super(message)
this.details = details
this.assembly = assembly
}
}
/**
* A Barebones HTTP API client for Transloadit.
*/
export default class Client<M extends Meta, B extends Body> {
#headers: Record<string, string> = {}
#fetchWithNetworkError: WrapPromiseFunctionType<typeof fetchWithNetworkError>
opts: Opts
constructor(opts: Opts) {
this.opts = opts
if (this.opts.client != null) {
this.#headers['Transloadit-Client'] = this.opts.client
}
this.#fetchWithNetworkError =
this.opts.rateLimitedQueue.wrapPromiseFunction(fetchWithNetworkError)
}
async #fetchJSON(
...args: Parameters<typeof fetchWithNetworkError>
): Promise<AssemblyResponse> {
const response = await this.#fetchWithNetworkError(...args)
if (response.status === 429) {
this.opts.rateLimitedQueue.rateLimit(2_000)
return this.#fetchJSON(...args)
}
if (!response.ok) {
const serverError = new Error(response.statusText)
// @ts-expect-error statusCode is not a standard property
serverError.statusCode = response.status
if (!`${args[0]}`.endsWith(ASSEMBLIES_ENDPOINT))
return Promise.reject(serverError)
// Failed assembly requests should return a more detailed error in JSON.
return response.json().then(
(assembly: AssemblyResponse) => {
if (!assembly.error) throw serverError
const error = new AssemblyError(
assembly.error,
assembly.message,
assembly,
)
if (assembly.assembly_id) {
error.details += ` Assembly ID: ${assembly.assembly_id}`
}
throw error
},
(err) => {
err.cause = serverError
throw err
},
)
}
return response.json()
}
async createAssembly({
params,
fields,
signature,
expectedFiles,
}: OptionsWithRestructuredFields & {
expectedFiles: number
}): Promise<AssemblyResponse> {
const data = new FormData()
data.append(
'params',
typeof params === 'string' ? params : JSON.stringify(params),
)
if (signature) {
data.append('signature', signature)
}
Object.keys(fields).forEach((key) => {
data.append(key, String(fields[key]))
})
data.append('num_expected_upload_files', String(expectedFiles))
const url = new URL(ASSEMBLIES_ENDPOINT, `${this.opts.service}`).href
return this.#fetchJSON(url, {
method: 'POST',
headers: this.#headers,
body: data,
}).catch((err) => this.#reportError(err, { url, type: 'API_ERROR' }))
}
/**
* Reserve resources for a file in an Assembly. Then addFile can be used later.
*/
async reserveFile(
assembly: AssemblyResponse,
file: UppyFile<M, B>,
): Promise<AssemblyResponse> {
const size = encodeURIComponent(file.size!)
const assemblyUrl = getAssemblyUrlSsl(assembly)
const url = `${assemblyUrl}/reserve_file?size=${size}`
return this.#fetchJSON(url, {
method: 'POST',
headers: this.#headers,
}).catch((err) =>
this.#reportError(err, { assembly, file, url, type: 'API_ERROR' }),
)
}
/**
* Import a remote file to an Assembly.
*/
async addFile(
assembly: AssemblyResponse,
file: UppyFile<M, B>,
): Promise<AssemblyResponse> {
if (!file.uploadURL) {
return Promise.reject(new Error('File does not have an `uploadURL`.'))
}
const size = encodeURIComponent(file.size!)
const uploadUrl = encodeURIComponent(file.uploadURL)
const filename = encodeURIComponent(file.name ?? 'Unnamed')
const fieldname = 'file'
const qs = `size=${size}&filename=${filename}&fieldname=${fieldname}&s3Url=${uploadUrl}`
const assemblyUrl = getAssemblyUrlSsl(assembly)
const url = `${assemblyUrl}/add_file?${qs}`
return this.#fetchJSON(url, {
method: 'POST',
headers: this.#headers,
}).catch((err) =>
this.#reportError(err, { assembly, file, url, type: 'API_ERROR' }),
)
}
/**
* Cancel a running Assembly.
*/
async cancelAssembly(assembly: AssemblyResponse): Promise<void> {
const url = getAssemblyUrlSsl(assembly)
await this.#fetchWithNetworkError(url, {
method: 'DELETE',
headers: this.#headers,
}).catch((err) => this.#reportError(err, { url, type: 'API_ERROR' }))
}
/**
* Get the current status for an assembly.
*/
async getAssemblyStatus(url: string): Promise<AssemblyResponse> {
return this.#fetchJSON(url, { headers: this.#headers }).catch((err) =>
this.#reportError(err, { url, type: 'STATUS_ERROR' }),
)
}
async submitError(
err: { message?: string; details?: string },
{
endpoint,
instance,
assembly,
}: {
endpoint?: string | URL
instance?: string
assembly?: string
} = {},
): Promise<AssemblyResponse> {
const message = err.details
? `${err.message} (${err.details})`
: err.message
return this.#fetchJSON('https://transloaditstatus.com/client_error', {
method: 'POST',
body: JSON.stringify({
endpoint,
instance,
assembly_id: assembly,
agent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
client: this.opts.client,
error: message,
}),
})
}
#reportError = (
err: AssemblyError,
params: {
assembly?: AssemblyResponse
url?: URL | string
file?: UppyFile<M, B>
type: string
},
) => {
if (this.opts.errorReporting === false) {
throw err
}
const opts: {
type: string
assembly?: string
instance?: string
endpoint?: URL | string
} = {
type: params.type,
}
if (params.assembly) {
opts.assembly = params.assembly.assembly_id
opts.instance = params.assembly.instance
}
if (params.url) {
opts.endpoint = params.url
}
this.submitError(err, opts).catch(() => {
// not much we can do then is there
})
throw err
}
}