blob: 007cb94a2bfd8e8323fad3dac46f20087e5e3bcd [file] [log] [blame] [edit]
package vfs
/*
Package vfs provides interfaces for dealing with virtual file systems.
The interfaces here are taken from io/fs, except they include a Context, which
may be used for things like HTTP requests.
*/
import (
"context"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"time"
"go.skia.org/infra/go/skerr"
)
// FS represents a virtual filesystem.
type FS interface {
// Open the given path for reading only. If the path is a directory,
// implementations should return a ReadDirFile.
Open(ctx context.Context, name string) (File, error)
// Create creates or truncates the named file. If the file already exists,
// it is truncated. If the file does not exist, it is created with mode 0666
// (before umask). If successful, methods on the returned File can be used
// for I/O; the associated file descriptor has mode O_RDWR. If there is an
// error, it will be of type *PathError.
Create(ctx context.Context, name string) (File, error)
// Close causes any resources associated with the FS to be cleaned up.
Close(ctx context.Context) error
}
// File represents a virtual file.
type File interface {
// Close the File.
Close(ctx context.Context) error
// Read behaves like io.Reader. It should return an error if this is a
// directory.
Read(ctx context.Context, buf []byte) (int, error)
// Stat returns FileInfo associated with the File.
Stat(ctx context.Context) (fs.FileInfo, error)
// ReadDir returns the contents of the File if it is a directory, and
// returns an error otherwise. Should behave the same as os.File.Readdir.
ReadDir(ctx context.Context, n int) ([]fs.FileInfo, error)
// Write is analogous to os.File.Write(). It writes the contents of b to the
// File and returns the number of bytes written and an error, if any. Write
// returns a non-nil error when n != len(b). Only valid if the File was
// created using FS.Create.
Write(ctx context.Context, b []byte) (n int, err error)
}
// ReuseContextFile is a File which reuses the same Context for all calls. This
// is useful for passing the File into library functions which do not use
// Contexts.
type ReuseContextFile struct {
File
ctx context.Context
}
// Close closes the ReuseContextFile.
func (f *ReuseContextFile) Close() error {
return f.File.Close(f.ctx)
}
// Read reads from the ReuseContextFile.
func (f *ReuseContextFile) Read(buf []byte) (int, error) {
return f.File.Read(f.ctx, buf)
}
// Stat returns the fs.FileInfo describing the ReuseContextFile.
func (f *ReuseContextFile) Stat() (fs.FileInfo, error) {
return f.File.Stat(f.ctx)
}
// WithContext returns a ReuseContextFile which wraps the given File.
func WithContext(ctx context.Context, f File) *ReuseContextFile {
return &ReuseContextFile{
File: f,
ctx: ctx,
}
}
// ReadFile is analogous to os.ReadFile.
func ReadFile(ctx context.Context, fs FS, path string) (rv []byte, rvErr error) {
f, err := fs.Open(ctx, path)
if err != nil {
return nil, skerr.Wrap(err)
}
defer func() {
closeErr := f.Close(ctx)
if rvErr == nil {
rvErr = closeErr
}
}()
wrapFile := WithContext(ctx, f)
return io.ReadAll(wrapFile)
}
// ReadDir is analogous to os.ReadDir.
func ReadDir(ctx context.Context, fs FS, path string) (rv []fs.FileInfo, rvErr error) {
f, err := fs.Open(ctx, path)
if err != nil {
return nil, skerr.Wrap(err)
}
defer func() {
closeErr := f.Close(ctx)
if rvErr == nil {
rvErr = closeErr
}
}()
return f.ReadDir(ctx, -1)
}
// Stat is analogous to os.Stat.
func Stat(ctx context.Context, fs FS, path string) (rv fs.FileInfo, rvErr error) {
f, err := fs.Open(ctx, path)
if err != nil {
return nil, skerr.Wrap(err)
}
defer func() {
closeErr := f.Close(ctx)
if rvErr == nil {
rvErr = closeErr
}
}()
return f.Stat(ctx)
}
// Walk is analogous to filepath.Walk.
func Walk(ctx context.Context, fs FS, root string, walkFn filepath.WalkFunc) error {
// This implementation is basically copied from filepath.Walk.
info, err := Stat(ctx, fs, root)
if err != nil {
err = walkFn(root, info, err)
} else {
err = walk(ctx, fs, root, info, walkFn)
}
if err == filepath.SkipDir {
return nil
}
return err
}
// walk is analogous to filepath.walk.
func walk(ctx context.Context, fs FS, fp string, info fs.FileInfo, walkFn filepath.WalkFunc) error {
// This implementation is basically copied from filepath.walk.
if !info.IsDir() {
return walkFn(fp, info, nil)
}
infos, err := ReadDir(ctx, fs, fp)
var names []string
if err == nil {
names = make([]string, 0, len(infos))
for _, fi := range infos {
names = append(names, fi.Name())
}
}
err1 := walkFn(fp, info, err)
// If err != nil, walk can't walk into this directory.
// err1 != nil means walkFn want walk to skip this directory or stop walking.
// Therefore, if one of err and err1 isn't nil, walk will return.
if err != nil || err1 != nil {
// The caller's behavior is controlled by the return value, which is decided
// by walkFn. walkFn may ignore err and return nil.
// If walkFn returns SkipDir, it will be handled by the caller.
// So walk should return whatever walkFn returns.
return err1
}
for _, name := range names {
filename := path.Join(fp, name)
fileInfo, err := Stat(ctx, fs, filename)
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = walk(ctx, fs, filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}
// WriteFile is analogous to os.WriteFile.
func WriteFile(ctx context.Context, fs FS, path string, data []byte) error {
f, err := fs.Create(ctx, path)
if err != nil {
return skerr.Wrap(err)
}
_, writeErr := f.Write(ctx, data)
if closeErr := f.Close(ctx); closeErr != nil && writeErr == nil {
return closeErr
}
return skerr.Wrap(writeErr)
}
// FileInfo implements fs.FileInfo by simply filling out the return values for
// all of the methods.
type FileInfo struct {
Name string
Size int64
Mode os.FileMode
ModTime time.Time
IsDir bool
Sys interface{}
}
// Get returns an fs.FileInfo backed by this FileInfo.
func (fi FileInfo) Get() *FileInfoImpl {
return &FileInfoImpl{fi}
}
// FileInfoImpl implements fs.FileInfo.
type FileInfoImpl struct {
FileInfo
}
// Name implements fs.FileInfo.
func (fi *FileInfoImpl) Name() string {
return fi.FileInfo.Name
}
// Size implements fs.FileInfo.
func (fi *FileInfoImpl) Size() int64 {
return fi.FileInfo.Size
}
// Mode implements fs.FileInfo.
func (fi *FileInfoImpl) Mode() os.FileMode {
return fi.FileInfo.Mode
}
// ModTime implements fs.FileInfo.
func (fi *FileInfoImpl) ModTime() time.Time {
return fi.FileInfo.ModTime
}
// IsDir implements fs.FileInfo.
func (fi *FileInfoImpl) IsDir() bool {
return fi.FileInfo.IsDir
}
// Sys implements fs.FileInfo.
func (fi *FileInfoImpl) Sys() interface{} {
return fi.FileInfo.Sys
}
// Ensure that FileInfoImpl implements fs.FileInfo.
var _ fs.FileInfo = &FileInfoImpl{}