Trove

Getting Started

Install Trove, create a storage driver, store and retrieve objects in under 5 minutes.

Installation

Install the core Trove package:

go get github.com/xraph/trove

Then install one or more storage drivers:

go get github.com/xraph/trove/drivers/localdriver
go get github.com/xraph/trove/drivers/memdriver
go get github.com/xraph/trove/drivers/s3driver

Create a Driver

Every Trove instance requires a storage driver. Drivers implement driver.Driver and handle the actual backend I/O.

import "github.com/xraph/trove/drivers/localdriver"

drv := localdriver.New()
err := drv.Open(ctx, "file:///var/data/storage")

The local driver stores objects as files on disk. Buckets map to subdirectories under the root path. Metadata is stored in sidecar .meta.json files.

import "github.com/xraph/trove/drivers/memdriver"

drv := memdriver.New()
// No Open() call required -- memdriver is ready immediately.

The memory driver stores all data in-process using Go maps. It is not persistent and is intended for unit tests and development.

Open a Trove Instance

Pass the driver to trove.Open to create a Trove handle. All subsequent operations flow through this handle.

import "github.com/xraph/trove"

t, err := trove.Open(drv)
if err != nil {
    log.Fatal(err)
}
defer t.Close(ctx)

Open accepts functional options for configuration:

t, err := trove.Open(drv,
    trove.WithDefaultBucket("primary"),
    trove.WithChunkSize(8 * 1024 * 1024),  // 8 MB streaming chunks
    trove.WithPoolSize(32),                 // max concurrent streams
)

Create a Bucket

Buckets are top-level namespaces that group objects. Create one before storing objects:

err := t.CreateBucket(ctx, "uploads")

If you set a default bucket with trove.WithDefaultBucket("uploads"), you can pass an empty string for the bucket parameter in subsequent operations and Trove will use the default.

Store an Object

Put stores an object from any io.Reader. It returns *driver.ObjectInfo with the stored object's metadata.

import (
    "strings"
    "github.com/xraph/trove/driver"
)

info, err := t.Put(ctx, "uploads", "photos/cat.jpg", file,
    driver.WithContentType("image/jpeg"),
    driver.WithMetadata(map[string]string{
        "author":      "alice",
        "uploaded-by": "web-app",
    }),
)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("stored %s (%d bytes, etag=%s)\n", info.Key, info.Size, info.ETag)

For simple text content:

info, err := t.Put(ctx, "uploads", "notes/hello.txt",
    strings.NewReader("Hello, Trove!"),
    driver.WithContentType("text/plain"),
)

Retrieve an Object

Get returns an *driver.ObjectReader that combines a ReadCloser for the content with an Info field containing metadata.

obj, err := t.Get(ctx, "uploads", "photos/cat.jpg")
if err != nil {
    log.Fatal(err)
}
defer obj.Close()

// Access metadata via obj.Info
fmt.Printf("content-type: %s, size: %d\n", obj.Info.ContentType, obj.Info.Size)

// Read content
data, err := io.ReadAll(obj)
if err != nil {
    log.Fatal(err)
}

List Objects

List returns an *driver.ObjectIterator for cursor-based iteration. Use WithPrefix to filter by key prefix.

Collect all results at once:

iter, err := t.List(ctx, "uploads", driver.WithPrefix("photos/"))
if err != nil {
    log.Fatal(err)
}
defer iter.Close()

objects, err := iter.All(ctx)
if err != nil {
    log.Fatal(err)
}

for _, obj := range objects {
    fmt.Printf("  %s  %d bytes  %s\n", obj.Key, obj.Size, obj.ContentType)
}

Iterate one at a time:

iter, err := t.List(ctx, "uploads", driver.WithPrefix("photos/"))
if err != nil {
    log.Fatal(err)
}
defer iter.Close()

for {
    obj, err := iter.Next(ctx)
    if errors.Is(err, io.EOF) {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("  %s  %d bytes\n", obj.Key, obj.Size)
}

Additional list options:

OptionDescription
driver.WithPrefix(p)Filter objects by key prefix
driver.WithDelimiter(d)Group keys by delimiter (e.g. "/")
driver.WithMaxKeys(n)Maximum objects per page (default: 1000)
driver.WithCursor(c)Pagination cursor from a previous call

Delete an Object

err := t.Delete(ctx, "uploads", "photos/cat.jpg")

Check Metadata

Head returns object metadata without downloading the content. This is useful for checking existence, size, or content type before fetching.

info, err := t.Head(ctx, "uploads", "photos/cat.jpg")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("key:          %s\n", info.Key)
fmt.Printf("size:         %d\n", info.Size)
fmt.Printf("content-type: %s\n", info.ContentType)
fmt.Printf("etag:         %s\n", info.ETag)
fmt.Printf("modified:     %s\n", info.LastModified)
fmt.Printf("metadata:     %v\n", info.Metadata)

The ObjectInfo struct contains:

FieldTypeDescription
KeystringObject key
Sizeint64Content length in bytes
ContentTypestringMIME type
ETagstringEntity tag for conditional requests
LastModifiedtime.TimeLast modification timestamp
Metadatamap[string]stringUser-defined metadata
VersionIDstringVersion identifier (if versioning enabled)
StorageClassstringStorage tier (e.g. "STANDARD", "GLACIER")

Copy an Object

Copy duplicates an object within the same bucket or across buckets. The source is not modified.

info, err := t.Copy(ctx,
    "uploads", "photos/cat.jpg",       // source
    "archive", "2024/photos/cat.jpg",  // destination
)

Multi-Backend Routing

Trove can route objects to different storage backends based on bucket, key pattern, or custom logic.

import (
    "github.com/xraph/trove"
    "github.com/xraph/trove/drivers/localdriver"
    "github.com/xraph/trove/drivers/s3driver"
)

primary := localdriver.New()
primary.Open(ctx, "file:///var/data/hot")

archive := s3driver.New()
archive.Open(ctx, "s3://my-archive-bucket?region=us-east-1")

t, err := trove.Open(primary,
    // Register a named backend.
    trove.WithBackend("archive", archive),

    // Route by key pattern (filepath.Match syntax).
    trove.WithRoute("*.log", "archive"),
    trove.WithRoute("backups/*", "archive"),

    // Route by custom function.
    trove.WithRouteFunc(func(bucket, key string) string {
        if bucket == "compliance" {
            return "archive"
        }
        return "" // fall through to default
    }),
)

Route resolution priority:

  1. Route functions (highest)
  2. Pattern routes
  3. Default driver (fallback)

Add Middleware

Middleware intercepts the data stream on read and/or write. Register middleware for encryption, compression, or custom processing.

import (
    "github.com/xraph/trove/middleware"
    "github.com/xraph/trove/middleware/encrypt"
    "github.com/xraph/trove/middleware/compress"
)

// Encrypt all objects globally.
t.UseMiddleware(middleware.Registration{
    Middleware: encrypt.New(encrypt.WithKeyProvider(
        encrypt.NewStaticKeyProvider(myAES256Key),
    )),
})

// Compress only text files in the "logs" bucket.
t.UseMiddleware(middleware.Registration{
    Middleware: compress.New(),
    Scope:     middleware.And(
        middleware.ForBuckets("logs"),
        middleware.ForKeys("*.txt", "*.log", "*.json"),
    ),
    Priority:  10,
})

Next Steps

  • Architecture -- Understand the storage pipeline, routing, and middleware architecture
  • Drivers -- Full reference for all storage backend drivers
  • Middleware -- Encryption, compression, deduplication, scanning, and custom middleware
  • Streaming -- Chunked transfers, backpressure, resumable uploads, and progress hooks
  • Content-Addressable Storage -- Hash-based deduplication and garbage collection
  • Virtual Filesystem -- io/fs-compatible filesystem abstraction over object storage

On this page