|
3 | 3 | The Upload plugin exposes an API for both backend-only uploads and direct browser uploads using presigned URLs. You can: |
4 | 4 |
|
5 | 5 | - Upload from the backend (Node.js `Buffer`) and either create a new record or update an existing one. |
6 | | -- Generate a presigned upload URL on the backend, send it to the frontend, and upload directly from the browser to your storage provider using `multipart/form-data`. |
| 6 | +- Generate a presigned upload URL on the backend, send it to the frontend, and upload directly from the browser to your storage provider using an HTTP `PUT` request (raw file as body, plus any `uploadExtraParams` as headers). |
7 | 7 | - After the file is uploaded, it is considered **temporary** and can be auto-deleted until you commit it. |
8 | 8 | - You can commit by calling `commitUrlToNewRecord` or `commitUrlToUpdateExistingRecord` from your custom API. |
9 | 9 | - Or (only for custom create/edit components on the same resource) you can commit by writing `filePath` into the `pathColumnName` field using `update:recordFieldValue`. |
10 | 10 |
|
| 11 | +> Note: for presigned browser uploads the upload is performed via an HTTP `PUT` request with the raw file as the request body. |
| 12 | +> `uploadExtraParams` (if returned) must be sent as **HTTP headers** during that upload. |
| 13 | +
|
11 | 14 | ## Uploading from backend (Buffer API) |
12 | 15 |
|
13 | 16 | This API is useful when files are produced entirely on the backend (for example, reports generated by a background job or files received from a webhook), so you don't need to send them through the browser. |
@@ -83,7 +86,7 @@ For files that originate in the browser (drag & drop, file input, custom SPA, et |
83 | 86 |
|
84 | 87 | 1. Your custom or admin frontend sends a request to your backend. |
85 | 88 | 2. The backend calls `plugin.getUploadUrl(...)` and returns `{ uploadUrl, filePath, uploadExtraParams, pathColumnName }` to the frontend. |
86 | | -3. The frontend uploads the file directly to `uploadUrl` using `fetch` + `FormData` (including any `uploadExtraParams`). |
| 89 | +3. The frontend uploads the file directly to `uploadUrl` using `XMLHttpRequest` with method `PUT` (sending the file as the request body and attaching `uploadExtraParams` as request headers). This allows tracking upload progress. |
87 | 90 | 4. After the upload completes, you **commit** the uploaded file to a record using either: |
88 | 91 | - `plugin.commitUrlToUpdateExistingRecord` (update existing record), or |
89 | 92 | - `plugin.commitUrlToNewRecord` (create new record), or |
@@ -138,21 +141,37 @@ const { uploadUrl, filePath, uploadExtraParams } = await fetch('/api/uploads/get |
138 | 141 | }).then(r => r.json()); |
139 | 142 |
|
140 | 143 | // 2) Direct upload from browser to storage |
141 | | -const formData = new FormData(); |
142 | | -if (uploadExtraParams) { |
143 | | - Object.entries(uploadExtraParams).forEach(([key, value]) => { |
144 | | - formData.append(key, value as string); |
| 144 | +await new Promise<void>((resolve, reject) => { |
| 145 | + const xhr = new XMLHttpRequest(); |
| 146 | + |
| 147 | + xhr.upload.onprogress = (e) => { |
| 148 | + if (e.lengthComputable) { |
| 149 | + const pct = Math.round((e.loaded / e.total) * 100); |
| 150 | + console.log('Upload progress:', `${pct}%`); |
| 151 | + } |
| 152 | + }; |
| 153 | + |
| 154 | + xhr.addEventListener('error', () => reject(new Error('Upload failed: network error'))); |
| 155 | + xhr.addEventListener('abort', () => reject(new Error('Upload aborted'))); |
| 156 | + xhr.addEventListener('loadend', () => { |
| 157 | + const ok = xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300; |
| 158 | + if (!ok) { |
| 159 | + return reject(new Error(`Upload failed: HTTP ${xhr.status}`)); |
| 160 | + } |
| 161 | + resolve(); |
145 | 162 | }); |
146 | | -} |
147 | | -formData.append('file', file); |
148 | 163 |
|
149 | | -const uploadResp = await fetch(uploadUrl, { |
150 | | - method: 'POST', |
151 | | - body: formData, |
| 164 | + xhr.open('PUT', uploadUrl, true); |
| 165 | + xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); |
| 166 | + |
| 167 | + if (uploadExtraParams) { |
| 168 | + Object.entries(uploadExtraParams).forEach(([key, value]) => { |
| 169 | + xhr.setRequestHeader(key, String(value)); |
| 170 | + }); |
| 171 | + } |
| 172 | + |
| 173 | + xhr.send(file); |
152 | 174 | }); |
153 | | -if (!uploadResp.ok) { |
154 | | - throw new Error('Upload failed'); |
155 | | -} |
156 | 175 | ``` |
157 | 176 |
|
158 | 177 | ### Committing to an existing record (commitUrlToUpdateExistingRecord) |
@@ -310,21 +329,37 @@ async function onFileChange(e: Event) { |
310 | 329 | ).then(r => r.json()); |
311 | 330 |
|
312 | 331 | // 2) Direct upload to storage |
313 | | - const formData = new FormData(); |
314 | | - if (uploadExtraParams) { |
315 | | - Object.entries(uploadExtraParams).forEach(([key, value]) => { |
316 | | - formData.append(key, value as string); |
| 332 | + await new Promise<void>((resolve, reject) => { |
| 333 | + const xhr = new XMLHttpRequest(); |
| 334 | +
|
| 335 | + xhr.upload.onprogress = (e) => { |
| 336 | + if (e.lengthComputable) { |
| 337 | + const pct = Math.round((e.loaded / e.total) * 100); |
| 338 | + console.log('Upload progress:', `${pct}%`); |
| 339 | + } |
| 340 | + }; |
| 341 | +
|
| 342 | + xhr.addEventListener('error', () => reject(new Error('Upload failed: network error'))); |
| 343 | + xhr.addEventListener('abort', () => reject(new Error('Upload aborted'))); |
| 344 | + xhr.addEventListener('loadend', () => { |
| 345 | + const ok = xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300; |
| 346 | + if (!ok) { |
| 347 | + return reject(new Error(`Upload failed: HTTP ${xhr.status}`)); |
| 348 | + } |
| 349 | + resolve(); |
317 | 350 | }); |
318 | | - } |
319 | | - formData.append('file', file); |
320 | 351 |
|
321 | | - const uploadResp = await fetch(uploadUrl, { |
322 | | - method: 'POST', |
323 | | - body: formData, |
| 352 | + xhr.open('PUT', uploadUrl, true); |
| 353 | + xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); |
| 354 | +
|
| 355 | + if (uploadExtraParams) { |
| 356 | + Object.entries(uploadExtraParams).forEach(([key, value]) => { |
| 357 | + xhr.setRequestHeader(key, String(value)); |
| 358 | + }); |
| 359 | + } |
| 360 | +
|
| 361 | + xhr.send(file); |
324 | 362 | }); |
325 | | - if (!uploadResp.ok) { |
326 | | - throw new Error('Upload failed'); |
327 | | - } |
328 | 363 |
|
329 | 364 | // 3) Tell AdminForth to store filePath in the target field |
330 | 365 | emit('update:recordFieldValue', { |
|
0 commit comments