package filer import ( "context" "errors" "io" "io/fs" "os" "path/filepath" "slices" ) // LocalClient implements the [Filer] interface for the local filesystem. type LocalClient struct { // File operations will be relative to this path. root localRootPath } func NewLocalClient(root string) (Filer, error) { return &LocalClient{ root: NewLocalRootPath(root), }, nil } func (w *LocalClient) Write(ctx context.Context, name string, reader io.Reader, mode ...WriteMode) error { absPath, err := w.root.Join(name) if err != nil { return err } // Retrieve permission mask from the [WriteMode], if present. perm := fs.FileMode(0644) for _, m := range mode { bits := m & writeModePerm if bits != 0 { perm = fs.FileMode(bits) } } flags := os.O_WRONLY | os.O_CREATE if slices.Contains(mode, OverwriteIfExists) { flags |= os.O_TRUNC } else { flags |= os.O_EXCL } f, err := os.OpenFile(absPath, flags, perm) if errors.Is(err, fs.ErrNotExist) && slices.Contains(mode, CreateParentDirectories) { // Create parent directories if they don't exist. err = os.MkdirAll(filepath.Dir(absPath), 0755) if err != nil { return err } // Try again. f, err = os.OpenFile(absPath, flags, perm) } if err != nil { switch { case errors.Is(err, fs.ErrNotExist): return NoSuchDirectoryError{path: absPath} case errors.Is(err, fs.ErrExist): return FileAlreadyExistsError{path: absPath} default: return err } } _, err = io.Copy(f, reader) cerr := f.Close() if err == nil { err = cerr } return err } func (w *LocalClient) Read(ctx context.Context, name string) (io.ReadCloser, error) { absPath, err := w.root.Join(name) if err != nil { return nil, err } // This stat call serves two purposes: // 1. Checks file at path exists, and throws an error if it does not // 2. Allows us to error out if the path is a directory stat, err := os.Stat(absPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, FileDoesNotExistError{path: absPath} } return nil, err } if stat.IsDir() { return nil, NotAFile{path: absPath} } return os.Open(absPath) } func (w *LocalClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error { absPath, err := w.root.Join(name) if err != nil { return err } // Illegal to delete the root path. if absPath == w.root.rootPath { return CannotDeleteRootError{} } err = os.Remove(absPath) // Return early on success. if err == nil { return nil } if errors.Is(err, fs.ErrNotExist) { return FileDoesNotExistError{path: absPath} } if errors.Is(err, fs.ErrExist) { if slices.Contains(mode, DeleteRecursively) { return os.RemoveAll(absPath) } return DirectoryNotEmptyError{path: absPath} } return err } func (w *LocalClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) { absPath, err := w.root.Join(name) if err != nil { return nil, err } stat, err := os.Stat(absPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, NoSuchDirectoryError{path: absPath} } return nil, err } if !stat.IsDir() { return nil, NotADirectory{path: absPath} } return os.ReadDir(absPath) } func (w *LocalClient) Mkdir(ctx context.Context, name string) error { dirPath, err := w.root.Join(name) if err != nil { return err } return os.MkdirAll(dirPath, 0755) } func (w *LocalClient) Stat(ctx context.Context, name string) (fs.FileInfo, error) { absPath, err := w.root.Join(name) if err != nil { return nil, err } stat, err := os.Stat(absPath) if errors.Is(err, fs.ErrNotExist) { return nil, FileDoesNotExistError{path: absPath} } return stat, err }