Skip to content

Commit 64f39b7

Browse files
Merge pull request #401 from geturbackend/feature/sdk-upload-presigned
feat(sdk): Add granular presigned URL upload methods to StorageModule
2 parents 282e8d2 + 5e2bdd8 commit 64f39b7

2 files changed

Lines changed: 121 additions & 8 deletions

File tree

‎mintlify/docs/sdk/storage.mdx‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,58 @@ console.log(provider); // 'internal'
5252

5353
---
5454

55+
## Presigned URLs (Direct Client Uploads)
56+
57+
To allow frontend users to upload files directly without passing them through your server, use the presigned URL flow.
58+
59+
### 1. requestUploadUrl (Server)
60+
61+
Generates a temporary upload URL. Requires your **secret key**.
62+
63+
```typescript
64+
requestUploadUrl(filename: string, contentType: string, size: number): Promise<{ signedUrl: string, filePath: string }>
65+
```
66+
67+
### 2. uploadToPresignedUrl (Client)
68+
69+
A static helper to push the file to the generated URL from the frontend.
70+
71+
```typescript
72+
import { StorageModule } from '@urbackend/sdk';
73+
74+
StorageModule.uploadToPresignedUrl(file: File | Blob | Buffer, signedUrl: string): Promise<void>
75+
```
76+
77+
### 3. confirmUpload (Server)
78+
79+
Confirms the upload is complete and registers it in your storage quota.
80+
81+
```typescript
82+
confirmUpload(filePath: string, size: number): Promise<UploadResponse>
83+
```
84+
85+
**Full Example (React + Node.js):**
86+
87+
```typescript
88+
// 1. Server: Generate link
89+
app.post('/api/upload-link', async (req, res) => {
90+
const link = await urbackend.storage.requestUploadUrl('cat.png', 'image/png', 500000);
91+
res.json(link);
92+
});
93+
94+
// 2. Client (React): Upload directly
95+
const { signedUrl, filePath } = await fetch('/api/upload-link').then(r => r.json());
96+
await StorageModule.uploadToPresignedUrl(file, signedUrl);
97+
98+
// 3. Server: Confirm
99+
app.post('/api/upload-confirm', async (req, res) => {
100+
const result = await urbackend.storage.confirmUpload(req.body.filePath, 500000);
101+
res.json(result);
102+
});
103+
```
104+
105+
---
106+
55107
## deleteFile
56108

57109
Delete a previously uploaded file by its storage path.

‎sdks/urbackend-sdk/src/modules/storage.ts‎

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)