blob: b4cc07156bfce428dfcc1d54c6ca2f376dde637e [file] [log] [blame]
package vfs
/*
Package vfs provides interfaces for dealing with virtual file systems.
The interfaces here are taken io/fs, except they include a Context, which may be
used for things like HTTP requests.
*/
import (
"context"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"time"
"go.skia.org/infra/go/skerr"
)
// FS represents a virtual filesystem.
type FS interface {
// Open the given path. If the path is a directory, implementations should
// return a ReadDirFile.
Open(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. Shouold behave the same as os.File.Readdir.
ReadDir(ctx context.Context, n int) ([]fs.FileInfo, 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 ioutil.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 ioutil.ReadAll(wrapFile)
}
// ReadDir is analogous to ioutil.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
}
// 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{}