* feat: cloud Mount() per HIP-0106
Adds pkg/vfs.Mount(*zip.App, cloud.Deps) error so the unified Hanzo
Cloud binary can blank-import and register VFS alongside every
other Hanzo subsystem.
VFS has no public HTTP surface today — it's an S3-backed virtual
block filesystem driven through FUSE/CLI/sidecar. Mount() exposes
liveness/readiness probes plus a minimal block-CRUD HTTP API so
co-resident subsystems can talk to VFS without falling back to the
CLI:
- GET /v1/vfs/health — always 200
- GET /v1/vfs/readyz — 200 only when an instance is
attached via vfs.SetInstance
- PUT /v1/vfs/blocks — payload upload
- GET /v1/vfs/blocks/:id — payload download
- DELETE /v1/vfs/blocks/:id — backend delete
- GET /v1/vfs/stats — cache/backend counters
cmd/vfsd is a HIP-0106 thin shim that wires backend + age keys from
env (VFSD_BACKEND, VFSD_AGE_KEY) and listens on cloud.Config.ListenAddr.
The existing cmd/vfs CLI (put/get/stats/mount) is untouched.
init() registers with cloud.Register("vfs", 20, …). schema/vfs.zap
adds the minimal Health + Block ZAP interface; full multi-block
File surface lands in follow-up PRs.
Tests:
- pkg/vfs: TestMount_HealthReadyz + TestMount_PutGetRoundtrip green
- existing TestRoundTrip + TestStats unchanged
* refactor: collapse pkg/vfs/ → root for clean import path
Subsystem code lives in package vfs at repo root. Importers now use
`github.com/hanzoai/vfs` (no /pkg/vfs/ nesting). Standalone shim moved
to cmd/vfs/ where applicable.
Per Hanzo Go-stdlib pattern: repo IS the package.
* refactor: flatten import path — github.com/hanzoai/cloud (drop /pkg/cloud)
74 lines
2.4 KiB
Go
74 lines
2.4 KiB
Go
// Package vfs is the top-level virtual filesystem implementation.
|
|
//
|
|
// Block layer: 4 KiB pages, content-addressable via blake3 (256-bit).
|
|
// Block IDs are the lowercase hex of the blake3 digest of the
|
|
// post-encryption ciphertext, so identical plaintext blocks dedupe IFF
|
|
// they share the same recipient set + ephemeral key. (For practical
|
|
// dedup across a fleet, derive the encryption key deterministically
|
|
// from a domain salt — see BlockKeyMode.)
|
|
package vfs
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"github.com/zeebo/blake3"
|
|
)
|
|
|
|
// BlockSize is the canonical page size. Bumping requires a new on-disk
|
|
// format magic byte; do not change without coordination.
|
|
const BlockSize = 4096
|
|
|
|
// BlockID is the content hash of an encrypted block (blake3-256, hex).
|
|
type BlockID string
|
|
|
|
// HashBlock returns the BlockID for the given ciphertext bytes.
|
|
func HashBlock(ciphertext []byte) BlockID {
|
|
sum := blake3.Sum256(ciphertext)
|
|
return BlockID(hex.EncodeToString(sum[:]))
|
|
}
|
|
|
|
// Verify checks that the given ciphertext hashes to the expected ID.
|
|
// Returns nil on match, an error on mismatch. Constant-time compare
|
|
// guards against timing leaks on the hash itself (defense-in-depth;
|
|
// blake3 is already collision-resistant).
|
|
func (id BlockID) Verify(ciphertext []byte) error {
|
|
got := HashBlock(ciphertext)
|
|
if subtle.ConstantTimeCompare([]byte(id), []byte(got)) != 1 {
|
|
return fmt.Errorf("block: hash mismatch: have %s, computed %s", id, got)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Path returns the canonical backend key for a block ID. Pattern:
|
|
//
|
|
// blocks/<first-2-hex>/<full-hash>.zap.age
|
|
//
|
|
// The 2-hex shard prefix (256 fan-out) keeps single-directory listings
|
|
// tractable for object stores that paginate.
|
|
func (id BlockID) Path() string {
|
|
if len(id) < 2 {
|
|
return "blocks/" + string(id) + ".zap.age"
|
|
}
|
|
return "blocks/" + string(id[:2]) + "/" + string(id) + ".zap.age"
|
|
}
|
|
|
|
// Pad zero-pads or truncates a payload to BlockSize. Plaintext blocks
|
|
// are always exactly BlockSize bytes before encryption so that block
|
|
// boundaries don't leak file lengths to the backend (each block in
|
|
// isolation looks identical in size after age framing — a few hundred
|
|
// bytes of header + tag + BlockSize body).
|
|
func Pad(plaintext []byte) []byte {
|
|
if len(plaintext) == BlockSize {
|
|
return plaintext
|
|
}
|
|
out := make([]byte, BlockSize)
|
|
if len(plaintext) < BlockSize {
|
|
copy(out, plaintext)
|
|
return out
|
|
}
|
|
copy(out, plaintext[:BlockSize])
|
|
return out
|
|
}
|