114 lines
3.7 KiB
Go
114 lines
3.7 KiB
Go
// Package brainfs reads the brain directory through an os.Root (hyperguild#157).
|
|
// The brain dir is a Syncthing sendreceive folder and Syncthing syncs
|
|
// symlinks; a reader that walks and reads by path follows a planted symlink
|
|
// out of the brain, and blocks forever on a planted FIFO. Readers that go
|
|
// through a Brain only ever read regular files inside the brain: os.Root
|
|
// refuses to leave the root even if an entry is swapped mid-read, and every
|
|
// open is non-blocking with the type checked on the opened handle.
|
|
package brainfs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
)
|
|
|
|
// ErrNotRegular is returned for an entry that is not a regular file.
|
|
var ErrNotRegular = errors.New("not a regular file")
|
|
|
|
// Brain is an open brain directory.
|
|
type Brain struct {
|
|
root *os.Root
|
|
dir string
|
|
}
|
|
|
|
// Open opens brainDir (which may itself be a mount symlink) as a root.
|
|
func Open(brainDir string) (*Brain, error) {
|
|
r, err := os.OpenRoot(brainDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open brain root: %w", err)
|
|
}
|
|
return &Brain{root: r, dir: brainDir}, nil
|
|
}
|
|
|
|
// Close releases the root.
|
|
func (b *Brain) Close() error { return b.root.Close() }
|
|
|
|
// Rel converts a path under the brain dir to a root-relative path.
|
|
func (b *Brain) Rel(path string) (string, error) {
|
|
rel, err := filepath.Rel(b.dir, path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if rel == ".." || len(rel) > 2 && rel[:3] == ".."+string(filepath.Separator) {
|
|
return "", fmt.Errorf("%s is outside the brain dir", path)
|
|
}
|
|
return filepath.ToSlash(rel), nil
|
|
}
|
|
|
|
// ReadRegular reads a brain-relative path, refusing anything that is not a
|
|
// regular file, both before the open and on the opened handle.
|
|
func (b *Brain) ReadRegular(rel string) ([]byte, error) {
|
|
fi, err := b.root.Lstat(rel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !fi.Mode().IsRegular() {
|
|
return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular)
|
|
}
|
|
return b.readOpened(rel)
|
|
}
|
|
|
|
// readOpened opens rel non-blocking and reads it only if the OPENED handle is
|
|
// a regular file. This is the check that matters under a race: an entry
|
|
// swapped for a FIFO after the Lstat, with a writer holding it open, would
|
|
// otherwise park io.ReadAll forever (PR #161 review: removing it hung search
|
|
// after 16 iterations).
|
|
//
|
|
// Accepted residual, as in api/rawroot.go: a HARD link to an outside file is
|
|
// a regular file and is read. Syncthing syncs contents, not links, so a peer
|
|
// cannot create one; fs.protected_hardlinks=1 and the pod's secrets are on a
|
|
// different filesystem from the brain volume, so a local caller cannot link
|
|
// them in. Refusing Nlink > 1 is the next step if either changes.
|
|
func (b *Brain) readOpened(rel string) ([]byte, error) {
|
|
f, err := b.root.OpenFile(rel, os.O_RDONLY|syscall.O_NONBLOCK, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
hfi, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !hfi.Mode().IsRegular() {
|
|
return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular)
|
|
}
|
|
return io.ReadAll(f)
|
|
}
|
|
|
|
// WalkRegular walks a brain-relative directory through the root and calls fn
|
|
// for each regular file (directories are descended; symlinks, FIFOs and
|
|
// devices are skipped, never followed). An entry error is passed to onErr and
|
|
// the walk continues; a nil onErr ignores it.
|
|
func (b *Brain) WalkRegular(relDir string, fn func(rel string) error, onErr func(rel string, err error)) error {
|
|
return fs.WalkDir(b.root.FS(), filepath.ToSlash(relDir), func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
if onErr != nil {
|
|
onErr(p, err)
|
|
}
|
|
if d != nil && d.IsDir() {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if d.IsDir() || !d.Type().IsRegular() {
|
|
return nil
|
|
}
|
|
return fn(p)
|
|
})
|
|
}
|