From f5e30e1c3e31902d23a87da3985266f3657f1565 Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Tue, 22 Oct 2024 01:08:30 +0300 Subject: [PATCH 1/3] feat: use cdn v3 for uploading files --- libs/client/src/storage.ts | 104 ++++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index adb1981..9de7f73 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -75,6 +75,95 @@ async function initiateUpload( }); } +class CdnToken { + constructor( + public token: string, + public tokenType: string, + public baseUploadUrl: string, + public expiresAt: Date, + ) {} + + isExpired(): boolean { + return new Date().getTime() >= this.expiresAt.getTime(); + } +} + +type AuthTokenRequest = object; +type AuthTokenResponse = { + token: string; + token_type: string; + base_upload_url: string; + expires_at: string; +}; + +class CdnTokenManager { + private token: CdnToken; + + constructor(private config: RequiredConfig) { + this.token = new CdnToken("", "", "", new Date(0)); + } + + async getToken(): Promise { + if (this.token.isExpired()) { + this.token = await this.fetchToken(); + } + return this.token; + } + + private async fetchToken(): Promise { + const response = await dispatchRequest({ + method: "POST", + targetUrl: `${getRestApiUrl()}/storage/auth/token?storage_type=fal-cdn-v3`, + config: this.config, + }); + return new CdnToken( + response.token, + response.token_type, + response.base_upload_url, + new Date(response.expires_at), + ); + } +} + +type UploadCdnResponse = { + access_url: string; +}; + +async function uploadCdn( + file: Blob, + config: RequiredConfig, +) { + const tokenManager = new CdnTokenManager(config); + const token = await tokenManager.getToken(); + const response = await dispatchRequest({ + method: "POST", + targetUrl: `${token.baseUploadUrl}/files/upload`, + input: file, + config, + headers: { + Authorization: `${token.tokenType} ${token.token}`, + }, + }); + return response.access_url; +} + +async function uploadLegacy(file: Blob, config: RequiredConfig) { + const { fetch, responseHandler } = config; + const { upload_url: uploadUrl, file_url: url } = await initiateUpload( + file, + config, + ); + const response = await fetch(uploadUrl, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type || "application/octet-stream", + }, + }); + await responseHandler(response); + return url; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type KeyValuePair = [string, any]; @@ -87,20 +176,7 @@ export function createStorageClient({ }: StorageClientDependencies): StorageClient { const ref: StorageClient = { upload: async (file: Blob) => { - const { fetch, responseHandler } = config; - const { upload_url: uploadUrl, file_url: url } = await initiateUpload( - file, - config, - ); - const response = await fetch(uploadUrl, { - method: "PUT", - body: file, - headers: { - "Content-Type": file.type || "application/octet-stream", - }, - }); - await responseHandler(response); - return url; + return await uploadCdn(file, config); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any From 44628a754b8ab2437dc2917161690efc4ed4af5f Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Thu, 14 Nov 2024 11:02:07 -0800 Subject: [PATCH 2/3] fix(client): cdn v3 upload --- libs/client/src/storage.ts | 126 +++++++------------------------------ 1 file changed, 23 insertions(+), 103 deletions(-) diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index 9de7f73..5a76eff 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -28,100 +28,39 @@ export interface StorageClient { transformInput: (input: Record) => Promise>; } -type InitiateUploadResult = { - file_url: string; - upload_url: string; -}; - -type InitiateUploadData = { - file_name: string; - content_type: string | null; -}; - -/** - * Get the file extension from the content type. This is used to generate - * a file name if the file name is not provided. - * - * @param contentType the content type of the file. - * @returns the file extension or `bin` if the content type is not recognized. - */ -function getExtensionFromContentType(contentType: string): string { - const [_, fileType] = contentType.split("/"); - return fileType.split(/[-;]/)[0] ?? "bin"; -} - -/** - * Initiate the upload of a file to the server. This returns the URL to upload - * the file to and the URL of the file once it is uploaded. - * - * @param file the file to upload - * @returns the URL to upload the file to and the URL of the file once it is uploaded. - */ -async function initiateUpload( - file: Blob, - config: RequiredConfig, -): Promise { - const contentType = file.type || "application/octet-stream"; - const filename = - file.name || `${Date.now()}.${getExtensionFromContentType(contentType)}`; - return await dispatchRequest({ - method: "POST", - targetUrl: `${getRestApiUrl()}/storage/upload/initiate`, - input: { - content_type: contentType, - file_name: filename, - }, - config, - }); -} - -class CdnToken { - constructor( - public token: string, - public tokenType: string, - public baseUploadUrl: string, - public expiresAt: Date, - ) {} - - isExpired(): boolean { - return new Date().getTime() >= this.expiresAt.getTime(); - } -} - -type AuthTokenRequest = object; -type AuthTokenResponse = { +type CdnAuthToken = { + base_url: string; + expires_at: string; token: string; token_type: string; - base_upload_url: string; - expires_at: string; }; +function isExpired(token: CdnAuthToken): boolean { + return new Date(token.expires_at) < new Date(); +} + class CdnTokenManager { - private token: CdnToken; + private readonly config: RequiredConfig; + private token: CdnAuthToken; - constructor(private config: RequiredConfig) { - this.token = new CdnToken("", "", "", new Date(0)); + constructor(config: RequiredConfig) { + this.config = config; } - async getToken(): Promise { - if (this.token.isExpired()) { + async getToken(): Promise { + if (!this.token || isExpired(this.token)) { this.token = await this.fetchToken(); } return this.token; } - private async fetchToken(): Promise { - const response = await dispatchRequest({ + private async fetchToken(): Promise { + return dispatchRequest({ method: "POST", targetUrl: `${getRestApiUrl()}/storage/auth/token?storage_type=fal-cdn-v3`, config: this.config, + input: {}, }); - return new CdnToken( - response.token, - response.token_type, - response.base_upload_url, - new Date(response.expires_at), - ); } } @@ -129,39 +68,20 @@ type UploadCdnResponse = { access_url: string; }; -async function uploadCdn( - file: Blob, - config: RequiredConfig, -) { +async function uploadCdn(file: Blob, config: RequiredConfig) { const tokenManager = new CdnTokenManager(config); const token = await tokenManager.getToken(); - const response = await dispatchRequest({ + const response = await fetch(`${token.base_url}/files/upload`, { method: "POST", - targetUrl: `${token.baseUploadUrl}/files/upload`, - input: file, - config, - headers: { - Authorization: `${token.tokenType} ${token.token}`, - }, - }); - return response.access_url; -} - -async function uploadLegacy(file: Blob, config: RequiredConfig) { - const { fetch, responseHandler } = config; - const { upload_url: uploadUrl, file_url: url } = await initiateUpload( - file, - config, - ); - const response = await fetch(uploadUrl, { - method: "PUT", - body: file, headers: { + Authorization: `${token.token_type} ${token.token}`, "Content-Type": file.type || "application/octet-stream", }, + body: file, }); - await responseHandler(response); - return url; + const result: UploadCdnResponse = await response.json(); + console.log(result); + return result.access_url; } // eslint-disable-next-line @typescript-eslint/no-explicit-any From 84d913468afd3eaeef16d6323e681d50619aa1c5 Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Thu, 14 Nov 2024 11:50:21 -0800 Subject: [PATCH 3/3] fix: simplify token cache and fetch --- libs/client/package.json | 2 +- libs/client/src/storage.ts | 49 +++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/libs/client/package.json b/libs/client/package.json index 306350c..27a99c4 100644 --- a/libs/client/package.json +++ b/libs/client/package.json @@ -1,7 +1,7 @@ { "name": "@fal-ai/client", "description": "The fal.ai client for JavaScript and TypeScript", - "version": "1.1.1", + "version": "1.2.0-alpha.2", "license": "MIT", "repository": { "type": "git", diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index 5a76eff..015b768 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -36,41 +36,41 @@ type CdnAuthToken = { }; function isExpired(token: CdnAuthToken): boolean { - return new Date(token.expires_at) < new Date(); + return new Date().getTime() >= new Date(token.expires_at).getTime(); } -class CdnTokenManager { - private readonly config: RequiredConfig; - private token: CdnAuthToken; +interface TokenManager { + token: CdnAuthToken | null; - constructor(config: RequiredConfig) { - this.config = config; - } + fetchToken(config: RequiredConfig): Promise; - async getToken(): Promise { - if (!this.token || isExpired(this.token)) { - this.token = await this.fetchToken(); - } - return this.token; - } + getToken(config: RequiredConfig): Promise; +} + +type UploadCdnResponse = { + access_url: string; +}; - private async fetchToken(): Promise { +const tokenManager: TokenManager = { + token: null, + async getToken(config: RequiredConfig) { + if (!tokenManager.token || isExpired(tokenManager.token)) { + tokenManager.token = await tokenManager.fetchToken(config); + } + return tokenManager.token; + }, + async fetchToken(config: RequiredConfig): Promise { return dispatchRequest({ method: "POST", targetUrl: `${getRestApiUrl()}/storage/auth/token?storage_type=fal-cdn-v3`, - config: this.config, + config: config, input: {}, }); - } -} - -type UploadCdnResponse = { - access_url: string; + }, }; -async function uploadCdn(file: Blob, config: RequiredConfig) { - const tokenManager = new CdnTokenManager(config); - const token = await tokenManager.getToken(); +async function uploadFile(file: Blob, config: RequiredConfig) { + const token = await tokenManager.getToken(config); const response = await fetch(`${token.base_url}/files/upload`, { method: "POST", headers: { @@ -80,7 +80,6 @@ async function uploadCdn(file: Blob, config: RequiredConfig) { body: file, }); const result: UploadCdnResponse = await response.json(); - console.log(result); return result.access_url; } @@ -96,7 +95,7 @@ export function createStorageClient({ }: StorageClientDependencies): StorageClient { const ref: StorageClient = { upload: async (file: Blob) => { - return await uploadCdn(file, config); + return await uploadFile(file, config); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any