restic/internal/fs/fs_reader.go
Michael Eischer 3a995172b7 fs: rewrite Reader to build fs tree up front
This adds proper support for filenames that include directories. For
example, `/foo/bar` would result in an error when trying to open `/foo`.

The directory tree is now build upfront. This ensures let's the
directory tree construction be handled only once. All accessors then
only have to look up the constructed directory entries.
2025-04-11 21:37:40 +02:00

291 lines
6.7 KiB
Go

package fs
import (
"fmt"
"io"
"os"
"path"
"slices"
"sync"
"syscall"
"time"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
)
// Reader is a file system which provides a directory with a single file. When
// this file is opened for reading, the reader is passed through. The file can
// be opened once, all subsequent open calls return syscall.EIO. For Lstat(),
// the provided FileInfo is returned.
type Reader struct {
items map[string]readerItem
}
type ReaderOptions struct {
Mode os.FileMode
ModTime time.Time
Size int64
AllowEmptyFile bool
}
type readerItem struct {
open *sync.Once
fi *ExtendedFileInfo
rc io.ReadCloser
allowEmptyFile bool
children []string
}
// statically ensure that Local implements FS.
var _ FS = &Reader{}
func NewReader(name string, r io.ReadCloser, opts ReaderOptions) *Reader {
items := make(map[string]readerItem)
name = readerCleanPath(name)
isFile := true
for {
if isFile {
fi := &ExtendedFileInfo{
Name: path.Base(name),
Mode: opts.Mode,
ModTime: opts.ModTime,
Size: opts.Size,
}
items[name] = readerItem{
open: &sync.Once{},
fi: fi,
rc: r,
allowEmptyFile: opts.AllowEmptyFile,
}
isFile = false
} else {
fi := &ExtendedFileInfo{
Name: path.Base(name),
Mode: os.ModeDir | 0755,
ModTime: time.Now(), // FIXME
Size: 0,
}
items[name] = readerItem{
fi: fi,
// keep the children set during the previous iteration
children: items[name].children,
}
}
parent := path.Dir(name)
if parent == name {
break
}
// add the current file to the children of the parent directory
item := items[parent]
item.children = append(item.children, path.Base(name))
items[parent] = item
name = parent
}
return &Reader{
items: items,
}
}
func readerCleanPath(name string) string {
return path.Clean("/" + name)
}
// VolumeName returns leading volume name, for the Reader file system it's
// always the empty string.
func (fs *Reader) VolumeName(_ string) string {
return ""
}
func (fs *Reader) OpenFile(name string, flag int, _ bool) (f File, err error) {
if flag & ^(O_RDONLY|O_NOFOLLOW) != 0 {
return nil, pathError("open", name,
fmt.Errorf("invalid combination of flags 0x%x", flag))
}
name = readerCleanPath(name)
item, ok := fs.items[name]
if !ok {
return nil, pathError("open", name, syscall.ENOENT)
}
// Check if the path matches our target file
if item.rc != nil {
item.open.Do(func() {
f = newReaderFile(item.rc, item.fi, item.allowEmptyFile)
})
if f == nil {
return nil, pathError("open", name, syscall.EIO)
}
return f, nil
}
f = fakeDir{
entries: slices.Clone(item.children),
}
return f, nil
}
// Lstat returns the FileInfo structure describing the named file.
// If there is an error, it will be of type *os.PathError.
func (fs *Reader) Lstat(name string) (*ExtendedFileInfo, error) {
name = readerCleanPath(name)
item, ok := fs.items[name]
if !ok {
return nil, pathError("lstat", name, os.ErrNotExist)
}
return item.fi, nil
}
// Join joins any number of path elements into a single path, adding a
// Separator if necessary. Join calls Clean on the result; in particular, all
// empty strings are ignored. On Windows, the result is a UNC path if and only
// if the first path element is a UNC path.
func (fs *Reader) Join(elem ...string) string {
return path.Join(elem...)
}
// Separator returns the OS and FS dependent separator for dirs/subdirs/files.
func (fs *Reader) Separator() string {
return "/"
}
// IsAbs reports whether the path is absolute. For the Reader, this is always the case.
func (fs *Reader) IsAbs(_ string) bool {
return true
}
// Abs returns an absolute representation of path. If the path is not absolute
// it will be joined with the current working directory to turn it into an
// absolute path. The absolute path name for a given file is not guaranteed to
// be unique. Abs calls Clean on the result.
//
// For the Reader, all paths are absolute.
func (fs *Reader) Abs(p string) (string, error) {
return readerCleanPath(p), nil
}
// Clean returns the cleaned path. For details, see filepath.Clean.
func (fs *Reader) Clean(p string) string {
return path.Clean(p)
}
// Base returns the last element of p.
func (fs *Reader) Base(p string) string {
return path.Base(p)
}
// Dir returns p without the last element.
func (fs *Reader) Dir(p string) string {
return path.Dir(p)
}
func newReaderFile(rd io.ReadCloser, fi *ExtendedFileInfo, allowEmptyFile bool) *readerFile {
return &readerFile{
ReadCloser: rd,
AllowEmptyFile: allowEmptyFile,
fakeFile: fakeFile{
fi: fi,
name: fi.Name,
},
}
}
type readerFile struct {
io.ReadCloser
AllowEmptyFile, bytesRead bool
fakeFile
}
// ErrFileEmpty is returned inside a *os.PathError by Read() for the file
// opened from the fs provided by Reader when no data could be read and
// AllowEmptyFile is not set.
var ErrFileEmpty = errors.New("no data read")
func (r *readerFile) Read(p []byte) (int, error) {
n, err := r.ReadCloser.Read(p)
if n > 0 {
r.bytesRead = true
}
// return an error if we did not read any data
if err == io.EOF && !r.AllowEmptyFile && !r.bytesRead {
return n, pathError("read", r.fakeFile.name, ErrFileEmpty)
}
return n, err
}
func (r *readerFile) Close() error {
return r.ReadCloser.Close()
}
// ensure that readerFile implements File
var _ File = &readerFile{}
// fakeFile implements all File methods, but only returns errors for anything
// except Stat()
type fakeFile struct {
name string
fi *ExtendedFileInfo
}
// ensure that fakeFile implements File
var _ File = fakeFile{}
func (f fakeFile) MakeReadable() error {
return nil
}
func (f fakeFile) Readdirnames(_ int) ([]string, error) {
return nil, pathError("readdirnames", f.name, os.ErrInvalid)
}
func (f fakeFile) Read(_ []byte) (int, error) {
return 0, pathError("read", f.name, os.ErrInvalid)
}
func (f fakeFile) Close() error {
return nil
}
func (f fakeFile) Stat() (*ExtendedFileInfo, error) {
return f.fi, nil
}
func (f fakeFile) ToNode(_ bool) (*restic.Node, error) {
node := buildBasicNode(f.name, f.fi)
// fill minimal info with current values for uid, gid
node.UID = uint32(os.Getuid())
node.GID = uint32(os.Getgid())
node.ChangeTime = node.ModTime
return node, nil
}
// fakeDir implements Readdirnames and Readdir, everything else is delegated to fakeFile.
type fakeDir struct {
entries []string
fakeFile
}
func (d fakeDir) Readdirnames(n int) ([]string, error) {
if n > 0 {
return nil, pathError("readdirnames", d.name, errors.New("not implemented"))
}
return slices.Clone(d.entries), nil
}
func pathError(op, name string, err error) *os.PathError {
return &os.PathError{Op: op, Path: name, Err: err}
}