upload

package
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 6, 2026 License: MIT Imports: 21 Imported by: 0

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.

  1. User selects file in <input type="file">
  2. Client performs HTTP POST to /upload endpoint (traditional)
  3. Server streams to temp storage (disk/S3), returns temp_id
  4. Client includes temp_id in form submission via WebSocket
  5. 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.

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

View Source
var ErrExpired = errors.New("upload: file expired")

ErrExpired is returned when a temp file has expired.

View Source
var ErrNotFound = errors.New("upload: file not found")

ErrNotFound is returned when a temp file doesn't exist.

View Source
var ErrTooLarge = errors.New("upload: file too large")

ErrTooLarge is returned when a file exceeds the size limit.

View Source
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

func Handler(store Store) http.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

func HandlerWithConfig(store Store, config *Config) http.Handler

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

func NormalizeFilename(raw string) string

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

func NewDiskStore(dir string, maxSize int64) (*DiskStore, error)

NewDiskStore creates a new DiskStore.

Parameters:

  • dir: Directory to store temp files
  • maxSize: Maximum file size in bytes (0 = no limit)

func (*DiskStore) Claim

func (s *DiskStore) Claim(tempID string) (*File, error)

Claim retrieves and removes a temp file.

func (*DiskStore) Cleanup

func (s *DiskStore) Cleanup(maxAge time.Duration) error

Cleanup removes expired temp files.

func (*DiskStore) Save

func (s *DiskStore) Save(req SaveRequest, r io.Reader) (string, error)

Save stores the uploaded file and returns a temp ID.

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

func Claim(store Store, tempID string) (*File, error)

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.

func (*File) Close

func (f *File) Close() error

Close closes the file reader if open.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL