File storage
S3-compatible: R2, MinIO, B2.
File uploads â avatars, attachments, exports. Grit's storage package abstracts S3-compatible backends: MinIO in dev (ships with docker-compose), Cloudflare R2 or AWS S3 in production. One interface, swap the backend via env.
The Storage interface
type Storage interface {Put(ctx context.Context, key string, body io.Reader, contentType string) (string, error)Get(ctx context.Context, key string) (io.ReadCloser, error)Delete(ctx context.Context, key string) errorPresignedURL(ctx context.Context, key string, expires time.Duration) (string, error)}
Same four operations regardless of backend. The handler doesn't know if it's talking to MinIO or R2.
Switching backends
# Choose backendSTORAGE_DRIVER=minio # minio | r2 | s3# MinIO (dev â bundled in docker-compose)MINIO_ENDPOINT=http://localhost:9000MINIO_ACCESS_KEY=minioadminMINIO_SECRET_KEY=minioadminMINIO_BUCKET=bench-api-uploads# Cloudflare R2 (prod)R2_ACCOUNT_ID=...R2_ACCESS_KEY=...R2_SECRET_KEY=...R2_BUCKET=acme-prod-uploads
Change STORAGE_DRIVER, restart. Same handler code, different bucket.
The Upload handler
Grit ships a generic upload handler at POST /api/uploads:
POST /api/uploadsHeaders: Authorization: Bearer <token>Body: multipart/form-data with file=<file>Response:{ "data": { "id": "9b...", "url": "/api/uploads/9b...", "size": 24832 } }
Behind the scenes: file goes into the storage backend keyed by its SHA-256 hash (deduplication by content), an Upload row is created in the DB. The returned URL is a Grit proxy URL that streams the file with auth + access checks.
Direct uploads (presigned URLs) â when files get large
For files >5 MB, you don't want them to pass through your API process. Use presigned URLs:
// API generates a one-time URL the client uploads directly tourl, err := h.storage.PresignedURL(ctx, key, 15*time.Minute)// Return url to the client; they PUT the file directly to S3/R2
Bandwidth doesn't hit your API. Useful for image uploads, video, backups, exports.
internal/safefetch.Get(url) instead â it blocks loopback, RFC1918, and cloud-metadata IPs. The Defender's Handbook page covers SSRF in depth.Content type validation
The handler validates content-type from the uploaded bytes (sniffing), not from the user-supplied Content-Type header. An attacker who renames shell.php to image.png gets caught â the sniffer sees PHP source, the handler rejects.
Image processing
For avatars and thumbnails, Grit ships an image processing pipeline:
processed, err := h.imageProcessor.Resize(ctx, body, 256, 256)// Outputs JPEG, 256x256, ~20 KBstorage.Put(ctx, key, processed, "image/jpeg")
Quick check
Try it
Upload a real file and read it back:
- With your access token, POST a file (a small PNG) to
/api/uploadswith curl:Terminal$curl -X POST http://localhost:8080/api/uploads \$ -H "Authorization: Bearer YOUR_TOKEN" \$ -F "file=@avatar.png" - Note the
urlin the response. - Open MinIO console (
http://localhost:9001, loginminioadmin/minioadmin) â you should see the file in the bucket. - Fetch the file via the API URL with your token â should stream the PNG back.
What's next
Last battery â AI Gateway. Stream from Claude, GPT-4, and ~98 other models with one API key. Built for products that wrap AI as a feature.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled â suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub