mirror of https://github.com/databricks/cli.git
Include recursive deletion in filer interface (#442)
## Changes This captures the recursive deletion of a directory tree in the filer interface. Prompted by #433. ## Tests Integration tests pass (ran the filer ones on AWS and Azure).
This commit is contained in:
parent
d6d35e314f
commit
be10ff9a75
|
@ -68,6 +68,10 @@ func runFilerReadWriteTest(t *testing.T, ctx context.Context, f filer.Filer) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
filerTest{t, f}.assertContents(ctx, "/foo/bar", `hello universe`)
|
filerTest{t, f}.assertContents(ctx, "/foo/bar", `hello universe`)
|
||||||
|
|
||||||
|
// Write should succeed if there is no existing file at the specified path.
|
||||||
|
err = f.Write(ctx, "/foo/qux", strings.NewReader(`hello universe`))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Stat on a directory should succeed.
|
// Stat on a directory should succeed.
|
||||||
// Note: size and modification time behave differently between WSFS and DBFS.
|
// Note: size and modification time behave differently between WSFS and DBFS.
|
||||||
info, err := f.Stat(ctx, "/foo")
|
info, err := f.Stat(ctx, "/foo")
|
||||||
|
@ -97,6 +101,21 @@ func runFilerReadWriteTest(t *testing.T, ctx context.Context, f filer.Filer) {
|
||||||
// Delete should succeed for file that does exist.
|
// Delete should succeed for file that does exist.
|
||||||
err = f.Delete(ctx, "/foo/bar")
|
err = f.Delete(ctx, "/foo/bar")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Delete should fail for a non-empty directory.
|
||||||
|
err = f.Delete(ctx, "/foo")
|
||||||
|
assert.True(t, errors.As(err, &filer.DirectoryNotEmptyError{}))
|
||||||
|
assert.True(t, errors.Is(err, fs.ErrInvalid))
|
||||||
|
|
||||||
|
// Delete should succeed for a non-empty directory if the DeleteRecursively flag is set.
|
||||||
|
err = f.Delete(ctx, "/foo", filer.DeleteRecursively)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Delete of the filer root should ALWAYS fail, otherwise subsequent writes would fail.
|
||||||
|
// It is not in the filer's purview to delete its root directory.
|
||||||
|
err = f.Delete(ctx, "/")
|
||||||
|
assert.True(t, errors.As(err, &filer.CannotDeleteRootError{}))
|
||||||
|
assert.True(t, errors.Is(err, fs.ErrInvalid))
|
||||||
}
|
}
|
||||||
|
|
||||||
func runFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) {
|
func runFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) {
|
||||||
|
|
|
@ -165,12 +165,17 @@ func (w *DbfsClient) Read(ctx context.Context, name string) (io.Reader, error) {
|
||||||
return handle, nil
|
return handle, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *DbfsClient) Delete(ctx context.Context, name string) error {
|
func (w *DbfsClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error {
|
||||||
absPath, err := w.root.Join(name)
|
absPath, err := w.root.Join(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Illegal to delete the root path.
|
||||||
|
if absPath == w.root.rootPath {
|
||||||
|
return CannotDeleteRootError{}
|
||||||
|
}
|
||||||
|
|
||||||
// Issue info call before delete because delete succeeds if the specified path doesn't exist.
|
// Issue info call before delete because delete succeeds if the specified path doesn't exist.
|
||||||
//
|
//
|
||||||
// For discussion: we could decide this is actually convenient, remove the call below,
|
// For discussion: we could decide this is actually convenient, remove the call below,
|
||||||
|
@ -193,10 +198,36 @@ func (w *DbfsClient) Delete(ctx context.Context, name string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.workspaceClient.Dbfs.Delete(ctx, files.Delete{
|
recursive := false
|
||||||
|
if slices.Contains(mode, DeleteRecursively) {
|
||||||
|
recursive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.workspaceClient.Dbfs.Delete(ctx, files.Delete{
|
||||||
Path: absPath,
|
Path: absPath,
|
||||||
Recursive: false,
|
Recursive: recursive,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Return early on success.
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling of this error only if it is an API error.
|
||||||
|
var aerr *apierr.APIError
|
||||||
|
if !errors.As(err, &aerr) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch aerr.StatusCode {
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
// Anecdotally, this error is returned when attempting to delete a non-empty directory.
|
||||||
|
if aerr.ErrorCode == "IO_ERROR" {
|
||||||
|
return DirectoryNotEmptyError{absPath}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *DbfsClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
|
func (w *DbfsClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
|
||||||
|
|
|
@ -14,6 +14,12 @@ const (
|
||||||
CreateParentDirectories = iota << 1
|
CreateParentDirectories = iota << 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DeleteMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DeleteRecursively DeleteMode = iota
|
||||||
|
)
|
||||||
|
|
||||||
type FileAlreadyExistsError struct {
|
type FileAlreadyExistsError struct {
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
@ -62,6 +68,29 @@ func (err NotADirectory) Is(other error) bool {
|
||||||
return other == fs.ErrInvalid
|
return other == fs.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DirectoryNotEmptyError struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err DirectoryNotEmptyError) Error() string {
|
||||||
|
return fmt.Sprintf("directory not empty: %s", err.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err DirectoryNotEmptyError) Is(other error) bool {
|
||||||
|
return other == fs.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
type CannotDeleteRootError struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err CannotDeleteRootError) Error() string {
|
||||||
|
return "unable to delete filer root"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err CannotDeleteRootError) Is(other error) bool {
|
||||||
|
return other == fs.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
// Filer is used to access files in a workspace.
|
// Filer is used to access files in a workspace.
|
||||||
// It has implementations for accessing files in WSFS and in DBFS.
|
// It has implementations for accessing files in WSFS and in DBFS.
|
||||||
type Filer interface {
|
type Filer interface {
|
||||||
|
@ -72,8 +101,8 @@ type Filer interface {
|
||||||
// Read file at `path`.
|
// Read file at `path`.
|
||||||
Read(ctx context.Context, path string) (io.Reader, error)
|
Read(ctx context.Context, path string) (io.Reader, error)
|
||||||
|
|
||||||
// Delete file at `path`.
|
// Delete file or directory at `path`.
|
||||||
Delete(ctx context.Context, path string) error
|
Delete(ctx context.Context, path string, mode ...DeleteMode) error
|
||||||
|
|
||||||
// Return contents of directory at `path`.
|
// Return contents of directory at `path`.
|
||||||
ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error)
|
ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error)
|
||||||
|
|
|
@ -79,7 +79,7 @@ func (f *fakeFiler) Read(ctx context.Context, p string) (io.Reader, error) {
|
||||||
return strings.NewReader("foo"), nil
|
return strings.NewReader("foo"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeFiler) Delete(ctx context.Context, p string) error {
|
func (f *fakeFiler) Delete(ctx context.Context, p string, mode ...DeleteMode) error {
|
||||||
return fmt.Errorf("not implemented")
|
return fmt.Errorf("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,15 +184,25 @@ func (w *WorkspaceFilesClient) Read(ctx context.Context, name string) (io.Reader
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string) error {
|
func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error {
|
||||||
absPath, err := w.root.Join(name)
|
absPath, err := w.root.Join(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Illegal to delete the root path.
|
||||||
|
if absPath == w.root.rootPath {
|
||||||
|
return CannotDeleteRootError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
recursive := false
|
||||||
|
if slices.Contains(mode, DeleteRecursively) {
|
||||||
|
recursive = true
|
||||||
|
}
|
||||||
|
|
||||||
err = w.workspaceClient.Workspace.Delete(ctx, workspace.Delete{
|
err = w.workspaceClient.Workspace.Delete(ctx, workspace.Delete{
|
||||||
Path: absPath,
|
Path: absPath,
|
||||||
Recursive: false,
|
Recursive: recursive,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return early on success.
|
// Return early on success.
|
||||||
|
@ -206,7 +216,12 @@ func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if aerr.StatusCode == http.StatusNotFound {
|
switch aerr.StatusCode {
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
if aerr.ErrorCode == "DIRECTORY_NOT_EMPTY" {
|
||||||
|
return DirectoryNotEmptyError{absPath}
|
||||||
|
}
|
||||||
|
case http.StatusNotFound:
|
||||||
return FileDoesNotExistError{absPath}
|
return FileDoesNotExistError{absPath}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue