storage

Memory and local disk

The two drivers that ship in the root storage module, for tests and single-server deployments.

Two Driver implementations ship inside the root storage module, no extra dependency beyond it: storage/memory, an in-process fake for tests, and storage/local, a filesystem-backed driver good enough for a single-server deployment or local development. Both satisfy the same Driver contract as the S3-compatible driver, so code written against Disk doesn't change when you swap between them.

storage/memory: for tests

import "github.com/gp-system/storage/memory"

driver := memory.New()
driver := memory.New(memory.WithBaseURL("https://cdn.test"))
disk := storage.NewDisk(driver, storage.WithName("test"))

memory.New returns a Driver backed by an in-process map: no filesystem, no network, no container. WithBaseURL sets the base that URL builds links from (default: an internal placeholder scheme); without it, URL still works, it just won't look like a real link. This is the driver of choice for a service-layer unit test:

func TestUploadStoresImageAndURL(t *testing.T) {
    disk := storage.NewDisk(memory.New(memory.WithBaseURL("https://cdn.test")))
    svc := service.NewProductImageService(disk, repo)

    url, err := svc.Upload(ctx, "p42", strings.NewReader("png-bytes"), storage.PutOptions{ContentType: "image/png"})

    require.NoError(t, err)
    require.Equal(t, "https://cdn.test/products/p42/image", url)
    require.True(t, disk.Exists(ctx, "products/p42/image"))
}

If you're writing a Driver of your own rather than using either of these two, run it through storage/storagetest to confirm it behaves the same way.

storage/local: filesystem-backed, with signed URLs

import "github.com/gp-system/storage/local"

cfg := local.Config{
    Root:          "/var/data/uploads",
    PublicBaseURL: "https://cdn.example.com",
    BaseURL:       "https://api.example.com/files",
    Secret:        os.Getenv("STORAGE_LOCAL_SECRET"),
}
driver, err := local.New(cfg)
disk := storage.NewDisk(driver, storage.WithName("uploads"))
FieldMeaning
Rootthe directory objects are read from and written to; every path is resolved under it, and ValidPath rejects anything that would escape it
PublicBaseURLbase for Disk.URL: a reverse proxy or CDN in front of Root, if content is publicly served
BaseURLbase URL the local driver's own signed links point at, i.e. where you mount its Handler
SecretHMAC key signed URLs are generated and verified with

The signed-URL Handler

storage/local doesn't just generate signed URLs, it also serves them: driver.Handler() returns an http.Handler you mount at the path implied by BaseURL, and it's what actually enforces the signature and expiry on incoming requests:

mux := http.NewServeMux()
mux.Handle("/files/", http.StripPrefix("/files/", driver.Handler()))

A request without a valid, unexpired signature is rejected before the filesystem is touched. This is what makes TemporaryURL/TemporaryUploadURL meaningful on a driver that has no cloud infrastructure behind it: the HMAC signature (over the path, an expiry timestamp, and Secret) is the entire access-control mechanism, so treat Secret like any other credential (an env variable, never committed) and rotate it if it leaks.

Standalone example

main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "strings"
    "time"

    "github.com/gp-system/storage"
    "github.com/gp-system/storage/local"
)

func main() {
    driver, err := local.New(local.Config{
        Root:          "./data",
        PublicBaseURL: "http://localhost:8080/files",
        BaseURL:       "http://localhost:8080/files",
        Secret:        os.Getenv("STORAGE_LOCAL_SECRET"),
    })
    if err != nil {
        log.Fatal(err)
    }
    disk := storage.NewDisk(driver, storage.WithName("local"))

    ctx := context.Background()
    if err := disk.Put(ctx, "hello.txt", strings.NewReader("hello, disk"), storage.PutOptions{}); err != nil {
        log.Fatal(err)
    }

    signed, err := disk.TemporaryURL(ctx, "hello.txt", storage.TemporaryURLOptions{TTL: 5 * time.Minute})
    if err != nil {
        log.Fatal(err)
    }
    log.Println("signed link:", signed)

    mux := http.NewServeMux()
    mux.Handle("/files/", http.StripPrefix("/files/", driver.Handler()))
    log.Fatal(http.ListenAndServe(":8080", mux))
}
  • Overview: the Driver/Disk/Manager model, and the decorators these drivers compose with.
  • Disk API: the full method set exposed through Disk.
  • S3: the driver to switch to for a production, multi-instance deployment.
  • Testing: the conformance suite both of these drivers pass.
Copyright © 2026