File Upload
Server-side image uploads with Bun.Image compression and pluggable storage (local FS or S3).
The file-upload feature gives you a complete backend for the files/upload-area block: a POST /api/files endpoint that accepts multipart/form-data, compresses images with Bun.Image, persists metadata via Drizzle, and stores the binary on local disk or S3.
Install
bun x bosia@latest feat file-upload # prompts for DB dialect
bun x bosia@latest feat -y file-upload # auto: sqlite default
bun x bosia@latest feat file-upload -d postgres # explicit
bun x bosia@latest feat -y file-upload -d mysql # auto + explicit dialect-y / --yes is a feat-level flag (auto-confirms prompts, uses each feature's default option values). -d / --dialect belongs to the file-upload feature — declared in its meta.json options, parsed only when the feature is being installed. The CLI also installs the drizzle feature on first use and pulls the files/upload-area + files/crop-image blocks.
After install:
bun run db:generate
bun run db:migrateWhat you get
| Path | Purpose |
|---|---|
src/features/file-upload/schemas/file.table.ts |
Drizzle table (matches your dialect) |
src/features/file-upload/file.service.ts |
Validation + compression orchestration |
src/features/file-upload/file.repository.ts |
DB queries |
src/features/file-upload/storage/ |
local + s3 adapters |
src/routes/api/files/+server.ts |
GET list, POST upload |
src/routes/api/files/[id]/+server.ts |
DELETE (cascades to storage) |
src/routes/uploads/[...path]/+server.ts |
Streams local files (skipped for S3) |
Env vars
Added to .env on install:
STORAGE_DRIVER=local # or "s3"
UPLOAD_DIR=./uploads
PUBLIC_BASE_URL=http://localhost:3000
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT= # leave empty for AWS S3
S3_REGION=autoWire the block
<script lang="ts">
import UploadArea from "$lib/blocks/files/upload-area/block.svelte";
</script>
<UploadArea uploadUrl="/api/files" onUploaded={(res) => console.log(res.url)} />The POST /api/files response shape matches what upload-area expects:
{
"id": "uuid",
"key": "uuid.webp",
"url": "http://localhost:3000/uploads/uuid.webp",
"mime": "image/webp",
"size": 123456,
"width": 1440,
"height": 1080,
"createdAt": "..."
}Image processing
- Allowed MIME:
image/jpeg,image/png,image/webp,image/heic,image/avif. Others are rejected with400. - Decoded with
Bun.Image, fit-inside resized to 1920×1080 max (no upscale), re-encoded as WebP @ q=0.85. - File extension always becomes
.webpregardless of source format.
To change limits, edit the MAX constant in file.service.ts.
Switching to S3
STORAGE_DRIVER=s3
S3_BUCKET=my-bucket
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_ENDPOINT=https://nyc3.digitaloceanspaces.com # optional, for non-AWSRestart the server. The uploads/ static route is harmless when unused — file URLs now point at the bucket.
Notes
- The
uploads/[...path]route streams fromUPLOAD_DIRwith a path-traversal guard. It is the simplest way to serve local files; in production, prefer a reverse proxy. - Cropping is client-side via the
files/crop-imageblock — wire it throughupload-area'sonCropRequestprop.