Skip to content

storage: add read-only Bucket backed by fs.FS#96

Merged
achille-roussel merged 1 commit into
mainfrom
bucket-from-fs
Jun 20, 2026
Merged

storage: add read-only Bucket backed by fs.FS#96
achille-roussel merged 1 commit into
mainfrom
bucket-from-fs

Conversation

@achille-roussel

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only storage.Bucket implementation backed by a standard fs.FS.

func NewBucketFromFS(fsys fs.FS) Bucket
func NewBucketFromNewFS(newFS func(context.Context) fs.FS) Bucket

NewBucketFromNewFS calls the constructor once per Bucket operation, passing that call's ctx, so the underlying fs.FS can be derived from the caller's context (request, transaction, tenant, …) instead of being fixed at construction time. NewBucketFromFS is the trivial constant wrapper over it.

Behavior

  • ReadsHeadObject, GetObject (with BytesRange support via io.Seeker, falling back to discarding leading bytes), ListObjects, and WatchObjects (emits the current listing once, then blocks until the context is canceled, since fs.FS has no change notifications).
  • WritesCreate, PutObject, DeleteObject, DeleteObjects, CopyObject all return ErrBucketReadOnly.
  • Presign — get/head return ErrPresignNotSupported; put/delete return ErrBucketReadOnly (mirroring EmptyBucket).
  • Object keys map directly onto fs paths; validation applies ValidObjectKey then the stricter fs.ValidPath. Missing files and directories both report ErrObjectNotFound. Location() returns :fs:.

Listing

ListObjects walks the tree lazily, one directory at a time (fs.ReadDir), and never materializes the full listing. fs.WalkDir is deliberately not used: it visits a directory's contents before lexically-smaller sibling files (e.g. data/x before data.txt, since '/' > '.'), which doesn't match S3/key ordering. The walker sorts each directory's entries as if directory names had a trailing "/", making traversal order identical to sorting full keys. Because the stream is globally sorted, delimiter common-prefixes are deduped with a single tracked value (O(1) memory), and subtrees outside the prefix — or fully collapsed by the delimiter — are skipped without descending.

Tests

storage/fsbucket_test.go covers reads, ranges, listing variants (prefix / delimiter / max-keys / start-after), read-only enforcement, per-call context capture, and a cross-directory ordering case that would fail with a naive fs.WalkDir traversal.

🤖 Generated with Claude Code

NewBucketFromFS wraps a fs.FS as a read-only storage.Bucket, and
NewBucketFromNewFS derives the fs.FS from a constructor that receives
the per-operation context (useful for request/tenant-scoped filesystems).

Writes return ErrBucketReadOnly and presigning is unsupported. ListObjects
walks the tree lazily, one directory at a time, yielding in lexical key
order (sorting directory entries as if their names had a trailing "/", so
the traversal matches full-key ordering rather than fs.WalkDir's).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@achille-roussel achille-roussel merged commit 768b374 into main Jun 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant