wsfs

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package wsfs is the file layer of a fiber's workspace. fiberd runs as root and the fiber owns every name under the workspace root, so each operation here treats the tree as hostile. The root is opened once and every path is resolved beneath that descriptor, never from a string. Uploads land in a staging directory the fiber cannot see and are renamed into place from descriptors. Downloads, listings and the park archive send regular files only.

Only Linux has openat2, so only Linux gets a Workspace. The path rules and the archive reader are plain Go and are checked everywhere.

Index

Constants

View Source
const (
	MaxPathBytes = 4096
	MaxDepth     = 64
)

Path limits from the design. A path is relative, at most MaxPathBytes long and MaxDepth components deep, with no NUL, "." or "..".

View Source
const DefaultEntries = 65536

DefaultEntries is the entry ceiling a workspace gets when Limits names none.

Variables

View Source
var (
	// ErrPath is a path the rules refuse.
	ErrPath = errors.New("wsfs: bad path")
	// ErrNotRegular is a name that is not a regular file where one is
	// needed, such as a FIFO, a device, a socket, a link or a directory.
	ErrNotRegular = errors.New("wsfs: not a regular file")
	// ErrSize is a body that does not match its declared size.
	ErrSize = errors.New("wsfs: size mismatch")
	// ErrChecksum is a body whose sha256 does not match the declared one.
	ErrChecksum = errors.New("wsfs: sha256 mismatch")
	// ErrBudget is a reservation the budget refused.
	ErrBudget = errors.New("wsfs: over budget")
	// ErrEntries is a tree or an archive with more entries than allowed.
	ErrEntries = errors.New("wsfs: too many entries")
	// ErrTooLarge is a tree or an archive with more bytes than allowed.
	ErrTooLarge = errors.New("wsfs: too large")
	// ErrArchive is an archive entry the unpacker refuses.
	ErrArchive = errors.New("wsfs: bad archive")
	// ErrNotEmpty is an unpack into a root that already holds names.
	ErrNotEmpty = errors.New("wsfs: root not empty")
	// ErrForeignLink is a file in the tree with a hard link outside it.
	ErrForeignLink = errors.New("wsfs: hard link from outside the workspace")
	// ErrUnsupported is a platform without openat2.
	ErrUnsupported = errors.New("wsfs: unsupported on this platform")
)

Errors the layer returns. Callers match them with errors.Is.

Functions

func Clean

func Clean(p string, allowRoot bool) (string, error)

Clean checks a caller's path against the rules and returns it with forward slashes and no trailing slash. The path is relative, at most MaxPathBytes long, at most MaxDepth components deep, and no component is empty, "." or "..". A NUL anywhere is refused. The empty path names the root and is allowed only when allowRoot is set.

Types

type Budget

type Budget interface {
	Reserve(bytes, entries int64) error
	Release(bytes, entries int64)
}

Budget accounts the bytes and entries a workspace holds. Upload and Unpack call Reserve before a byte lands and Release when the write fails, so a later stage can put a project quota behind it. A reservation is bytes and entries together, and Release takes back what one Reserve gave.

type Counter

type Counter struct {
	// contains filtered or unexported fields
}

Counter is a Budget in memory. It is the default and what the tests use. The zero value refuses everything.

func NewCounter

func NewCounter(l Limits) *Counter

NewCounter makes a Counter that allows up to l.Bytes bytes and l.Entries entries. Zero Bytes allows no bytes, as a zero disk budget gives no workspace.

func (*Counter) Held

func (c *Counter) Held() (bytes, entries int64)

Held reports what the counter holds.

func (*Counter) Release

func (c *Counter) Release(bytes, entries int64)

Release gives back a reservation. A release past zero is a bug in the caller and is left visible, not clamped.

func (*Counter) Reserve

func (c *Counter) Reserve(bytes, entries int64) error

Reserve takes bytes and entries, or refuses with ErrBudget and takes nothing.

type File

type File struct {
	// contains filtered or unexported fields
}

File is an open download. It reads at most the bytes the file held when it was opened, from the offset asked for, so a fiber that grows the file after the open cannot keep the stream going.

func (*File) Close

func (f *File) Close() error

Close closes the file.

func (*File) Info

func (f *File) Info() Info

Info describes the file as it was when opened.

func (*File) Read

func (f *File) Read(p []byte) (int, error)

Read reads the next bytes and ends with io.EOF at the bound.

type Info

type Info struct {
	// Path is relative to the workspace root, with forward slashes.
	Path string
	// Size is the apparent size in bytes. A directory reports zero.
	Size int64
	// Mode holds the permission bits, and fs.ModeDir for a directory.
	Mode fs.FileMode
	// ModTime is the last modification time.
	ModTime time.Time
	// SHA256 is the content hash, set by Upload only.
	SHA256 []byte
}

Info describes one entry of a workspace.

type Limits

type Limits struct {
	Bytes   int64
	Entries int64
}

Limits bound what a workspace may hold. The park archive and the unpacker refuse a tree past either. Zero Bytes allows no file bytes at all, as a zero disk budget gives no workspace. Zero Entries means DefaultEntries.

type Options

type Options struct {
	// Budget is required. Upload and Unpack reserve against it.
	Budget Budget
	// Limits bound the tree for List, Capture and Unpack.
	Limits Limits
	// Owner, when set, owns the root and everything fiberd writes. Nil
	// leaves fiberd's own ids.
	Owner *Owner
}

Options configure a workspace.

type Owner

type Owner struct {
	UID int
	GID int
}

Owner is who owns what fiberd writes into a workspace. A backend whose fibers run as another id sets it, so the fiber can use the files.

type Workspace

type Workspace struct {
	// contains filtered or unexported fields
}

Workspace is one fiber's workspace on the home. Every method is safe for concurrent use, and Close waits for the operations in flight.

func Open

func Open(dir string, o Options) (*Workspace, error)

Open opens the workspace at dir, making dir, its root and its staging directory when they are absent. The staging directory is 0700 and outside the root, so no fiber sees it. Staging files a crash left are removed. The root and the staging directory must be on one file system, since an upload is renamed between them. A kernel without openat2 gets ErrUnsupported and no workspace.

func (*Workspace) Capture

func (w *Workspace) Capture(out io.Writer) error

Capture writes the tree under the root to out as a tar. Directories, regular files and symbolic links are carried, a link as its target string and never followed. A file with several names in the tree is written once and then as hard links to the first name. FIFOs, sockets and devices are skipped. Sparse files count by apparent size. A tree past the limits is refused, and so is a file with a name outside the tree, since fiberd must not seal host bytes into a delta. Every path is checked with the same rules Unpack applies, so a tree that parks can always resume.

func (*Workspace) Close

func (w *Workspace) Close()

Close waits for the operations in flight and releases the descriptors. A File from Download keeps its own and outlives Close.

func (*Workspace) Download

func (w *Workspace) Download(p string, offset int64) (*File, error)

Download opens the regular file p for reading from offset. The reader is bounded by the size the file had at the open.

func (*Workspace) List

func (w *Workspace) List(p string) ([]Info, error)

List returns the regular files and directories directly under the directory p, sorted by name. "" names the root. Links, FIFOs, sockets and devices are left out. More entries than the limit is ErrEntries.

func (*Workspace) Size

func (w *Workspace) Size() (bytes, entries int64, err error)

Size walks the tree and reports its apparent bytes and entries, as Capture would count them, without reading a file. A file with several names counts once. FIFOs, sockets and devices count as entries and no bytes. The walk is bounded by the entry ceiling, so a hostile tree cannot keep fiberd walking.

func (*Workspace) Unpack

func (w *Workspace) Unpack(in io.Reader) error

Unpack reads a workspace archive into an empty root. Every name and link target is checked with the path rules, entries and bytes are counted against the limits and reserved against the budget, regular files go through the staging directory and are renamed into place, and a symbolic link is made as a string that nothing here follows. A hard link may only name a regular file the same archive wrote. A root that already holds names is refused with ErrNotEmpty. On failure what is still reserved is released and the caller discards the tree.

func (*Workspace) Upload

func (w *Workspace) Upload(p string, size int64, mode fs.FileMode, sum []byte, r io.Reader) (Info, error)

Upload writes size bytes from r to p. The path is checked, the budget is reserved for size bytes and, for a new name, one entry, the body is written to the staging directory, its length and optional sha256 are checked, and it is synced before it is renamed into place. Missing directories on the way are made. mode gives the permission bits, 0644 when zero. Nothing appears under the root until every check has passed, and a failed upload releases its reservation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL