Skip to content

Commit 7f6c4e8

Browse files
committed
fix: update upload API documentation to clarify usage of XMLHttpRequest and HTTP PUT method
1 parent 79f7160 commit 7f6c4e8

File tree

1 file changed

+61
-26
lines changed

1 file changed

+61
-26
lines changed

adminforth/documentation/docs/tutorial/08-Plugins/05-1-upload-api.md

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
The Upload plugin exposes an API for both backend-only uploads and direct browser uploads using presigned URLs. You can:
44

55
- 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).
77
- After the file is uploaded, it is considered **temporary** and can be auto-deleted until you commit it.
88
- You can commit by calling `commitUrlToNewRecord` or `commitUrlToUpdateExistingRecord` from your custom API.
99
- Or (only for custom create/edit components on the same resource) you can commit by writing `filePath` into the `pathColumnName` field using `update:recordFieldValue`.
1010

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+
1114
## Uploading from backend (Buffer API)
1215

1316
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
8386

8487
1. Your custom or admin frontend sends a request to your backend.
8588
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.
8790
4. After the upload completes, you **commit** the uploaded file to a record using either:
8891
- `plugin.commitUrlToUpdateExistingRecord` (update existing record), or
8992
- `plugin.commitUrlToNewRecord` (create new record), or
@@ -138,21 +141,37 @@ const { uploadUrl, filePath, uploadExtraParams } = await fetch('/api/uploads/get
138141
}).then(r => r.json());
139142

140143
// 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();
145162
});
146-
}
147-
formData.append('file', file);
148163

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);
152174
});
153-
if (!uploadResp.ok) {
154-
throw new Error('Upload failed');
155-
}
156175
```
157176

158177
### Committing to an existing record (commitUrlToUpdateExistingRecord)
@@ -310,21 +329,37 @@ async function onFileChange(e: Event) {
310329
).then(r => r.json());
311330
312331
// 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();
317350
});
318-
}
319-
formData.append('file', file);
320351
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);
324362
});
325-
if (!uploadResp.ok) {
326-
throw new Error('Upload failed');
327-
}
328363
329364
// 3) Tell AdminForth to store filePath in the target field
330365
emit('update:recordFieldValue', {

0 commit comments

Comments
 (0)