File Uploads

Nimbus uses Go's standard multipart handling for file uploads, combined with the built-in storage package for persisting files to disk or other drivers.

Basic file upload

Use c.Request.FormFile(fieldName) to retrieve an uploaded file. It returns the file, its header (name, size, MIME type), and any error.

func uploadHandler(c *http.Context) error {
    file, header, err := c.Request.FormFile("avatar")
    if err != nil {
        return c.JSON(400, map[string]string{"error": "missing file"})
    }
    defer file.Close()

    return c.JSON(200, map[string]any{
        "filename": header.Filename,
        "size":     header.Size,
    })
}

Saving to disk manually

Create a destination file with os.Create and copy the upload contents using io.Copy.

func saveUpload(c *http.Context) error {
    file, header, err := c.Request.FormFile("document")
    if err != nil {
        return c.JSON(400, map[string]string{"error": "missing file"})
    }
    defer file.Close()

    dst, err := os.Create(filepath.Join("storage/app", header.Filename))
    if err != nil {
        return c.JSON(500, map[string]string{"error": "could not save"})
    }
    defer dst.Close()

    if _, err := io.Copy(dst, file); err != nil {
        return c.JSON(500, map[string]string{"error": "write failed"})
    }

    return c.JSON(201, map[string]string{"path": header.Filename})
}

File size limits

Call c.Request.ParseMultipartForm(maxMemory) before accessing files to set the memory limit. Also validate header.Size against your own maximum.

const maxUploadSize = 5 << 20 // 5 MB

func limitedUpload(c *http.Context) error {
    c.Request.Body = http.MaxBytesReader(c.Response, c.Request.Body, maxUploadSize)

    if err := c.Request.ParseMultipartForm(maxUploadSize); err != nil {
        return c.JSON(413, map[string]string{"error": "file too large"})
    }

    file, header, err := c.Request.FormFile("photo")
    if err != nil {
        return c.JSON(400, map[string]string{"error": "missing file"})
    }
    defer file.Close()

    if header.Size > maxUploadSize {
        return c.JSON(413, map[string]string{"error": "exceeds 5 MB limit"})
    }

    return c.JSON(200, map[string]string{"filename": header.Filename})
}

Validating file types

Check the Content-Type from the file header, or sniff the first 512 bytes with http.DetectContentType for reliable MIME detection.

var allowedTypes = map[string]bool{
    "image/jpeg": true,
    "image/png":  true,
    "image/webp": true,
}

func validateFileType(c *http.Context) error {
    file, _, err := c.Request.FormFile("image")
    if err != nil {
        return c.JSON(400, map[string]string{"error": "missing file"})
    }
    defer file.Close()

    buf := make([]byte, 512)
    n, _ := file.Read(buf)
    mime := http.DetectContentType(buf[:n])

    if !allowedTypes[mime] {
        return c.JSON(422, map[string]string{"error": "invalid file type: " + mime})
    }

    return c.JSON(200, map[string]string{"type": mime})
}

Using the storage driver

The storage.LocalDriver provides a clean API for persisting files. Create a driver pointing to your storage root, then call Put(path, reader).

import "github.com/CodeSyncr/nimbus/storage"

var disk = storage.NewLocalDriver("storage/app")

func uploadWithStorage(c *http.Context) error {
    file, header, err := c.Request.FormFile("attachment")
    if err != nil {
        return c.JSON(400, map[string]string{"error": "missing file"})
    }
    defer file.Close()

    dest := "uploads/" + header.Filename
    if err := disk.Put(dest, file); err != nil {
        return c.JSON(500, map[string]string{"error": "storage failed"})
    }

    return c.JSON(201, map[string]string{"path": dest})
}

Storage driver interface

All storage drivers implement the storage.Driver interface:

  • Put(path, io.Reader) — Write a file.
  • Get(path) — Open a file for reading (returns io.ReadCloser).
  • Delete(path) — Remove a file.
  • Exists(path) — Check if a file exists.