Documentation
¶
Overview ¶
Package upload provides file upload handling for Vango.
Since WebSocket connections are poor at handling large binary uploads (blocking heartbeats), this package uses a hybrid HTTP+WebSocket approach.
The Problem ¶
Large file uploads over WebSocket block the heartbeat and event loop, causing connection timeouts and poor user experience.
The Solution ¶
Hybrid approach: HTTP POST for upload, WebSocket for processing.
- User selects file in <input type="file">
- Client performs HTTP POST to /upload endpoint (traditional)
- Server streams to temp storage (disk/S3), returns temp_id
- Client includes temp_id in form submission via WebSocket
- Vango handler calls upload.Claim(temp_id) to finalize
Usage ¶
Mount the upload handler in your router:
r.Post("/upload", upload.Handler(uploadStore))
Security Warning: CSRF Protection Required ¶
IMPORTANT: The handlers in this package do NOT include CSRF protection. If you mount these handlers without CSRF middleware, your upload endpoint will be vulnerable to Cross-Site Request Forgery attacks.
When using with Vango, ALWAYS use one of these secure patterns:
// Option 1: Use App helper (recommended)
app.HandleUpload("/api/upload", store)
// Option 2: Manual CSRF wrapping
csrfMw := app.Server().CSRFMiddleware()
mux.Handle("POST /api/upload", csrfMw(upload.Handler(store)))
// Option 3: Use Server helper
mux.Handle("POST /api/upload", srv.UploadHandler(store, nil))
If using outside Vango, implement equivalent CSRF protection using your framework's CSRF middleware (e.g., gorilla/csrf, chi middleware, etc.).
Security ¶
The upload handler enforces Config.AllowedTypes against a server-side detected MIME type (http.DetectContentType). Client-provided part headers like Content-Type are not trusted. HandlerWithConfig treats a nil Config as DefaultConfig and fails closed with 503 Service Unavailable when mounted with a nil or typed-nil Store.
For defense-in-depth, also consider:
- CSRF protection (REQUIRED - see Security Warning above)
- Restricting filename extensions via Config.AllowedExtensions
- Enforcing extension-to-type match via Config.RequireExtensionMatch
- Treating File.OriginalFilename as untrusted client input
- Virus/malware scanning before making uploads available to end users
Handle the uploaded file in your Vango component:
func CreatePost(ctx vango.Ctx, formData server.FormData) error {
tempID := formData.Get("attachment_temp_id")
var attachment *upload.File
if tempID != "" {
file, err := upload.Claim(uploadStore, tempID)
if err != nil {
return err
}
attachment = file
}
// attachment.Filename / attachment.SafeFilename are normalized and safe-by-default.
// attachment.OriginalFilename is untrusted client input.
return nil
}
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrExpired = errors.New("upload: file expired")
ErrExpired is returned when a temp file has expired.
var ErrNotFound = errors.New("upload: file not found")
ErrNotFound is returned when a temp file doesn't exist.
var ErrTooLarge = errors.New("upload: file too large")
ErrTooLarge is returned when a file exceeds the size limit.
var ErrTypeNotAllowed = errors.New("upload: file type not allowed")
ErrTypeNotAllowed is returned when a file's MIME type is not in AllowedTypes.
Functions ¶
func Handler ¶
Handler returns an http.Handler for file uploads.
SECURITY WARNING: This handler does NOT include CSRF protection. You MUST wrap this handler with CSRF middleware to prevent CSRF attacks:
// Recommended: Use App helper
app.HandleUpload("/api/upload", store)
// Or wrap manually with CSRF middleware
csrfMw := app.Server().CSRFMiddleware()
mux.Handle("POST /api/upload", csrfMw(upload.Handler(store)))
See package documentation for complete security guidance.
The handler expects a multipart form with a "file" field. It returns JSON with the temp_id:
{"temp_id": "abc123"}
func HandlerWithConfig ¶
HandlerWithConfig returns an upload handler with custom configuration.
SECURITY WARNING: This handler does NOT include CSRF protection. See Handler documentation for secure usage patterns.
func NormalizeFilename ¶
NormalizeFilename normalizes an untrusted client filename into a safe single path segment.
Types ¶
type Config ¶
type Config struct {
// MaxFileSize is the maximum allowed file size in bytes.
// Default: 10MB.
MaxFileSize int64
// AllowedTypes is a list of allowed MIME types.
// If empty, all types are allowed.
AllowedTypes []string
// AllowedExtensions is a list of allowed filename extensions (e.g. ".png", "jpg").
// If empty, all extensions are allowed.
AllowedExtensions []string
// RequireExtensionMatch rejects uploads whose filename extension does not match the detected MIME type.
// This is an optional defense-in-depth check to avoid content-type/extension confusion.
RequireExtensionMatch bool
// TempExpiry is how long temp files live before cleanup.
// Default: 1 hour.
TempExpiry time.Duration
}
Config holds configuration for the upload handler.
func DefaultConfig ¶
func DefaultConfig() *Config
DefaultConfig returns a Config with sensible defaults.
type DiskStore ¶
type DiskStore struct {
// contains filtered or unexported fields
}
DiskStore stores uploads on the local filesystem.
func NewDiskStore ¶
NewDiskStore creates a new DiskStore.
Parameters:
- dir: Directory to store temp files
- maxSize: Maximum file size in bytes (0 = no limit)
type File ¶
type File struct {
// ID is the unique identifier for this upload.
ID string
// Filename is a compatibility alias for SafeFilename.
// It is always normalized by the framework and never contains raw client input.
Filename string
// OriginalFilename is the raw filename from the client upload request.
// Treat this as untrusted input.
OriginalFilename string
// SafeFilename is the normalized filename produced by NormalizeFilename.
SafeFilename string
// ContentType is the MIME type of the file.
ContentType string
// Size is the file size in bytes.
Size int64
// Path is the local filesystem path (for DiskStore).
Path string
// URL is the remote URL (for S3/CDN storage).
URL string
// Reader provides access to the file contents.
// May be nil if the file is stored on disk (use Path instead).
Reader io.ReadCloser
}
File represents an uploaded file.
func Claim ¶
Claim retrieves a temp file by ID. Call this in your Vango handler after receiving the temp_id.
Example:
file, err := upload.Claim(store, tempID)
if err != nil {
return err
}
defer file.Close()
// file.Filename/file.SafeFilename are normalized and safe-by-default.
// file.OriginalFilename is untrusted client input.
type SaveRequest ¶
type SaveRequest struct {
// OriginalFilename is the raw filename provided by the client.
// This value is untrusted and must never be used directly in security-sensitive contexts.
OriginalFilename string
// SafeFilename is framework-normalized and safe for headers, logs, and storage keys.
SafeFilename string
// ContentType is the server-detected MIME type of the upload.
ContentType string
// Size is the client-reported size in bytes.
Size int64
// Request is the incoming HTTP request that produced this upload.
// It is optional and may be nil for direct Store.Save calls in tests/tools.
Request *http.Request
// Context is the request context associated with this upload.
// It is optional and may be nil for direct Store.Save calls in tests/tools.
Context context.Context
}
SaveRequest contains upload metadata passed to Store.Save.
type Store ¶
type Store interface {
// Save stores the uploaded file and returns a temp ID.
// The file is stored temporarily until Claim is called.
// Implementations must consume r synchronously and must not retain it after Save returns.
// HandlerWithConfig cleans up multipart temp files at the end of the request.
Save(req SaveRequest, r io.Reader) (tempID string, err error)
// Claim retrieves and removes a temp file, returning a file handle.
// After claiming, the temp file is deleted (or marked for deletion).
// Implementations should populate OriginalFilename and SafeFilename when available.
Claim(tempID string) (*File, error)
// Cleanup removes expired temp files.
// Call this periodically (e.g., every 5 minutes).
Cleanup(maxAge time.Duration) error
}
Store is the interface for upload storage backends. Implement this interface to use S3, GCS, or other storage.