disk, err := storage.NewLocal("storage/app/public", "/storage") // os.Root: no "..", no escaping symlinks
r.Handle("/storage/", http.StripPrefix("/storage", storage.Handler(disk, nil)))
path, err := storage.PutUploaded(ctx, disk, fileHeader, "avatars", storage.Images(2<<20))
url := disk.URL(path)Disks#
storage.Disk has Put (atomic rename on local disks), Open, Exists, Delete, List and URL; storage.Get and storage.PutBytes are helpers. storage.NewMemory(baseURL) serves tests, and S3 lives in its own module. Custom disks pass storage/storagetest (conformance and traversal suites).
Serving files#
storage.Handler(disk, signer) never serves dotfiles or directories and sends X-Content-Type-Options: nosniff. With a signed.Signer it answers only signed, unexpired URLs:
private, _ := storage.NewLocal("storage/app/private", "")
r.Handle("/files/", http.StripPrefix("/files", storage.Handler(private, signer)))
link, err := storage.TemporaryURL(signer, "/files", "invoices/42.pdf", 15*time.Minute)Public files are served by the handler instead of a storage:link symlink (which needs extra privileges on Windows).
Uploads#
PutUploaded never uses the client's file name: it stores a random name with an extension from the type detected in the content (http.DetectContentType), checks the size from the header and again while copying, and answers 422 for disallowed types or sizes.
storage.Images(2 << 20) // JPEG, PNG, GIF, WebP up to 2 MiB
storage.UploadRules{MaxSize: 10 << 20, Types: []string{"application/pdf"}}For streaming, resumable (tus), scanned and direct-to-bucket uploads, see File uploads.
S3#
go get github.com/AlexGalhardo/vukem/drivers/s3A storage.Disk on Amazon S3 or any S3-compatible service (MinIO, Cloudflare R2, DigitalOcean Spaces, Backblaze B2). There is no AWS SDK: requests are signed by a standard-library Signature Version 4 implementation verified against the AWS test suite.
cfg, err := config.Load[s3.Config](app.Config.Source)
disk, err := s3.New(cfg) // validates the config, no I/O
err = disk.Put(ctx, "videos/intro.mp4", file) // one PUT up to S3_PART_SIZE, multipart above
info, err := disk.Stat(ctx, "videos/intro.mp4") // Size, ETag, ContentType, LastModified
up, err := disk.PresignPut("uploads/avatar.png", 5*time.Minute) // browser uploads straight to the bucket
link, err := disk.PresignGet("invoices/42.pdf", time.Hour)| Variable | Default | Meaning |
|---|---|---|
S3_BUCKET | — | bucket name (3–63 of a-z 0-9 . -) |
S3_REGION | us-east-1 | signing region (auto for R2) |
S3_ENDPOINT | https://s3.<region>.amazonaws.com | origin of an S3-compatible service |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | — | required; S3_SESSION_TOKEN for temporary credentials |
S3_USE_PATH_STYLE | false | endpoint/bucket/key instead of bucket.endpoint/key (MinIO) |
S3_INSECURE_HTTP | false | allow an http:// endpoint (local development only) |
S3_PART_SIZE | 8 MiB | multipart part size, 5 MiB to 5 GiB |
S3_PUBLIC_URL | — | prefix for disk.URL (CDN or public bucket); empty means private |
Putbuffers at most one part. A multipart upload is aborted on any failure (part error, reader error, cancelled context, a200carrying an<Error>), so no orphaned parts are billed. The content type comes from the extension, else from the first bytes.Openstreams the object;Listfollows continuation tokens and skips folder placeholders. Missing objects matchfs.ErrNotExist; other failures are*s3.Error(StatusCode,Code,Message,RequestID).- Keys go through
storage.Clean: no empty key, no leading/, no.., backslash or:. - Presigned URLs sign only the
hostheader and expire after 1 second to 7 days. Credentials areconfig.Secret, so errors and%+vnever show them. - Integration tests run against MinIO (
task services:up); the driver passesstoragetest.Runandstoragetest.RunTraversal.