mirror of
https://github.com/hanzoai/vfs.git
synced 2026-10-30 18:18:21 +00:00
Phase 0.1.0 — production-shaped scaffold for the S3-backed virtual block filesystem. Stateful services (IAM, BD, ATS, TA, KMS, onyxd, lqd snapshots) get unlimited disk via S3 with PQ-encrypted blocks; the hot tier is a configurable LRU cache. Companion to hanzo/replicate (WAL streaming) — `replicate` is file-level granularity, `vfs` is block-level (4 KiB pages, content-addressable via blake3-256). Included -------- - pkg/backend: pluggable Backend interface + Open(URL) dispatch. - pkg/backend/file: file:// backend (atomic put via temp+rename, path traversal blocked, recursive list with prefix filter). - pkg/backend/s3: AWS SDK v2 S3 backend with `?endpoint=` override for S3-compat (R2, MinIO, Hanzo Storage). NotFound → ErrNotFound. - pkg/vfs/block: blake3-256 hashing → BlockID, 2-hex shard prefix for the backend key, BlockSize=4096, zero-pad to BlockSize before encryption (block boundaries don't leak file lengths). - pkg/vfs/crypto: luxfi/age v1.5.0 wrapper. Recipients (write-side) + Identities (read-side); hybrid X25519 + ML-KEM-768 supported via the same recipient list. - pkg/vfs/cache: in-memory LRU bound by total bytes; touch-promotes on read; eviction on Put when over cap. NVMe write-back tier lands in 0.2.0. - pkg/vfs: top-level handle. PutBlock/GetBlock with cache hit-path + content-hash integrity check on read. Stats() surfaces cache + backend metadata. - pkg/mount: fuse_stub.go (default build) + fuse_unix.go (-tags fuse). Real FUSE wiring lands in 0.2.0; current stub returns a build-tag error pointing at `make build-fuse`. - cmd/vfs: cobra CLI (put/get/stats/mount). Backend registered via blank-import; --age-recipient repeatable; --age-key reads identity PEM from file or VFS_AGE_KEY env. Tests ----- - block: hash determinism, path shape, verify match/mismatch, pad. - cache: LRU eviction, touch-promotion, basic ops. - file backend: put/get/delete/stat/list, path-traversal block. - vfs roundtrip: encrypt → backend → decrypt → byte-equal plaintext, zero-pad correctness; multiple writes of identical plaintext both round-trip; stats reflect insertions. CI / build ---------- - .github/workflows/build.yml: vet + race-tests on Go 1.24, amd64 binary upload, GHCR image build + push on main / v* tags. - Dockerfile: distroless static, CGO_ENABLED=0, ARG VERSION ldflag. - Makefile: build / build-fuse / test / fmt / vet / check / image / install. VERSION-pinned. Roadmap ------- - 0.2.0: bazil.org/fuse mount, NVMe disk cache layer - 0.3.0: gcs + azureblob backends - 0.4.0: K8s sidecar mode + Helm chart - 0.5.0: WASI guest mode - 1.0.0: chunked uploads, multipart resume, GC for orphan blocks, metrics + SLO targets
116 lines
4.0 KiB
Go
116 lines
4.0 KiB
Go
// Package backend defines the object-store interface that VFS writes
|
|
// encrypted blocks against. Implementations cover S3, GCS, Azure Blob,
|
|
// and a `file://` adapter for local dev. The interface is intentionally
|
|
// minimal: VFS does its own content addressing and encryption above
|
|
// this layer; the backend is just a typed key→bytes store.
|
|
package backend
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
)
|
|
|
|
// ErrNotFound is returned by Get when the requested key is absent.
|
|
var ErrNotFound = errors.New("backend: key not found")
|
|
|
|
// Backend is the contract every object-store adapter implements.
|
|
//
|
|
// Keys are arbitrary `/`-delimited strings; VFS uses
|
|
// `blocks/<blake3-prefix>/<full-hash>.zap.age` for block storage.
|
|
//
|
|
// Implementations MUST be safe for concurrent use. Get/Put/Delete are
|
|
// idempotent — repeating the same operation is allowed and SHOULD NOT
|
|
// surface as an error.
|
|
type Backend interface {
|
|
// Get returns the bytes stored at key. If the key does not exist,
|
|
// the returned error wraps ErrNotFound (errors.Is(err, ErrNotFound)).
|
|
Get(ctx context.Context, key string) ([]byte, error)
|
|
|
|
// Put writes data at key. Overwriting is allowed.
|
|
Put(ctx context.Context, key string, data []byte) error
|
|
|
|
// Delete removes the key. Removing a non-existent key MUST NOT error.
|
|
Delete(ctx context.Context, key string) error
|
|
|
|
// List returns keys with the given prefix. The returned channel is
|
|
// closed when the listing completes; on error the channel is closed
|
|
// after the error is sent on errCh.
|
|
List(ctx context.Context, prefix string) (keys <-chan string, errCh <-chan error)
|
|
|
|
// Stat returns the size in bytes for key without fetching the body.
|
|
// ErrNotFound is returned if the key is absent.
|
|
Stat(ctx context.Context, key string) (size int64, err error)
|
|
|
|
// Close releases any resources (HTTP connections, file handles).
|
|
// Safe to call multiple times.
|
|
Close() error
|
|
|
|
// String returns a stable, human-readable description of the
|
|
// backend (e.g. "s3://bucket/prefix").
|
|
String() string
|
|
}
|
|
|
|
// Reader returns an io.ReadCloser when streaming is preferred over
|
|
// in-memory Get. Optional — implementations may return ErrNotImplemented.
|
|
type Reader interface {
|
|
GetReader(ctx context.Context, key string) (io.ReadCloser, error)
|
|
}
|
|
|
|
// Writer returns an io.WriteCloser for streaming uploads. Optional.
|
|
type Writer interface {
|
|
PutWriter(ctx context.Context, key string) (io.WriteCloser, error)
|
|
}
|
|
|
|
// ErrNotImplemented signals that a backend doesn't support the optional
|
|
// streaming interface for the given operation.
|
|
var ErrNotImplemented = errors.New("backend: operation not implemented")
|
|
|
|
// Open dispatches a backend URL to the right implementation. Recognised
|
|
// schemes:
|
|
//
|
|
// file://path — local filesystem (dev only)
|
|
// s3://bucket/prefix — AWS S3 / S3-compatible (R2, Hanzo Storage, MinIO)
|
|
// gcs://bucket/prefix — Google Cloud Storage (TODO)
|
|
// azureblob://... — Azure Blob (TODO)
|
|
//
|
|
// The opener function is registered by each backend package's init().
|
|
type Opener func(ctx context.Context, u *url.URL) (Backend, error)
|
|
|
|
var openers = map[string]Opener{}
|
|
|
|
// Register attaches an Opener for a URL scheme. Called from backend
|
|
// implementations' init().
|
|
func Register(scheme string, opener Opener) {
|
|
if scheme == "" || opener == nil {
|
|
panic("backend.Register: scheme and opener are required")
|
|
}
|
|
openers[scheme] = opener
|
|
}
|
|
|
|
// Open parses the backend URL and dispatches to the registered opener.
|
|
func Open(ctx context.Context, raw string) (Backend, error) {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("backend.Open: parse %q: %w", raw, err)
|
|
}
|
|
if u.Scheme == "" {
|
|
return nil, fmt.Errorf("backend.Open: missing scheme in %q", raw)
|
|
}
|
|
opener, ok := openers[u.Scheme]
|
|
if !ok {
|
|
return nil, fmt.Errorf("backend.Open: no opener registered for %q (have: %v)", u.Scheme, registeredSchemes())
|
|
}
|
|
return opener(ctx, u)
|
|
}
|
|
|
|
func registeredSchemes() []string {
|
|
out := make([]string, 0, len(openers))
|
|
for k := range openers {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|