Testing
storage/storagetest is a shared conformance test suite: the same set of behavioral tests run against every Driver implementation, built-in or your own, so "does this driver actually behave like the others" is a question you answer by running one function, not by hand-writing a parallel test file per driver.
Running it against a Driver
import (
"testing"
"github.com/gp-system/storage/storagetest"
)
func TestMyDriver(t *testing.T) {
storagetest.Run(t, func(t *testing.T) storage.Driver {
dir := t.TempDir()
driver, err := local.New(local.Config{Root: dir, Secret: "test-secret"})
if err != nil {
t.Fatal(err)
}
return driver
}, storagetest.Options{})
}
Run(t, factory, opts) takes a fresh-driver factory, called once per subtest so tests never share state, and a storagetest.Options. It then exercises the full Driver surface: put and read round-trips, range reads, stat, list and pagination, copy, delete idempotency, ValidPath rejection of traversal attempts, and, where the driver supports it, temporary URLs. A driver that returns ErrUnsupported for an optional capability is treated as a valid, if less capable, implementation, not a failure: storagetest checks that the error is the right one, not that every driver supports everything.
Options for drivers that don't preserve everything
type Options struct {
PersistsContentType bool
PersistsMetadata bool
}
Not every backend round-trips every piece of metadata; a minimal driver (say, one backed by a bare key/value store with no attribute support) might not preserve PutOptions.ContentType or other metadata across a Put/Stat cycle. Rather than forcing every driver to fake support it doesn't have, storagetest.Options lets you say so up front: leave PersistsContentType/PersistsMetadata at their zero value (false) and the suite skips the assertions that would otherwise fail on a driver that's honest about its limits.
Writing a custom Driver
The Driver interface (see Overview) is the whole surface a new backend needs to cover. A minimal skeleton:
type myDriver struct {
name string
// backend-specific fields
}
func (d *myDriver) Name() string { return d.name }
func (d *myDriver) Put(ctx context.Context, path string, r io.Reader, opts storage.PutOptions) error {
if !storage.ValidPath(path) {
return storage.ErrInvalidPath
}
// write r to the backend at path
return nil
}
func (d *myDriver) Reader(ctx context.Context, path string) (io.ReadCloser, error) {
// return the object at path, or storage.ErrNotExist if it's missing
}
func (d *myDriver) RangeReader(ctx context.Context, path string, offset, length int64) (io.ReadCloser, error) {
// a byte-range read; length < 0 means "to the end"
}
func (d *myDriver) Stat(ctx context.Context, path string) (storage.FileInfo, error) { /* ... */ }
func (d *myDriver) List(ctx context.Context, opts storage.ListOptions) (storage.ListPage, error) { /* ... */ }
func (d *myDriver) Copy(ctx context.Context, src, dst string) error { /* ... */ }
func (d *myDriver) Delete(ctx context.Context, path string) error { /* idempotent */ }
func (d *myDriver) URL(ctx context.Context, path string) (string, error) { /* ... */ }
func (d *myDriver) TemporaryURL(ctx context.Context, path string, opts storage.TemporaryURLOptions) (string, error) {
return "", storage.ErrUnsupported // if the backend can't sign URLs
}
func (d *myDriver) TemporaryUploadURL(ctx context.Context, path string, opts storage.TemporaryUploadURLOptions) (storage.UploadURL, error) {
return storage.UploadURL{}, storage.ErrUnsupported
}
A few things worth calling out while writing one:
- Call
storage.ValidPathat the top of every method that takes a path and touches the backend; it's the one thing every driver is expected to share. Deletemust be idempotent: deleting a path that's already gone is not an error.- If a capability genuinely doesn't apply to the backend, return
storage.ErrUnsupportedrather than a half-working approximation; callers are expected to handle it (seeDisk.ProvidesTemporaryURLs). - Implement the optional
Moverinterface only if the backend has a native, non-copy move; otherwise leave it unimplemented andDisk.Movefalls back toCopy+Deleteon your driver's behalf. - Run it through
storagetest.Runbefore wiring it into anything real. It's the fastest way to find a corner (an off-by-one in a range read, aListpage that doesn't paginate correctly) that a hand-written smoke test wouldn't catch.
Related pages
- Overview: the full
Drivercontract and theMoverinterface. - Disk API: the methods that end up calling into your driver.
- Memory and local disk: read these two drivers' source for a worked example of the contract.
- S3: the third built-in driver, and the one with the most backend-specific logic.