Memory and local disk
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"))
| Field | Meaning |
|---|---|
Root | the directory objects are read from and written to; every path is resolved under it, and ValidPath rejects anything that would escape it |
PublicBaseURL | base for Disk.URL: a reverse proxy or CDN in front of Root, if content is publicly served |
BaseURL | base URL the local driver's own signed links point at, i.e. where you mount its Handler |
Secret | HMAC 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
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))
}