Skip to content

ghulamazad/GFileMux

Repository files navigation

GFileMux

Go Version License: MIT Go Report Card

GFileMux is a fast, lightweight Go package for handling multipart file uploads. Inspired by Multer, it offers flexible storage options, middleware-style handling, and seamless processing with minimal overhead. Compatible with any Go HTTP framework, GFileMux simplifies file uploads for your web apps.

GFileMux is a personal passion project by me (@ghulamazad). I built it to avoid rewriting multipart upload boilerplate and to create a clean, composable, framework-agnostic solution for Go—similar in spirit to Multer for Node.js. It’s developed in my free time.

Table of Contents

Features

Efficient File Parsing – Handles multipart/form-data seamlessly.
📂 Flexible Storage – Disk, in-memory, and Amazon S3 backends with a clean interface.
🔍 Rich Validation – Filter by MIME type, file extension, and minimum/maximum size.
🏷 Custom Naming – Define unique filename strategies via a pluggable function.
Concurrent Processing – Processes multiple form fields in parallel using errgroup and sync.Map.
🔒 Bucket Allowlist – Restrict which storage buckets may be used per handler.
🔑 SHA-256 Checksums – Optionally compute and expose upload integrity hashes.
📋 Structured Errors – Type-safe errors (ValidationError, StorageError, etc.) for precise error handling.
📝 Structured Logging – Plug in a log/slog.Logger for lifecycle events.
🛠 Middleware Support – Works with net/http and any compatible router/framework.

Installation

To install GFileMux in your Go project, use go get pointing to this repository.

To get the latest release:

go get github.com/ghulamazad/GFileMux@latest

To lock to a specific version (recommended for production):

go get github.com/ghulamazad/GFileMux@v0.2.0

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "log/slog"
    "net/http"
    "strings"

    "github.com/ghulamazad/GFileMux"
    "github.com/ghulamazad/GFileMux/storage"
    "github.com/google/uuid"
)

func main() {
    // Disk storage — directory is auto-created if it does not exist.
    disk, err := storage.NewDiskStorage("./uploads")
    if err != nil {
        log.Fatalf("storage init: %v", err)
    }
    defer disk.Close()

    handler, err := GFileMux.New(
        GFileMux.WithStorage(disk),
        GFileMux.WithMaxFileSize(10<<20),            // 10 MB body limit
        GFileMux.WithMaxFiles(5),                    // max 5 files per field
        GFileMux.WithAllowedBuckets("images"),       // only "images" bucket allowed
        GFileMux.WithChecksumValidation(true),       // compute SHA-256 per file
        GFileMux.WithLogger(slog.Default()),         // structured logging
        GFileMux.WithFileValidatorFunc(
            GFileMux.ChainValidators(
                GFileMux.ValidateMimeType("image/jpeg", "image/png"),
                GFileMux.ValidateFileExtension(".jpg", ".jpeg", ".png"),
                GFileMux.ValidateMinFileSize(1024), // at least 1 KB
            ),
        ),
        GFileMux.WithFileNameGeneratorFunc(func(original string) string {
            ext := original[strings.LastIndex(original, "."):]
            return uuid.NewString() + ext
        }),
    )
    if err != nil {
        log.Fatalf("handler init: %v", err)
    }

    mux := http.NewServeMux()

    mux.Handle("/upload", handler.Upload("images", "photos")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        files, _ := GFileMux.GetUploadedFilesFromContext(r)
        fmt.Fprintf(w, "uploaded %d file(s)\n", files.Count())
        for _, f := range files.All() {
            path, _ := disk.Path(context.Background(), GFileMux.PathOptions{
                Key:    f.StorageKey,
                Bucket: f.FolderDestination,
            })
            fmt.Fprintf(w, "  %s → %s (sha256: %s)\n", f.OriginalName, path, f.ChecksumSHA256)
        }
    })))

    log.Println("listening on :3300")
    log.Fatal(http.ListenAndServe(":3300", mux))
}

Configuration

handler, err := GFileMux.New(
    GFileMux.WithStorage(disk),
    GFileMux.WithMaxFileSize(10<<20),
    GFileMux.WithMaxFiles(3),
    GFileMux.WithFileValidatorFunc(
        GFileMux.ChainValidators(
            GFileMux.ValidateMimeType("image/jpeg", "image/png"),
            GFileMux.ValidateMinFileSize(512),
        ),
    ),
    GFileMux.WithFileNameGeneratorFunc(func(orig string) string {
        return uuid.NewString() + filepath.Ext(orig)
    }),
    GFileMux.WithLogger(slog.Default()),
    GFileMux.WithChecksumValidation(true),
)

Storage Backends

Disk Storage

Files are stored on the local filesystem. The upload directory (and any bucket subdirectory) is created automatically if it does not exist.

disk, err := storage.NewDiskStorage("./uploads")
if err != nil {
    log.Fatal(err)
}

Passing a bucket to Upload() stores files under <directory>/<bucket>/:

handler.Upload("avatars", "photo") // → ./uploads/avatars/<filename>

Delete a stored file:

err := disk.Delete(ctx, "avatars", "filename.jpg")

Memory Storage

Keeps uploaded files in a thread-safe in-memory map. Primarily useful for testing.

mem := storage.NewMemoryStorage()

// Retrieve raw bytes after upload:
data, err := mem.Get("bucket", "filename.jpg")

// Delete:
err = mem.Delete(ctx, "bucket", "filename.jpg")

S3 Storage

cfg, _ := config.LoadDefaultConfig(context.TODO())

s3Store, err := storage.NewS3FromConfig(cfg, storage.S3Options{
    UsePathStyle: true,
    ACL:          types.ObjectCannedACLPublicRead,
})

// Delete an S3 object:
err = s3Store.Delete(ctx, "my-bucket", "path/to/file.jpg")

Validation

ValidateMimeType

GFileMux.ValidateMimeType("image/jpeg", "image/png", "application/pdf")

ValidateFileExtension

GFileMux.ValidateFileExtension(".jpg", ".jpeg", ".png")

Comparison is case-insensitive (.JPG matches .jpg).

ValidateMinFileSize

GFileMux.ValidateMinFileSize(1024) // reject files smaller than 1 KB

ChainValidators

Combine multiple validators — the first failure short-circuits the chain:

GFileMux.ChainValidators(
    GFileMux.ValidateMimeType("image/jpeg"),
    GFileMux.ValidateFileExtension(".jpg"),
    GFileMux.ValidateMinFileSize(512),
    func(f GFileMux.File) error {
        // custom validation logic
        return nil
    },
)

Options

WithStorage

GFileMux.WithStorage(disk)

WithMaxFileSize

GFileMux.WithMaxFileSize(10 << 20) // 10 MB

WithMaxFiles

Limit the number of files accepted per form field.

GFileMux.WithMaxFiles(5)

WithFileValidatorFunc

GFileMux.WithFileValidatorFunc(GFileMux.ValidateMimeType("image/jpeg"))

WithFileNameGeneratorFunc

GFileMux.WithFileNameGeneratorFunc(func(original string) string {
    return uuid.NewString() + filepath.Ext(original)
})

WithIgnoreNonExistentKey

GFileMux.WithIgnoreNonExistentKey(true) // silently skip missing form fields

WithUploadErrorHandlerFunc

GFileMux.WithUploadErrorHandlerFunc(func(err error) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusBadRequest)
        fmt.Fprintf(w, `{"error": %q}`, err.Error())
    }
})

WithAllowedBuckets

Reject uploads to unlisted bucket names before any I/O occurs.

GFileMux.WithAllowedBuckets("avatars", "documents")

WithLogger

Attach a log/slog logger for structured lifecycle events.

GFileMux.WithLogger(slog.Default())
// or a custom handler:
GFileMux.WithLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

WithChecksumValidation

When enabled, a SHA-256 hex digest is computed for each file and stored in File.ChecksumSHA256.

GFileMux.WithChecksumValidation(true)

API Reference

Upload

Standard middleware for one or more form fields:

handler.Upload("bucket", "field1", "field2")(nextHandler)

UploadSingle

Convenience middleware that enforces exactly one file per field:

handler.UploadSingle("avatars", "photo")(nextHandler)

File

type File struct {
    FieldName         string `json:"field_name,omitempty"`
    OriginalName      string `json:"original_name,omitempty"`
    UploadedFileName  string `json:"uploaded_file_name,omitempty"`
    FolderDestination string `json:"folder_destination,omitempty"`
    StorageKey        string `json:"storage_key,omitempty"`
    MimeType          string `json:"mime_type,omitempty"`
    Size              int64  `json:"size,omitempty"`
    ChecksumSHA256    string `json:"checksum_sha256,omitempty"`
}

Files helpers

files, _ := GFileMux.GetUploadedFilesFromContext(r)

files.All()     // []File — flat slice across all fields
files.Count()   // int   — total count across all fields

// By field:
byField, _ := GFileMux.GetFilesByFieldFromContext(r, "photos")

Storage Interface

type Storage interface {
    Upload(ctx context.Context, reader io.Reader, options *UploadFileOptions) (*UploadedFileMetadata, error)
    Path(ctx context.Context, options PathOptions) (string, error)
    Delete(ctx context.Context, bucket, key string) error
    io.Closer
}

Error Types

Use errors.As to distinguish error categories:

var ve *GFileMux.ValidationError
var se *GFileMux.StorageError
var mfe *GFileMux.MaxFilesError
var sizeErr *GFileMux.SizeError

switch {
case errors.As(err, &ve):
    // field validation failed
case errors.As(err, &mfe):
    // too many files
case errors.As(err, &sizeErr):
    // body too large
case errors.As(err, &se):
    // backend I/O error (se.Backend, se.Op, se.Unwrap())
}

License

This project is licensed under the MIT License. See LICENSE for details.

About

GFileMux is a fast, lightweight Go package for handling multipart file uploads. Inspired by Multer, it offers flexible storage options, middleware-style handling, and seamless processing with minimal overhead. Compatible with any Go HTTP framework, GFileMux simplifies file uploads for your web apps.

Topics

Resources

License

Contributing

Stars

3 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages