Browser uploads
The usual pattern for user uploads in a web app: your server never handles the file. It signs a short-lived URL; the browser uploads straight to SwiftFS.
browser ──(1) "I want to upload photo.jpg"──► your server
browser ◄─(2) presigned PUT URL (10 minutes)── your server (signs with its access key)
browser ──(3) PUT the file──────────────────► SwiftFS (checks the signature, and CORS)This is the flow SwiftFS's JavaScript SDK test suite runs, end to end, in a real browser.
1. Allow your site's origin (CORS)
Browsers only send cross-origin uploads that the bucket's CORS rules allow:
{
"CORSRules": [
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["PUT", "GET"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
}aws --endpoint-url "$S3_ENDPOINT" s3api put-bucket-cors --bucket acme-media --cors-configuration file://cors.json
aws --endpoint-url "$S3_ENDPOINT" s3api get-bucket-cors --bucket acme-mediaSwiftFS answers the browser's preflight (OPTIONS) from these rules, and adds
the CORS headers to actual responses, errors included. A bucket has up to 100
rules; an origin or header may contain one * wildcard.
2. Sign an upload on your server
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ endpoint: process.env.S3_ENDPOINT, region: process.env.AWS_REGION, forcePathStyle: true });
export async function uploadUrl(userId, fileName, contentType) {
const key = `uploads/${userId}/${crypto.randomUUID()}-${fileName}`;
const url = await getSignedUrl(
s3,
new PutObjectCommand({ Bucket: "acme-media", Key: key, ContentType: contentType }),
{ expiresIn: 600 },
);
return { key, url };
}Use a key that is scoped to this bucket (see access keys): the server only ever needs to write uploads.
3. Upload from the browser
const { url, key } = await (await fetch("/api/upload-url", { method: "POST", body: JSON.stringify({ name: file.name, type: file.type }) })).json();
const res = await fetch(url, { method: "PUT", body: file, headers: { "Content-Type": file.type } });
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);The Content-Type must be the one you signed. For files over a few hundred
megabytes, sign the parts of a multipart upload instead.
Serving what was uploaded
- Public media (avatars, product images): allow anonymous reads of a
prefix with a bucket policy, then link to
https://endpoint/acme-media/public/…. - Private files (invoices, documents): your server signs a GET URL for each
download.
ResponseContentDispositionmakes the browser save it under a friendly name:
import { GetObjectCommand } from "@aws-sdk/client-s3";
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: "acme-media", Key: key, ResponseContentDisposition: 'attachment; filename="invoice.pdf"' }),
{ expiresIn: 300 },
);Browser form uploads (POST Object) are not supported yet: use presigned PUTs.