Add implementation of filer.Filer for local filesystem (#460)

## Changes

Local file reads on Windows require the file handle to be closed after
using it. This commit includes an interface change to return an
`io.ReadCloser` from `Read` to accommodate this.

## Tests

The existing integration tests for the filer interface all pass.
This commit is contained in:
Pieter Noordhuis 2023-06-12 15:53:58 +02:00 committed by GitHub
parent 2aa61a7c1b
commit 960ce2e18e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 206 additions and 7 deletions

View File

@ -31,6 +31,8 @@ func (f filerTest) assertContents(ctx context.Context, name string, contents str
return
}
defer reader.Close()
var body bytes.Buffer
_, err = io.Copy(&body, reader)
if !assert.NoError(f, err) {
@ -309,3 +311,20 @@ func TestAccFilerDbfsReadDir(t *testing.T) {
ctx, f := setupFilerDbfsTest(t)
runFilerReadDirTest(t, ctx, f)
}
func setupFilerLocalTest(t *testing.T) (context.Context, filer.Filer) {
ctx := context.Background()
f, err := filer.NewLocalClient(t.TempDir())
require.NoError(t, err)
return ctx, f
}
func TestAccFilerLocalReadWrite(t *testing.T) {
ctx, f := setupFilerLocalTest(t)
runFilerReadWriteTest(t, ctx, f)
}
func TestAccFilerLocalReadDir(t *testing.T) {
ctx, f := setupFilerLocalTest(t)
runFilerReadDirTest(t, ctx, f)
}

View File

@ -139,7 +139,7 @@ func (w *DbfsClient) Write(ctx context.Context, name string, reader io.Reader, m
return err
}
func (w *DbfsClient) Read(ctx context.Context, name string) (io.Reader, error) {
func (w *DbfsClient) Read(ctx context.Context, name string) (io.ReadCloser, error) {
absPath, err := w.root.Join(name)
if err != nil {
return nil, err
@ -159,7 +159,13 @@ func (w *DbfsClient) Read(ctx context.Context, name string) (io.Reader, error) {
return nil, NotAFile{absPath}
}
return w.workspaceClient.Dbfs.Open(ctx, absPath, files.FileModeRead)
handle, err := w.workspaceClient.Dbfs.Open(ctx, absPath, files.FileModeRead)
if err != nil {
return nil, err
}
// A DBFS handle open for reading does not need to be closed.
return io.NopCloser(handle), nil
}
func (w *DbfsClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error {

View File

@ -111,7 +111,7 @@ type Filer interface {
Write(ctx context.Context, path string, reader io.Reader, mode ...WriteMode) error
// Read file at `path`.
Read(ctx context.Context, path string) (io.Reader, error)
Read(ctx context.Context, path string) (io.ReadCloser, error)
// Delete file or directory at `path`.
Delete(ctx context.Context, path string, mode ...DeleteMode) error

View File

@ -70,13 +70,13 @@ func (f *fakeFiler) Write(ctx context.Context, p string, reader io.Reader, mode
return fmt.Errorf("not implemented")
}
func (f *fakeFiler) Read(ctx context.Context, p string) (io.Reader, error) {
func (f *fakeFiler) Read(ctx context.Context, p string) (io.ReadCloser, error) {
_, ok := f.entries[p]
if !ok {
return nil, fs.ErrNotExist
}
return strings.NewReader("foo"), nil
return io.NopCloser(strings.NewReader("foo")), nil
}
func (f *fakeFiler) Delete(ctx context.Context, p string, mode ...DeleteMode) error {

174
libs/filer/local_client.go Normal file
View File

@ -0,0 +1,174 @@
package filer
import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"golang.org/x/exp/slices"
)
// LocalClient implements the [Filer] interface for the local filesystem.
type LocalClient struct {
// File operations will be relative to this path.
root RootPath
}
func NewLocalClient(root string) (Filer, error) {
return &LocalClient{
root: NewRootPath(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
}
flags := os.O_WRONLY | os.O_CREATE
if slices.Contains(mode, OverwriteIfExists) {
flags |= os.O_TRUNC
} else {
flags |= os.O_EXCL
}
absPath = filepath.FromSlash(absPath)
f, err := os.OpenFile(absPath, flags, 0644)
if os.IsNotExist(err) && 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, 0644)
}
if err != nil {
switch {
case os.IsNotExist(err):
return NoSuchDirectoryError{path: absPath}
case os.IsExist(err):
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
absPath = filepath.FromSlash(absPath)
stat, err := os.Stat(absPath)
if err != nil {
if os.IsNotExist(err) {
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{}
}
absPath = filepath.FromSlash(absPath)
err = os.Remove(absPath)
// Return early on success.
if err == nil {
return nil
}
if os.IsNotExist(err) {
return FileDoesNotExistError{path: absPath}
}
if os.IsExist(err) {
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
}
absPath = filepath.FromSlash(absPath)
stat, err := os.Stat(absPath)
if err != nil {
if os.IsNotExist(err) {
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
}
dirPath = filepath.FromSlash(dirPath)
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
}
absPath = filepath.FromSlash(absPath)
stat, err := os.Stat(absPath)
if os.IsNotExist(err) {
return nil, FileDoesNotExistError{path: absPath}
}
return stat, err
}

View File

@ -152,7 +152,7 @@ func (w *WorkspaceFilesClient) Write(ctx context.Context, name string, reader io
return err
}
func (w *WorkspaceFilesClient) Read(ctx context.Context, name string) (io.Reader, error) {
func (w *WorkspaceFilesClient) Read(ctx context.Context, name string) (io.ReadCloser, error) {
absPath, err := w.root.Join(name)
if err != nil {
return nil, err
@ -184,7 +184,7 @@ func (w *WorkspaceFilesClient) Read(ctx context.Context, name string) (io.Reader
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
return io.NopCloser(bytes.NewReader(b)), nil
}
func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error {