mirror of https://github.com/databricks/cli.git
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:
parent
2aa61a7c1b
commit
960ce2e18e
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue