Documentation
¶
Overview ¶
Package objfs is a lightweight object-storage abstraction that is also an io/fs.FS.
Inspired by Thanos' objstore, objfs exposes a small Bucket interface that every backend implements. Because Bucket embeds io/fs.FS, any backend can be handed to the standard library — io/fs.WalkDir, io/fs.ReadFile, net/http.FileServerFS, templates, and so on — while still offering context-aware upload, delete, listing and (optionally) presigned URLs.
The core module depends only on the standard library and ships the local filesystem backend (NewLocal). Cloud backends live in opt-in submodules so their SDK dependencies are only pulled in when imported:
github.com/armadakv/objfs/s3 // Amazon S3 (and S3-compatible) github.com/armadakv/objfs/gcs // Google Cloud Storage github.com/armadakv/objfs/azblob // Azure Blob Storage
Example ¶
Example shows the core workflow: upload, read back through the io/fs.FS surface, and check for the optional presign capability — all backend-agnostic.
package main
import (
"context"
"fmt"
"io/fs"
"strings"
"github.com/armadakv/objfs"
)
func main() {
ctx := context.Background()
// Any backend works here; the local one needs only the standard library.
var bucket objfs.Bucket
bucket, _ = objfs.NewLocal("/tmp/objfs-example")
defer bucket.Close()
_ = bucket.Upload(ctx, "greetings/hello.txt", strings.NewReader("hi"))
// Read it back via the standard library — bucket is an io/fs.FS.
data, _ := fs.ReadFile(bucket, "greetings/hello.txt")
fmt.Printf("contents: %s\n", data)
// Presigning is an optional capability. Local does not support it.
if _, err := objfs.PresignedGet(ctx, bucket, "greetings/hello.txt", 0); err != nil {
fmt.Println("presign:", err)
}
}
Output: contents: hi presign: objfs: presign GET: objfs: unsupported operation
Index ¶
- Variables
- func NewFile(r io.ReadCloser, attrs Attributes) fs.File
- func NewRandomAccessFile(b Bucket, attrs Attributes) fs.File
- func PresignedGet(ctx context.Context, b Bucket, name string, expiry time.Duration) (string, error)
- func PresignedPut(ctx context.Context, b Bucket, name string, expiry time.Duration) (string, error)
- func ReadDir(ctx context.Context, b Bucket, name string) ([]fs.DirEntry, error)
- func ReadFile(ctx context.Context, b Bucket, name string) ([]byte, error)
- func SortDirEntries(entries []fs.DirEntry)
- type Attributes
- type Bucket
- type FileInfo
- type Local
- func (l *Local) Close() error
- func (l *Local) Delete(ctx context.Context, name string) error
- func (l *Local) Exists(ctx context.Context, name string) (bool, error)
- func (l *Local) Get(ctx context.Context, name string) (io.ReadCloser, error)
- func (l *Local) GetRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error)
- func (l *Local) List(ctx context.Context, prefix string, fn func(Attributes) error) error
- func (l *Local) Open(name string) (fs.File, error)
- func (l *Local) ReadDir(name string) ([]fs.DirEntry, error)
- func (l *Local) ReadFile(name string) ([]byte, error)
- func (l *Local) Stat(ctx context.Context, name string) (Attributes, error)
- func (l *Local) Sub(dir string) (fs.FS, error)
- func (l *Local) Upload(ctx context.Context, name string, r io.Reader, _ ...UploadOption) error
- type Operation
- type Presigner
- type UploadOption
- type UploadOptions
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNotExist = fs.ErrNotExist
ErrNotExist is returned when an object does not exist. It aliases io/fs.ErrNotExist so that errors.Is(err, fs.ErrNotExist) holds for every backend, keeping objfs interchangeable with the standard library.
var ErrUnsupported = errors.New("objfs: unsupported operation")
ErrUnsupported is returned by operations a backend does not implement, most commonly Presigner.PresignedURL on backends without signed-URL support.
var SkipAll = fs.SkipAll
SkipAll is returned by a Bucket.List callback to stop iteration early without reporting an error. It mirrors io/fs.SkipAll.
Functions ¶
func NewFile ¶
func NewFile(r io.ReadCloser, attrs Attributes) fs.File
NewFile returns an io/fs.File that reads from r and reports info from attrs. The returned file takes ownership of r and closes it on Close.
func NewRandomAccessFile ¶
func NewRandomAccessFile(b Bucket, attrs Attributes) fs.File
NewRandomAccessFile returns an io/fs.File for the object described by attrs whose bytes are fetched on demand from b via Bucket.GetRange. The returned file also satisfies io.ReaderAt and io.Seeker.
attrs.Name and attrs.Size must be populated; Size bounds Seek and ReadAt.
func PresignedGet ¶
PresignedGet is a convenience wrapper that returns a download URL for name, or an error wrapping ErrUnsupported if b is not a Presigner.
func PresignedPut ¶
PresignedPut is a convenience wrapper that returns an upload URL for name, or an error wrapping ErrUnsupported if b is not a Presigner.
func ReadDir ¶
ReadDir lists the immediate children of the directory name within b, synthesising subdirectories from the "/" delimiter. It is the backend- agnostic implementation of io/fs.ReadDirFS built on Bucket.List; cloud backends override it with native delimiter listing for efficiency, but it is exported so it works against any Bucket.
Object stores have no empty directories, so a name with no children yields an empty slice rather than an error.
func ReadFile ¶
ReadFile reads the named object from b in full. Backends expose it as their io/fs.ReadFileFS implementation; it issues a single Get (no preliminary Stat), which is cheaper than the Open+ReadAll fallback on stores where Open stats first.
func SortDirEntries ¶
SortDirEntries sorts entries by name, as io/fs.ReadDir requires. Backends call it before returning from ReadDir.
Types ¶
type Attributes ¶
type Attributes struct {
// Name is the object's slash-separated key.
Name string
// Size is the object size in bytes.
Size int64
// LastModified is the last-modified time, if known.
LastModified time.Time
// ContentType is the stored MIME type, if any.
ContentType string
// ETag is the backend's entity tag, if any.
ETag string
// Metadata holds backend user metadata, if any.
Metadata map[string]string
}
Attributes describes a stored object.
type Bucket ¶
type Bucket interface {
// FS provides Open(name) and, where the backend supports it, the richer
// fs.ReadDirFS / fs.StatFS / fs.SubFS behaviours. Open uses
// context.Background internally.
fs.FS
// Get returns a reader for the whole object. The caller must Close it.
// It returns an error wrapping ErrNotExist if name does not exist.
Get(ctx context.Context, name string) (io.ReadCloser, error)
// GetRange returns a reader for the half-open byte range [off, off+length).
// A negative length reads to the end of the object.
GetRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error)
// Upload stores the contents of r under name, creating intermediate
// "directories" as needed. Existing objects are overwritten.
Upload(ctx context.Context, name string, r io.Reader, opts ...UploadOption) error
// Delete removes name. Deleting a missing object is not an error.
Delete(ctx context.Context, name string) error
// Stat returns metadata about name without reading its body.
Stat(ctx context.Context, name string) (Attributes, error)
// Exists reports whether name exists.
Exists(ctx context.Context, name string) (bool, error)
// List calls fn once per object whose name begins with prefix. Iteration
// stops, and the error is returned, if fn returns a non-nil error;
// returning [SkipAll] stops iteration without error.
List(ctx context.Context, prefix string, fn func(Attributes) error) error
// Close releases resources held by the backend (connection pools, clients).
io.Closer
}
Bucket is a read-write object store that doubles as an io/fs.FS.
Object names are always slash-separated paths satisfying io/fs.ValidPath (no leading slash, no "." or ".." elements, no empty segments). The reusable io/fs helpers — ReadFile, WalkDir, Glob, Sub — therefore work against any Bucket via the embedded FS.
The context-free io/fs.FS Open exists for stdlib compatibility; prefer the context-aware methods (Get, Upload, ...) in application code.
func Sub ¶
Sub returns a Bucket rooted at dir within b: every name passed to the returned bucket is prefixed with dir before reaching b, and names reported by List have the prefix stripped. dir must satisfy io/fs.ValidPath.
The result is a full Bucket — Upload, Delete and (when b supports it) presigning all operate within the sub-tree — which is richer than the Open-only view that io/fs.Sub would produce. Nested Subs are flattened.
type FileInfo ¶
type FileInfo struct {
// contains filtered or unexported fields
}
FileInfo adapts Attributes to io/fs.FileInfo.
func NewDirInfo ¶
NewDirInfo returns an io/fs.FileInfo describing a synthetic directory at name. Object stores have no real directories; backends synthesise them from key prefixes so that io/fs.WalkDir works.
func NewFileInfo ¶
func NewFileInfo(a Attributes) FileInfo
NewFileInfo returns an io/fs.FileInfo describing a regular object.
func (FileInfo) Attributes ¶
func (fi FileInfo) Attributes() Attributes
Attributes returns the object metadata backing this FileInfo.
type Local ¶
type Local struct {
// contains filtered or unexported fields
}
Local is a Bucket backed by a directory on the local filesystem. It is part of the core module and depends only on the standard library, which makes it the natural choice for tests, local development and single-node deployments.
All access is confined to the root directory via os.Root: object names that would escape the root (via "..", absolute paths or symlinks) are rejected. Local does not implement Presigner; presign helpers return ErrUnsupported.
func NewLocal ¶
NewLocal opens (creating it if necessary) the directory dir and returns a Bucket rooted there.
func (*Local) GetRange ¶
func (l *Local) GetRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error)
GetRange returns a reader over [off, off+length); a negative length reads to the end of the object.
func (*Local) List ¶
List walks the directory tree and reports every regular file whose name begins with prefix.
func (*Local) Open ¶
Open implements io/fs.FS. The returned file is an *os.File, so directories support ReadDir and regular files support Seek.
func (*Local) ReadDir ¶
ReadDir implements io/fs.ReadDirFS, returning the immediate children of the directory name (real filesystem directories, so empty dirs are honoured).
func (*Local) ReadFile ¶
ReadFile implements io/fs.ReadFileFS.
func (*Local) Sub ¶
Sub implements io/fs.SubFS, returning a prefix-scoped Bucket.
type Operation ¶
type Operation int
Operation identifies the HTTP method a presigned URL authorises.
type Presigner ¶
type Presigner interface {
// PresignedURL returns a URL authorising op on name, valid for expiry.
PresignedURL(ctx context.Context, name string, op Operation, expiry time.Duration) (string, error)
}
Presigner is an optional capability for backends that can mint time-limited URLs granting direct access to an object without further authentication.
Detect it with a type assertion, or use the package helpers PresignedGet and PresignedPut:
if p, ok := bucket.(objfs.Presigner); ok {
url, err := p.PresignedURL(ctx, "report.pdf", objfs.OpGet, time.Hour)
}
type UploadOption ¶
type UploadOption func(*UploadOptions)
An UploadOption customises UploadOptions.
func WithCacheControl ¶
func WithCacheControl(cc string) UploadOption
WithCacheControl sets the Cache-Control header.
func WithContentType ¶
func WithContentType(ct string) UploadOption
WithContentType sets the object's MIME type.
func WithMetadata ¶
func WithMetadata(m map[string]string) UploadOption
WithMetadata sets backend user metadata. Repeated calls merge.
type UploadOptions ¶
type UploadOptions struct {
// ContentType sets the object's MIME type. When empty, backends may sniff
// it from the name or content.
ContentType string
// CacheControl sets the Cache-Control header on backends that support it.
CacheControl string
// Metadata sets backend user metadata.
Metadata map[string]string
}
UploadOptions configures a single Bucket.Upload call.
func ApplyUploadOptions ¶
func ApplyUploadOptions(opts []UploadOption) UploadOptions
ApplyUploadOptions builds an UploadOptions from opts. Backends call this at the top of Upload.