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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
var body bytes.Buffer
|
var body bytes.Buffer
|
||||||
_, err = io.Copy(&body, reader)
|
_, err = io.Copy(&body, reader)
|
||||||
if !assert.NoError(f, err) {
|
if !assert.NoError(f, err) {
|
||||||
|
@ -309,3 +311,20 @@ func TestAccFilerDbfsReadDir(t *testing.T) {
|
||||||
ctx, f := setupFilerDbfsTest(t)
|
ctx, f := setupFilerDbfsTest(t)
|
||||||
runFilerReadDirTest(t, ctx, f)
|
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
|
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)
|
absPath, err := w.root.Join(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -159,7 +159,13 @@ func (w *DbfsClient) Read(ctx context.Context, name string) (io.Reader, error) {
|
||||||
return nil, NotAFile{absPath}
|
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 {
|
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
|
Write(ctx context.Context, path string, reader io.Reader, mode ...WriteMode) error
|
||||||
|
|
||||||
// Read file at `path`.
|
// 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 file or directory at `path`.
|
||||||
Delete(ctx context.Context, path string, mode ...DeleteMode) error
|
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")
|
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]
|
_, ok := f.entries[p]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fs.ErrNotExist
|
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 {
|
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
|
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)
|
absPath, err := w.root.Join(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -184,7 +184,7 @@ func (w *WorkspaceFilesClient) Read(ctx context.Context, name string) (io.Reader
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error {
|
||||||
|
|
Loading…
Reference in New Issue