Skip to content

Commit 189643b

Browse files
committed
docs: strict content type AdminForth/2032/fix-upload-plugin-and-storage-
Storage adapters: strictContentType option of the S3 adapters. Upload plugin API: the examples send the contentType returned with the upload URL, and size has to be the real size of the file.
1 parent b69ed3e commit 189643b

2 files changed

Lines changed: 31 additions & 10 deletions

File tree

‎documentation/docs/tutorial/06-Adapters/04-storage-adapters.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,31 @@ pnpm add @adminforth/storage-adapter-amazon-s3
1616

1717
Stores uploaded files in [Amazon S3](https://aws.amazon.com/s3/), providing scalable cloud storage.
1818

19+
### Strict content type
20+
21+
The upload plugin picks the content type of a file by its extension, e.g. `image/png` for `.png`. By default S3 does not check it on upload, so a client can store a file with any `Content-Type`, e.g. `text/html` under a `.png` key, and the browser will open it as a page. Set `strictContentType: true` to sign the content type into the upload URL, then an upload with another type is rejected:
22+
23+
```ts
24+
new AdminForthAdapterS3Storage({
25+
bucket: process.env.AWS_BUCKET_NAME as string,
26+
region: process.env.AWS_REGION as string,
27+
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
28+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
29+
strictContentType: true,
30+
}),
31+
```
32+
33+
Before turning it on, update `@adminforth/bulk-ai-flow`, `@adminforth/markdown`, `@adminforth/rich-editor` and `@adminforth/oauth`: their older versions send the type reported by the browser, and the upload fails when it differs from the one picked by the extension. Your own code that uploads files to the URL from the [upload plugin API](../09-Plugins/05-1-upload-api.md) has to send the `contentType` returned with the URL as well. While the option is off, the adapter logs a warning on start. It will be on by default in the next major version.
34+
1935
## S3 compatible adapter
2036
```bash
2137
pnpm add @adminforth/storage-adapter-s3-compatible
2238
```
2339

2440
Provides S3 compatible interface for object storage services such as MinIO, Wasabi, Cloudflare R2 or other third-party S3 providers.
41+
42+
It supports the same `strictContentType` option as the Amazon S3 adapter, see [Strict content type](#strict-content-type).
43+
2544
### How the adapter works
2645

2746
The Amazon S3 adapter uses tagging to mark objects with the special tag `adminforth-candidate-for-cleanup`, and then creates a lifecycle rule that automatically expires objects with that tag.

‎documentation/docs/tutorial/09-Plugins/05-1-upload-api.md‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ app.post(
115115

116116
const plugin = admin.getPluginById('my_reports_plugin');
117117

118-
const { uploadUrl, filePath, uploadExtraParams, pathColumnName } = await plugin.getUploadUrl({
118+
const { uploadUrl, contentType: uploadContentType, filePath, uploadExtraParams, pathColumnName } = await plugin.getUploadUrl({
119119
recordId, // can be undefined for new records
120120
filename, // e.g. file.name
121121
contentType, // e.g. file.type
122-
size, // optional, will be validated against maxFileSize to drop error earlier
122+
size, // e.g. file.size: validated against maxFileSize, the upload URL then accepts only a file of exactly this size
123123
});
124124

125-
res.json({ uploadUrl, filePath, uploadExtraParams, pathColumnName });
125+
res.json({ uploadUrl, contentType: uploadContentType, filePath, uploadExtraParams, pathColumnName });
126126
}),
127127
);
128128
```
@@ -133,7 +133,7 @@ On the frontend:
133133
const file = input.files[0];
134134

135135
// 1) Ask your backend to generate upload URL
136-
const { uploadUrl, filePath, uploadExtraParams } = await fetch('/api/uploads/get-url-existing', {
136+
const { uploadUrl, contentType, filePath, uploadExtraParams } = await fetch('/api/uploads/get-url-existing', {
137137
method: 'POST',
138138
headers: { 'Content-Type': 'application/json' },
139139
body: JSON.stringify({
@@ -166,7 +166,8 @@ await new Promise<void>((resolve, reject) => {
166166
});
167167

168168
xhr.open('PUT', uploadUrl, true);
169-
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
169+
// the type picked by the plugin from the file extension, the storage may reject another one
170+
xhr.setRequestHeader('Content-Type', contentType);
170171

171172
if (uploadExtraParams) {
172173
Object.entries(uploadExtraParams).forEach(([key, value]) => {
@@ -321,7 +322,7 @@ async function onFileChange(e: Event) {
321322
if (!file) return;
322323
323324
// 1) Ask backend for presigned URL
324-
const { uploadUrl, filePath, uploadExtraParams, pathColumnName } = await fetch(
325+
const { uploadUrl, contentType, filePath, uploadExtraParams, pathColumnName } = await fetch(
325326
'/api/uploads/avatar/get-url',
326327
{
327328
method: 'POST',
@@ -357,7 +358,8 @@ async function onFileChange(e: Event) {
357358
});
358359
359360
xhr.open('PUT', uploadUrl, true);
360-
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
361+
// the type picked by the plugin from the file extension, the storage may reject another one
362+
xhr.setRequestHeader('Content-Type', contentType);
361363
362364
if (uploadExtraParams) {
363365
Object.entries(uploadExtraParams).forEach(([key, value]) => {
@@ -395,14 +397,14 @@ app.post(
395397
396398
const plugin = admin.getPluginById('my_reports_plugin');
397399
398-
const { uploadUrl, filePath, uploadExtraParams, pathColumnName } = await plugin.getUploadUrl({
400+
const { uploadUrl, contentType: uploadContentType, filePath, uploadExtraParams, pathColumnName } = await plugin.getUploadUrl({
399401
recordId, // can be undefined for create pages
400402
filename,
401403
contentType,
402-
size, // optional
404+
size, // exact size of the file, the upload URL accepts only this size
403405
});
404406
405-
res.json({ uploadUrl, filePath, uploadExtraParams, pathColumnName });
407+
res.json({ uploadUrl, contentType: uploadContentType, filePath, uploadExtraParams, pathColumnName });
406408
}),
407409
);
408410
```

0 commit comments

Comments
 (0)