File Browser
Use Trove's VFS layer to browse, navigate, and serve files with a familiar filesystem API.
Trove's VFS (Virtual Filesystem) package provides a hierarchical file-system interface over flat object storage keys. It supports creating, reading, writing, deleting, and walking files and directories with an API modeled after Go's os and io/fs packages.
Creating a VFS Instance
There are two ways to create a VFS:
import (
"context"
"fmt"
"io"
"log"
"github.com/xraph/trove"
"github.com/xraph/trove/vfs"
"github.com/xraph/trove/drivers/memdriver"
)
ctx := context.Background()
drv := memdriver.New()
t, _ := trove.Open(drv, trove.WithVFS("uploads"))
defer t.Close(ctx)
t.CreateBucket(ctx, "uploads")
t.CreateBucket(ctx, "media")
// Option 1: Use t.VFS() for the default VFS bucket (set via WithVFS).
fs := t.VFS()
// Option 2: Specify a bucket explicitly.
mediaFS := t.VFS("media")Stat: Check File or Directory Info
Stat returns a *vfs.FileInfo with name, size, modification time, and directory status.
// Create a file first.
f, _ := fs.Create(ctx, "docs/readme.md")
f.Write([]byte("# My Project"))
f.Close()
// Stat the file.
info, err := fs.Stat(ctx, "docs/readme.md")
if err != nil {
log.Fatal(err)
}
fmt.Println(info.Name()) // "readme.md"
fmt.Println(info.Size()) // 13
fmt.Println(info.IsDir()) // false
fmt.Println(info.ModTime()) // time.Time of last modificationFileInfo implements Go's standard fs.FileInfo interface, so it works with any code expecting that type.
ReadDir: List Directory Contents
ReadDir returns a []vfs.DirEntry slice for the given directory path.
// Create some files.
for _, name := range []string{"docs/a.txt", "docs/b.txt", "docs/sub/c.txt"} {
f, _ := fs.Create(ctx, name)
f.Write([]byte("content"))
f.Close()
}
// List the "docs" directory.
entries, err := fs.ReadDir(ctx, "docs")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Printf("%-20s dir=%v\n", entry.Name(), entry.IsDir())
}
// Output:
// a.txt dir=false
// b.txt dir=false
// sub dir=trueEach DirEntry implements Go's fs.DirEntry interface with Name(), IsDir(), Type(), and Info() methods.
Walk: Recursive Traversal
Walk traverses the file tree rooted at the given path, calling your function for each file and directory.
err := fs.Walk(ctx, "", func(path string, info *vfs.FileInfo, err error) error {
if err != nil {
return err // propagate errors
}
prefix := ""
if info.IsDir() {
prefix = "[DIR] "
} else {
prefix = "[FILE] "
}
fmt.Printf("%s%s (%d bytes)\n", prefix, path, info.Size())
return nil
})
if err != nil {
log.Fatal(err)
}Skipping Directories and Stopping Early
fs.Walk(ctx, "", func(path string, info *vfs.FileInfo, err error) error {
if err != nil {
return err
}
// Skip the "temp" directory entirely.
if info.IsDir() && info.Name() == "temp" {
return vfs.SkipDir
}
// Stop walking after finding a specific file.
if info.Name() == "target.txt" {
fmt.Println("Found:", path)
return vfs.SkipAll
}
return nil
})Open: Read File Content
Open returns a file handle for reading. Always close the handle when done.
f, err := fs.Open(ctx, "docs/readme.md")
if err != nil {
log.Fatal(err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Content: %s\n", data)Create: Write a New File
Create returns a writable file handle. Content is flushed to storage on Close().
f, err := fs.Create(ctx, "reports/q4-2024.csv")
if err != nil {
log.Fatal(err)
}
f.Write([]byte("name,revenue\n"))
f.Write([]byte("product-a,1500\n"))
f.Write([]byte("product-b,2300\n"))
// Close flushes to the underlying storage driver.
if err := f.Close(); err != nil {
log.Fatal(err)
}Mkdir: Create Directories
Mkdir creates a directory marker (a zero-length object with a trailing "/").
if err := fs.Mkdir(ctx, "images"); err != nil {
log.Fatal(err)
}
if err := fs.Mkdir(ctx, "images/thumbnails"); err != nil {
log.Fatal(err)
}Remove and RemoveAll
// Remove a single file.
if err := fs.Remove(ctx, "reports/old.csv"); err != nil {
log.Fatal(err)
}
// Remove a directory and all its contents recursively.
if err := fs.RemoveAll(ctx, "temp"); err != nil {
log.Fatal(err)
}Rename: Move Files
Rename performs a copy-then-delete operation to move or rename a file.
if err := fs.Rename(ctx, "docs/readme.md", "docs/README.md"); err != nil {
log.Fatal(err)
}
// Move to a different directory.
if err := fs.Rename(ctx, "docs/old-spec.txt", "archive/old-spec.txt"); err != nil {
log.Fatal(err)
}SetMetadata and GetMetadata
Attach and retrieve custom key-value metadata on files.
// Set metadata.
err := fs.SetMetadata(ctx, "reports/q4-2024.csv", map[string]string{
"author": "alice",
"department": "finance",
"reviewed": "true",
})
if err != nil {
log.Fatal(err)
}
// Get metadata.
meta, err := fs.GetMetadata(ctx, "reports/q4-2024.csv")
if err != nil {
log.Fatal(err)
}
for k, v := range meta {
fmt.Printf(" %s = %s\n", k, v)
}Serving Files over HTTP
Use the IOFS adapter to serve VFS files over HTTP with Go's standard library.
import (
"net/http"
"github.com/xraph/trove/vfs"
)
// Wrap the VFS in an io/fs.FS adapter.
iofs := vfs.NewIOFS(t.VFS(), ctx)
// Serve files at /files/*.
http.Handle("/files/", http.StripPrefix("/files/", http.FileServer(http.FS(iofs))))
// Or parse templates from storage.
tmpl, err := template.ParseFS(iofs, "templates/*.html")
if err != nil {
log.Fatal(err)
}
log.Println("Serving on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))The IOFS adapter implements fs.FS and returns fs.ReadDirFile for directories, making it compatible with http.FileServer, template.ParseFS, embed-style patterns, and any code that accepts fs.FS.
How VFS Maps to Object Keys
VFS translates between hierarchical paths and flat object keys:
| VFS Operation | Object Key |
|---|---|
Open("docs/readme.md") | docs/readme.md |
Mkdir("images") | images/ (zero-length marker) |
ReadDir("docs") | List(prefix="docs/", delimiter="/") |
Walk("") | Recursive List + ReadDir |
Directories are detected by:
- Keys ending with
/(explicit markers fromMkdir) - Keys containing
/in the relative path (implicit directories)
Multiple VFS Views
Create separate VFS views for different buckets to organize content:
// Default VFS bucket (set via WithVFS option).
defaultFS := t.VFS()
// Dedicated views for different content types.
mediaFS := t.VFS("media")
logsFS := t.VFS("logs")
archiveFS := t.VFS("archive")
// Each view operates independently on its bucket.
mediaFS.Create(ctx, "photos/sunset.jpg")
logsFS.Create(ctx, "2024/01/app.log")