@@ -92,7 +92,7 @@ export class StorageModule {
9292 contentType = file . type || contentType ;
9393 fileSize = file . size ;
9494 fileData = file ;
95- } else if ( file instanceof Blob ) {
95+ } else if ( typeof Blob !== "undefined" && file instanceof Blob ) {
9696 contentType = file . type || contentType ;
9797 fileSize = file . size ;
9898 fileData = file ;
@@ -105,11 +105,7 @@ export class StorageModule {
105105 }
106106
107107 // step 1 — ask server for a signed URL
108- const { signedUrl, filePath } = await this . client . request < { signedUrl : string ; filePath : string } > (
109- "POST" ,
110- "/api/storage/upload-request" ,
111- { body : { filename : resolvedName , contentType, size : fileSize } }
112- ) ;
108+ const { signedUrl, filePath } = await this . requestUploadUrl ( resolvedName , contentType , fileSize ) ;
113109
114110 // step 2 — upload directly to cloud, server not involved
115111 const putResponse = await fetch ( signedUrl , {
@@ -123,12 +119,77 @@ export class StorageModule {
123119 }
124120
125121 // step 3 — tell server we're done so it can verify + update quota
122+ return this . confirmUpload ( filePath , fileSize ) ;
123+ }
124+
125+ /**
126+ * Requests a pre-signed upload URL for direct-to-cloud client uploads.
127+ * Useful when delegating uploads to a frontend client without exposing the secret key.
128+ *
129+ * @param {string } filename - The name of the file
130+ * @param {string } contentType - The MIME type of the file
131+ * @param {number } size - The size of the file in bytes
132+ * @returns {Promise<{ signedUrl: string; filePath: string; token?: string }> } The pre-signed URL and file path
133+ */
134+ public async requestUploadUrl ( filename : string , contentType : string , size : number ) : Promise < { signedUrl : string ; filePath : string ; token ?: string } > {
135+ return this . client . request < { signedUrl : string ; filePath : string ; token ?: string } > (
136+ "POST" ,
137+ "/api/storage/upload-request" ,
138+ { body : { filename, contentType, size } }
139+ ) ;
140+ }
141+
142+ /**
143+ * Confirms a direct-to-cloud upload to finalize it and update quotas.
144+ *
145+ * @param {string } filePath - The path returned by requestUploadUrl
146+ * @param {number } size - The size of the uploaded file
147+ * @returns {Promise<UploadResponse> } The upload confirmation details
148+ */
149+ public async confirmUpload ( filePath : string , size : number ) : Promise < UploadResponse > {
126150 return this . client . request < UploadResponse > (
127151 "POST" ,
128152 "/api/storage/upload-confirm" ,
129- { body : { filePath, size : fileSize } }
153+ { body : { filePath, size } }
130154 ) ;
131- }
155+ }
156+
157+ /**
158+ * Uploads a file directly to a pre-signed URL.
159+ * Useful in client-side applications (like React) where the signed URL is provided by your secure backend.
160+ *
161+ * @param {unknown } file - The file to upload (File, Blob, or Buffer)
162+ * @param {string } signedUrl - The pre-signed URL obtained from your backend
163+ * @param {string } [contentType] - The MIME type (optional, auto-detected from File/Blob)
164+ * @returns {Promise<void> } Resolves when the upload completes successfully
165+ */
166+ public static async uploadToPresignedUrl ( file : unknown , signedUrl : string , contentType ?: string ) : Promise < void > {
167+ let fileData : BodyInit ;
168+ let type = contentType || "application/octet-stream" ;
169+
170+ if ( typeof File !== "undefined" && file instanceof File ) {
171+ type = contentType || file . type || type ;
172+ fileData = file ;
173+ } else if ( typeof Blob !== "undefined" && file instanceof Blob ) {
174+ type = contentType || file . type || type ;
175+ fileData = file ;
176+ } else if ( typeof Buffer !== "undefined" && Buffer . isBuffer ( file ) ) {
177+ fileData = file as unknown as BodyInit ;
178+ } else {
179+ throw new Error ( "Unsupported file type. Pass a File, Blob, or Buffer." ) ;
180+ }
181+
182+ const putResponse = await fetch ( signedUrl , {
183+ method : "PUT" ,
184+ headers : { "Content-Type" : type } ,
185+ body : fileData ,
186+ } ) ;
187+
188+ if ( ! putResponse . ok ) {
189+ throw new Error ( `Direct upload to cloud failed: ${ putResponse . status } ${ putResponse . statusText } ` ) ;
190+ }
191+ }
192+
132193 /**
133194 * Deletes a file from storage by its path
134195 *
0 commit comments