| 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{} |