Package upload (stdlib only) goes beyond storage.PutUploaded: it never buffers a whole file.
rules := upload.Images(5 << 20) // MaxFileSize and Types are required: zero Rules reject everything
rules.MaxFiles, rules.Dir = 3, "photos"
rules.Scanner = upload.ClamAV("127.0.0.1:3310") // optional
form, err := upload.Receive(c, disk, rules) // form.Values, form.Files[i].Path/Name/Type/Size/SHA256Streaming multipart#
Receivereadsr.MultipartReader()part by part and streams each file to thestorage.Disk: a 64 MiB upload allocates about 100 KiB.Rulesbound the file size, the whole request (MaxSize, defaultMaxFiles*MaxFileSize + 1 MiB, which replaces the router-wide body limit for that route), the file count, and field count and size. Sizes fail with 413; types, extensions and count with 422. On any error every file already written is deleted.- Type checks:
http.DetectContentTypeon the first 512 bytes must be inTypes, and the client's extension must be one known for that type (upload.Extensions("image/jpeg")→.jpg,.jpeg). The client'sContent-Typeis ignored. Stored names are random with the canonical extension; the sanitized client name (upload.SanitizeName) is metadata only. upload.Save(ctx, disk, rules, field, filename, r)applies the same checks to any reader (finished tus uploads, imports).
Virus scanning#
With Rules.Scanner, files are written to QuarantineDir/.<random> (dot-named, so storage.Handler never serves them), scanned, then copied to Dir. ErrInfected deletes the file and answers 422; a scanner error deletes it and answers 503 (fail closed). upload.ClamAV(addr) speaks clamd's INSTREAM over TCP with per-operation timeouts.
Resumable uploads (tus)#
upload.Tus implements tus 1.0 core with the creation, expiration and termination extensions:
tus, err := upload.Tus(upload.TusConfig{
Dir: "storage/app/tus", BasePath: "/uploads/tus/", MaxSize: 2 << 30,
Authorize: func(r *http.Request) error { /* session or token check */ return nil },
OnComplete: func(ctx context.Context, info upload.TusInfo, open func() (io.ReadCloser, error)) error {
rc, err := open()
if err != nil {
return err
}
defer rc.Close()
_, err = upload.Save(ctx, disk, rules, "file", info.Filename(), rc)
return err
},
})
r.Handle("/uploads/tus/", tus)
app.OnShutdown(func(context.Context) error { return tus.Close() })POSTchecksUpload-LengthandUpload-Metadataand answers 201 with a randomLocation;HEADreturns the offset;PATCHneedsTus-Resumable: 1.0.0,Content-Type: application/offset+octet-streamand the currentUpload-Offset, and appends the body as it streams. An interrupted request keeps what arrived. ConcurrentPATCHes on one upload get 423.- The upload id is the capability, so
Authorizeis required (boot error when nil; returnnilexplicitly to allow everyone). - Abandoned uploads expire after
Expiry(24h); scheduletus.Cleanup(ctx)to delete them. When the last byte arrivesOnCompleteruns; its error goes to the client (422 for validation) and the partial data is deleted either way.
Direct-to-S3 uploads#
Browsers can upload straight to the bucket. upload.Direct takes any upload.Presigner:
type Presigner interface {
PresignPut(ctx context.Context, key, contentType string, maxSize int64, ttl time.Duration) (url string, headers http.Header, err error)
}
direct, err := upload.Direct(upload.DirectConfig{Presigner: presigner, Rules: rules, Authorize: authorize})
r.Handle("/uploads/presign", direct)
// later, when the client reports the upload done:
file, err := upload.Verify(ctx, disk, key, rules)Direct answers POST {"filename","content_type","size"} with {"key","url","method":"PUT","headers","expires_at"} after checking the declared type, extension and size, under a random key in Rules.Dir (TTL 15 minutes, at most 1 hour). The declared type cannot be trusted, so upload.Verify sniffs the stored object and deletes it when it breaks the rules.
HTMX progress component#
The HTMX starter ships resources/views/components/upload.html (a data-upload form with a <progress>) and a /uploads demo page. resources/js/app.ts swaps htmx's fetch for XMLHttpRequest on those forms to report progress, without inline scripts, so the CSP holds. Validate files received through c.Input() with z.File() (validation).