Skip to content

File Uploads and Downloads (x/fileapi)

This guide shows how to wire x/fileapi for HTTP file transport — multipart upload, streaming download, metadata retrieval, and per-user file listing. x/fileapi provides the HTTP handlers; x/data/file provides the storage backends.

x/fileapi is experimental. See Release Posture and Extension Maturity before depending on it in production.

HTTP request
x/fileapi.Handler ← HTTP transport: validates, parses, enforces size limits
x/data/file.Storage ← stores/retrieves file bytes (local disk or S3)
x/data/file.MetadataManager ← persists file metadata in a database

x/fileapi never owns storage policy. Keep backend selection, tenant path rules, and retention policy in x/data/file or your app wiring.

import datafile "github.com/spcent/plumego/x/data/file"
metadata, err := datafile.NewDBMetadataManager(sqlDB)
if err != nil {
log.Fatal(err)
}
storage, err := datafile.NewLocalStorage(
"/var/app/uploads", // base path on disk
"http://localhost:8080/files", // base URL for download links
metadata,
)
if err != nil {
log.Fatal(err)
}
metadata, err := datafile.NewDBMetadataManager(sqlDB)
if err != nil {
log.Fatal(err)
}
storage, err := datafile.NewS3Storage(datafile.S3Config{
Bucket: os.Getenv("S3_BUCKET"),
Region: os.Getenv("S3_REGION"),
AccessKey: os.Getenv("S3_ACCESS_KEY"),
SecretKey: os.Getenv("S3_SECRET_KEY"),
}, metadata)
if err != nil {
log.Fatal(err)
}
import "github.com/spcent/plumego/x/fileapi"
const maxUploadSize = 50 << 20 // 50 MB
h := fileapi.NewHandler(storage, metadata).
WithMaxSize(maxUploadSize)

Wire each handler method to an explicit route. Route parameters must include {id} for file-specific operations:

import "github.com/spcent/plumego/router"
r := router.NewRouter()
r.Post("/files", h.Upload)
r.Get("/files", h.List)
r.Get("/files/{id}", h.Download)
r.Get("/files/{id}/info", h.GetInfo)
r.Get("/files/{id}/url", h.GetURL)
r.Delete("/files/{id}", h.Delete)
MethodHandlerDescription
POST /filesh.UploadAccepts multipart/form-data; returns file metadata
GET /filesh.ListLists files owned by the current user
GET /files/{id}h.DownloadStreams file bytes; sets Content-Disposition
GET /files/{id}/infoh.GetInfoReturns file metadata as JSON
GET /files/{id}/urlh.GetURLReturns a presigned download URL
DELETE /files/{id}h.DeleteRemoves the file and its metadata

Upload and list handlers scope files by user ID. Inject the user ID into the request context before the file routes:

import "github.com/spcent/plumego/x/fileapi"
func withUserID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get the authenticated user ID — e.g. from JWT principal
userID := authn.PrincipalFromContext(r.Context()).Subject
ctx := fileapi.WithUserID(r.Context(), userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
r.Use(withUserID)

In upload and list handlers, fileapi.UserIDFromContext(r.Context()) retrieves this value to scope the operation.

When your service uses x/tenant, mount tenant resolution before the file middleware:

import (
authmw "github.com/spcent/plumego/middleware/auth"
resolve "github.com/spcent/plumego/x/tenant/resolve"
)
authMw, err := authmw.Authenticate(authenticator)
if err != nil {
return err
}
r.Use(authMw) // JWT auth first
r.Use(resolve.Middleware(resolve.Options{AllowMissing: false}))
r.Use(withUserID) // then user ID from principal

Tenant-scoped storage path logic lives in x/data/file, not in x/fileapi. Pass the tenant ID through your Storage implementation if you need per-tenant storage buckets or directories.

Clients upload files using multipart/form-data with the field name file:

POST /files
Content-Type: multipart/form-data; boundary=----boundary
------boundary
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf
<binary content>
------boundary--

A successful upload returns 201 Created with a JSON body containing the file ID, name, size, content type, and download URL.