From 963105b0873b8c15ad2cb79025c7ad4066482221 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 18 Dec 2025 10:14:26 +0100 Subject: [PATCH 1/3] Unexport errors returned by the filer package --- libs/filer/dbfs_client.go | 20 +-- libs/filer/errors.go | 117 ++++++++++++++++++ libs/filer/errors_test.go | 96 ++++++++++++++ libs/filer/fake_filer.go | 2 +- libs/filer/filer.go | 94 -------------- libs/filer/files_client.go | 30 ++--- libs/filer/local_client.go | 20 +-- libs/filer/workspace_files_cache.go | 2 +- libs/filer/workspace_files_client.go | 26 ++-- .../workspace_files_extensions_client.go | 12 +- 10 files changed, 269 insertions(+), 150 deletions(-) create mode 100644 libs/filer/errors.go create mode 100644 libs/filer/errors_test.go diff --git a/libs/filer/dbfs_client.go b/libs/filer/dbfs_client.go index 7f7c4570d9..dfa6a6f7b8 100644 --- a/libs/filer/dbfs_client.go +++ b/libs/filer/dbfs_client.go @@ -106,7 +106,7 @@ func (w *DbfsClient) Write(ctx context.Context, name string, reader io.Reader, m // This API returns a 404 if the file doesn't exist. if aerr.StatusCode == http.StatusNotFound { if aerr.ErrorCode == "RESOURCE_DOES_NOT_EXIST" { - return NoSuchDirectoryError{path.Dir(absPath)} + return noSuchDirectoryError{path.Dir(absPath)} } } @@ -124,7 +124,7 @@ func (w *DbfsClient) Write(ctx context.Context, name string, reader io.Reader, m // This API returns a 400 if the file already exists. if aerr.StatusCode == http.StatusBadRequest { if aerr.ErrorCode == "RESOURCE_ALREADY_EXISTS" { - return FileAlreadyExistsError{absPath} + return fileAlreadyExistsError{absPath} } } @@ -150,7 +150,7 @@ func (w *DbfsClient) Read(ctx context.Context, name string) (io.ReadCloser, erro if err != nil { // Return error if file is a directory if strings.Contains(err.Error(), "cannot open directory for reading") { - return nil, NotAFile{absPath} + return nil, notAFile{absPath} } var aerr *apierr.APIError @@ -161,7 +161,7 @@ func (w *DbfsClient) Read(ctx context.Context, name string) (io.ReadCloser, erro // This API returns a 404 if the file doesn't exist. if aerr.StatusCode == http.StatusNotFound { if aerr.ErrorCode == "RESOURCE_DOES_NOT_EXIST" { - return nil, FileDoesNotExistError{absPath} + return nil, fileDoesNotExistError{absPath} } } @@ -180,7 +180,7 @@ func (w *DbfsClient) Delete(ctx context.Context, name string, mode ...DeleteMode // Illegal to delete the root path. if absPath == w.root.rootPath { - return CannotDeleteRootError{} + return cannotDeleteRootError{} } // Issue info call before delete because delete succeeds if the specified path doesn't exist. @@ -198,7 +198,7 @@ func (w *DbfsClient) Delete(ctx context.Context, name string, mode ...DeleteMode // This API returns a 404 if the file doesn't exist. if aerr.StatusCode == http.StatusNotFound { if aerr.ErrorCode == "RESOURCE_DOES_NOT_EXIST" { - return FileDoesNotExistError{absPath} + return fileDoesNotExistError{absPath} } } @@ -227,7 +227,7 @@ func (w *DbfsClient) Delete(ctx context.Context, name string, mode ...DeleteMode 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 directoryNotEmptyError{absPath} } } @@ -250,7 +250,7 @@ func (w *DbfsClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, e // This API returns a 404 if the file doesn't exist. if aerr.StatusCode == http.StatusNotFound { if aerr.ErrorCode == "RESOURCE_DOES_NOT_EXIST" { - return nil, NoSuchDirectoryError{absPath} + return nil, noSuchDirectoryError{absPath} } } @@ -258,7 +258,7 @@ func (w *DbfsClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, e } if len(res.Files) == 1 && res.Files[0].Path == absPath { - return nil, NotADirectory{absPath} + return nil, notADirectory{absPath} } info := make([]fs.DirEntry, len(res.Files)) @@ -296,7 +296,7 @@ func (w *DbfsClient) Stat(ctx context.Context, name string) (fs.FileInfo, error) // This API returns a 404 if the file doesn't exist. if aerr.StatusCode == http.StatusNotFound { if aerr.ErrorCode == "RESOURCE_DOES_NOT_EXIST" { - return nil, FileDoesNotExistError{absPath} + return nil, fileDoesNotExistError{absPath} } } diff --git a/libs/filer/errors.go b/libs/filer/errors.go new file mode 100644 index 0000000000..67f15ecb01 --- /dev/null +++ b/libs/filer/errors.go @@ -0,0 +1,117 @@ +package filer + +import "io/fs" + +// fileAlreadyExistsError is returned when attempting to write a file at a path +// that already exists, without using the OverwriteIfExists WriteMode flag. +type fileAlreadyExistsError struct { + path string +} + +func (err fileAlreadyExistsError) Error() string { + return "file already exists: " + err.path +} + +func (err fileAlreadyExistsError) Is(other error) bool { + return other == fs.ErrExist +} + +// fileDoesNotExistError is returned when attempting to read, delete, or stat +// a file that does not exist. It is also returned by the workspace files +// extensions client when a notebook exists at a path but a file was expected. +type fileDoesNotExistError struct { + path string +} + +func (err fileDoesNotExistError) Is(other error) bool { + return other == fs.ErrNotExist +} + +func (err fileDoesNotExistError) Error() string { + return "file does not exist: " + err.path +} + +// noSuchDirectoryError is returned when attempting to write a file to a path +// whose parent directory does not exist (without CreateParentDirectories mode), +// or when attempting to read a directory that does not exist. +type noSuchDirectoryError struct { + path string +} + +func (err noSuchDirectoryError) Error() string { + return "no such directory: " + err.path +} + +func (err noSuchDirectoryError) Is(other error) bool { + return other == fs.ErrNotExist +} + +// notADirectory is returned when attempting to read a directory (ReadDir) +// but the path points to a file instead of a directory. +type notADirectory struct { + path string +} + +func (err notADirectory) Error() string { + return "not a directory: " + err.path +} + +func (err notADirectory) Is(other error) bool { + return other == fs.ErrInvalid +} + +// notAFile is returned when attempting to read a file but the path points +// to a directory instead of a file. +type notAFile struct { + path string +} + +func (err notAFile) Error() string { + return "not a file: " + err.path +} + +func (err notAFile) Is(other error) bool { + return other == fs.ErrInvalid +} + +// directoryNotEmptyError is returned when attempting to delete a directory +// that contains files or subdirectories, without using the DeleteRecursively +// DeleteMode flag. +type directoryNotEmptyError struct { + path string +} + +func (err directoryNotEmptyError) Error() string { + return "directory not empty: " + err.path +} + +func (err directoryNotEmptyError) Is(other error) bool { + return other == fs.ErrInvalid +} + +// cannotDeleteRootError is returned when attempting to delete the root path +// of the filer. Deleting the root is not allowed as it would break subsequent +// file operations. +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 +} + +// permissionError is returned when access is denied to a path, for example +// when attempting to create a directory but lacking write permissions. +type permissionError struct { + path string +} + +func (err permissionError) Error() string { + return "access denied: " + err.path +} + +func (err permissionError) Is(other error) bool { + return other == fs.ErrPermission +} diff --git a/libs/filer/errors_test.go b/libs/filer/errors_test.go new file mode 100644 index 0000000000..a1fb066e09 --- /dev/null +++ b/libs/filer/errors_test.go @@ -0,0 +1,96 @@ +package filer + +import ( + "io/fs" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileAlreadyExistsError_Is(t *testing.T) { + err := fileAlreadyExistsError{path: "/test/path"} + assert.ErrorIs(t, err, fs.ErrExist) + assert.NotErrorIs(t, err, fs.ErrNotExist) +} + +func TestFileAlreadyExistsError_Error(t *testing.T) { + err := fileAlreadyExistsError{path: "/test/path"} + assert.Equal(t, "file already exists: /test/path", err.Error()) +} + +func TestFileDoesNotExistError_Is(t *testing.T) { + err := fileDoesNotExistError{path: "/test/path"} + assert.ErrorIs(t, err, fs.ErrNotExist) + assert.NotErrorIs(t, err, fs.ErrExist) +} + +func TestFileDoesNotExistError_Error(t *testing.T) { + err := fileDoesNotExistError{path: "/test/path"} + assert.Equal(t, "file does not exist: /test/path", err.Error()) +} + +func TestNoSuchDirectoryError_Is(t *testing.T) { + err := noSuchDirectoryError{path: "/test/path"} + assert.ErrorIs(t, err, fs.ErrNotExist) + assert.NotErrorIs(t, err, fs.ErrExist) +} + +func TestNoSuchDirectoryError_Error(t *testing.T) { + err := noSuchDirectoryError{path: "/test/path"} + assert.Equal(t, "no such directory: /test/path", err.Error()) +} + +func TestNotADirectory_Is(t *testing.T) { + err := notADirectory{path: "/test/path"} + assert.ErrorIs(t, err, fs.ErrInvalid) + assert.NotErrorIs(t, err, fs.ErrNotExist) +} + +func TestNotADirectory_Error(t *testing.T) { + err := notADirectory{path: "/test/path"} + assert.Equal(t, "not a directory: /test/path", err.Error()) +} + +func TestNotAFile_Is(t *testing.T) { + err := notAFile{path: "/test/path"} + assert.ErrorIs(t, err, fs.ErrInvalid) + assert.NotErrorIs(t, err, fs.ErrNotExist) +} + +func TestNotAFile_Error(t *testing.T) { + err := notAFile{path: "/test/path"} + assert.Equal(t, "not a file: /test/path", err.Error()) +} + +func TestDirectoryNotEmptyError_Is(t *testing.T) { + err := directoryNotEmptyError{path: "/test/path"} + assert.ErrorIs(t, err, fs.ErrInvalid) + assert.NotErrorIs(t, err, fs.ErrNotExist) +} + +func TestDirectoryNotEmptyError_Error(t *testing.T) { + err := directoryNotEmptyError{path: "/test/path"} + assert.Equal(t, "directory not empty: /test/path", err.Error()) +} + +func TestCannotDeleteRootError_Is(t *testing.T) { + err := cannotDeleteRootError{} + assert.ErrorIs(t, err, fs.ErrInvalid) + assert.NotErrorIs(t, err, fs.ErrNotExist) +} + +func TestCannotDeleteRootError_Error(t *testing.T) { + err := cannotDeleteRootError{} + assert.Equal(t, "unable to delete filer root", err.Error()) +} + +func TestPermissionError_Is(t *testing.T) { + err := permissionError{path: "/test/path"} + assert.ErrorIs(t, err, fs.ErrPermission) + assert.NotErrorIs(t, err, fs.ErrNotExist) +} + +func TestPermissionError_Error(t *testing.T) { + err := permissionError{path: "/test/path"} + assert.Equal(t, "access denied: /test/path", err.Error()) +} diff --git a/libs/filer/fake_filer.go b/libs/filer/fake_filer.go index 1e1cbd9852..f6eb5955ee 100644 --- a/libs/filer/fake_filer.go +++ b/libs/filer/fake_filer.go @@ -37,7 +37,7 @@ func (f *FakeFiler) ReadDir(ctx context.Context, p string) ([]fs.DirEntry, error p = strings.TrimSuffix(p, "/") entry, ok := f.entries[p] if !ok { - return nil, NoSuchDirectoryError{p} + return nil, noSuchDirectoryError{p} } if !entry.FakeDir { diff --git a/libs/filer/filer.go b/libs/filer/filer.go index 372c829297..4ad1d97b95 100644 --- a/libs/filer/filer.go +++ b/libs/filer/filer.go @@ -30,100 +30,6 @@ const ( DeleteRecursively DeleteMode = 1 << iota ) -type FileAlreadyExistsError struct { - path string -} - -func (err FileAlreadyExistsError) Error() string { - return "file already exists: " + err.path -} - -func (err FileAlreadyExistsError) Is(other error) bool { - return other == fs.ErrExist -} - -type FileDoesNotExistError struct { - path string -} - -func (err FileDoesNotExistError) Is(other error) bool { - return other == fs.ErrNotExist -} - -func (err FileDoesNotExistError) Error() string { - return "file does not exist: " + err.path -} - -type NoSuchDirectoryError struct { - path string -} - -func (err NoSuchDirectoryError) Error() string { - return "no such directory: " + err.path -} - -func (err NoSuchDirectoryError) Is(other error) bool { - return other == fs.ErrNotExist -} - -type NotADirectory struct { - path string -} - -func (err NotADirectory) Error() string { - return "not a directory: " + err.path -} - -func (err NotADirectory) Is(other error) bool { - return other == fs.ErrInvalid -} - -type NotAFile struct { - path string -} - -func (err NotAFile) Error() string { - return "not a file: " + err.path -} - -func (err NotAFile) Is(other error) bool { - return other == fs.ErrInvalid -} - -type DirectoryNotEmptyError struct { - path string -} - -func (err DirectoryNotEmptyError) Error() string { - return "directory not empty: " + 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 -} - -type PermissionError struct { - path string -} - -func (err PermissionError) Error() string { - return "access denied: " + err.path -} - -func (err PermissionError) Is(other error) bool { - return other == fs.ErrPermission -} - // Filer is used to access files in a workspace. // It has implementations for accessing files in WSFS and in DBFS. type Filer interface { diff --git a/libs/filer/files_client.go b/libs/filer/files_client.go index 2594d35a39..4160b9b209 100644 --- a/libs/filer/files_client.go +++ b/libs/filer/files_client.go @@ -138,7 +138,7 @@ func (w *FilesClient) Write(ctx context.Context, name string, reader io.Reader, // This API returns a 404 if the file doesn't exist. if aerr.StatusCode == http.StatusNotFound { - return NoSuchDirectoryError{path.Dir(absPath)} + return noSuchDirectoryError{path.Dir(absPath)} } return err @@ -163,7 +163,7 @@ func (w *FilesClient) Write(ctx context.Context, name string, reader io.Reader, // This API returns 409 if the file already exists, when the object type is file if aerr.StatusCode == http.StatusConflict && aerr.ErrorCode == "ALREADY_EXISTS" { - return FileAlreadyExistsError{absPath} + return fileAlreadyExistsError{absPath} } return err @@ -193,11 +193,11 @@ func (w *FilesClient) Read(ctx context.Context, name string) (io.ReadCloser, err if aerr.StatusCode == http.StatusNotFound { // Check if the path is a directory. If so, return not a file error. if _, err := w.statDir(ctx, name); err == nil { - return nil, NotAFile{absPath} + return nil, notAFile{absPath} } // No file or directory exists at the specified path. Return no such file error. - return nil, FileDoesNotExistError{absPath} + return nil, fileDoesNotExistError{absPath} } return nil, err @@ -211,7 +211,7 @@ func (w *FilesClient) deleteFile(ctx context.Context, name string) error { // Illegal to delete the root path. if absPath == w.root.rootPath { - return CannotDeleteRootError{} + return cannotDeleteRootError{} } err = w.workspaceClient.Files.DeleteByFilePath(ctx, absPath) @@ -229,7 +229,7 @@ func (w *FilesClient) deleteFile(ctx context.Context, name string) error { // This files delete API returns a 404 if the specified path does not exist. if aerr.StatusCode == http.StatusNotFound { - return FileDoesNotExistError{absPath} + return fileDoesNotExistError{absPath} } return err @@ -243,7 +243,7 @@ func (w *FilesClient) deleteDirectory(ctx context.Context, name string) error { // Illegal to delete the root path. if absPath == w.root.rootPath { - return CannotDeleteRootError{} + return cannotDeleteRootError{} } err = w.workspaceClient.Files.DeleteDirectoryByDirectoryPath(ctx, absPath) @@ -266,7 +266,7 @@ func (w *FilesClient) deleteDirectory(ctx context.Context, name string) error { if !slices.Contains(reasons, "FILES_API_DIRECTORY_IS_NOT_EMPTY") { return err } - return DirectoryNotEmptyError{absPath} + return directoryNotEmptyError{absPath} } return err @@ -395,11 +395,11 @@ func (w *FilesClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, if apierr.StatusCode == http.StatusNotFound { // Check if the path is a file. If so, return not a directory error. if _, err := w.statFile(ctx, name); err == nil { - return nil, NotADirectory{absPath} + return nil, notADirectory{absPath} } // No file or directory exists at the specified path. Return no such directory error. - return nil, NoSuchDirectoryError{absPath} + return nil, noSuchDirectoryError{absPath} } return nil, err } @@ -417,7 +417,7 @@ func (w *FilesClient) Mkdir(ctx context.Context, name string) error { // Special handling of this error only if it is an API error. var aerr *apierr.APIError if errors.As(err, &aerr) && aerr.StatusCode == http.StatusConflict { - return FileAlreadyExistsError{absPath} + return fileAlreadyExistsError{absPath} } return err @@ -449,7 +449,7 @@ func (w *FilesClient) statFile(ctx context.Context, name string) (fs.FileInfo, e // This API returns a 404 if the specified path does not exist. if aerr.StatusCode == http.StatusNotFound { - return nil, FileDoesNotExistError{absPath} + return nil, fileDoesNotExistError{absPath} } return nil, err @@ -477,7 +477,7 @@ func (w *FilesClient) statDir(ctx context.Context, name string) (fs.FileInfo, er // The directory metadata API returns a 404 if the specified path does not exist. if aerr.StatusCode == http.StatusNotFound { - return nil, NoSuchDirectoryError{absPath} + return nil, noSuchDirectoryError{absPath} } return nil, err @@ -492,8 +492,8 @@ func (w *FilesClient) Stat(ctx context.Context, name string) (fs.FileInfo, error return dirInfo, nil } - // Return early if the error is not a NoSuchDirectoryError. - if !errors.As(err, &NoSuchDirectoryError{}) { + // Return early if the error is not a noSuchDirectoryError. + if !errors.Is(err, fs.ErrNotExist) { return nil, err } diff --git a/libs/filer/local_client.go b/libs/filer/local_client.go index 385aa69247..ded6fc8e2b 100644 --- a/libs/filer/local_client.go +++ b/libs/filer/local_client.go @@ -58,9 +58,9 @@ func (w *LocalClient) Write(ctx context.Context, name string, reader io.Reader, if err != nil { switch { case errors.Is(err, fs.ErrNotExist): - return NoSuchDirectoryError{path: absPath} + return noSuchDirectoryError{path: absPath} case errors.Is(err, fs.ErrExist): - return FileAlreadyExistsError{path: absPath} + return fileAlreadyExistsError{path: absPath} default: return err } @@ -87,13 +87,13 @@ func (w *LocalClient) Read(ctx context.Context, name string) (io.ReadCloser, err stat, err := os.Stat(absPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { - return nil, FileDoesNotExistError{path: absPath} + return nil, fileDoesNotExistError{path: absPath} } return nil, err } if stat.IsDir() { - return nil, NotAFile{path: absPath} + return nil, notAFile{path: absPath} } return os.Open(absPath) @@ -107,7 +107,7 @@ func (w *LocalClient) Delete(ctx context.Context, name string, mode ...DeleteMod // Illegal to delete the root path. if absPath == w.root.rootPath { - return CannotDeleteRootError{} + return cannotDeleteRootError{} } err = os.Remove(absPath) @@ -118,14 +118,14 @@ func (w *LocalClient) Delete(ctx context.Context, name string, mode ...DeleteMod } if errors.Is(err, fs.ErrNotExist) { - return FileDoesNotExistError{path: absPath} + return fileDoesNotExistError{path: absPath} } if errors.Is(err, fs.ErrExist) { if slices.Contains(mode, DeleteRecursively) { return os.RemoveAll(absPath) } - return DirectoryNotEmptyError{path: absPath} + return directoryNotEmptyError{path: absPath} } return err @@ -140,13 +140,13 @@ func (w *LocalClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, stat, err := os.Stat(absPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { - return nil, NoSuchDirectoryError{path: absPath} + return nil, noSuchDirectoryError{path: absPath} } return nil, err } if !stat.IsDir() { - return nil, NotADirectory{path: absPath} + return nil, notADirectory{path: absPath} } return os.ReadDir(absPath) @@ -169,7 +169,7 @@ func (w *LocalClient) Stat(ctx context.Context, name string) (fs.FileInfo, error stat, err := os.Stat(absPath) if errors.Is(err, fs.ErrNotExist) { - return nil, FileDoesNotExistError{path: absPath} + return nil, fileDoesNotExistError{path: absPath} } return stat, err } diff --git a/libs/filer/workspace_files_cache.go b/libs/filer/workspace_files_cache.go index 5837ad27dc..7b822d5feb 100644 --- a/libs/filer/workspace_files_cache.go +++ b/libs/filer/workspace_files_cache.go @@ -385,7 +385,7 @@ func (c *cache) statFromReadDir(ctx context.Context, name string, entry *readDir return e.wait(ctx) } - return nil, FileDoesNotExistError{name} + return nil, fileDoesNotExistError{name} } // Stat returns the file info for a file or directory. diff --git a/libs/filer/workspace_files_client.go b/libs/filer/workspace_files_client.go index 6acd066808..eb09d532e3 100644 --- a/libs/filer/workspace_files_client.go +++ b/libs/filer/workspace_files_client.go @@ -172,14 +172,14 @@ func (w *WorkspaceFilesClient) Write(ctx context.Context, name string, reader io // This API returns a 404 if the parent directory does not exist. if aerr.StatusCode == http.StatusNotFound { if !slices.Contains(mode, CreateParentDirectories) { - return NoSuchDirectoryError{path.Dir(absPath)} + return noSuchDirectoryError{path.Dir(absPath)} } // Create parent directory. err = w.workspaceClient.Workspace.MkdirsByPath(ctx, path.Dir(absPath)) if err != nil { if errors.As(err, &aerr) && aerr.StatusCode == http.StatusForbidden { - return PermissionError{absPath} + return permissionError{absPath} } return fmt.Errorf("unable to mkdir to write file %s: %w", absPath, err) } @@ -190,7 +190,7 @@ func (w *WorkspaceFilesClient) Write(ctx context.Context, name string, reader io // This API returns 409 if the file already exists, when the object type is file if aerr.StatusCode == http.StatusConflict { - return FileAlreadyExistsError{absPath} + return fileAlreadyExistsError{absPath} } // This API returns 400 if the file already exists, when the object type is notebook @@ -199,16 +199,16 @@ func (w *WorkspaceFilesClient) Write(ctx context.Context, name string, reader io // Parse file path from regex capture group matches := regex.FindStringSubmatch(aerr.Message) if len(matches) == 2 { - return FileAlreadyExistsError{matches[1]} + return fileAlreadyExistsError{matches[1]} } // Default to path specified to filer.Write if regex capture fails - return FileAlreadyExistsError{absPath} + return fileAlreadyExistsError{absPath} } // This API returns StatusForbidden when you have read access but don't have write access to a file if aerr.StatusCode == http.StatusForbidden { - return PermissionError{absPath} + return permissionError{absPath} } return err @@ -230,7 +230,7 @@ func (w *WorkspaceFilesClient) Read(ctx context.Context, name string) (io.ReadCl return nil, err } if stat.IsDir() { - return nil, NotAFile{absPath} + return nil, notAFile{absPath} } // Export file contents. Note the /workspace/export API has a limit of 10MBs @@ -246,7 +246,7 @@ func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string, mode ... // Illegal to delete the root path. if absPath == w.root.rootPath { - return CannotDeleteRootError{} + return cannotDeleteRootError{} } recursive := slices.Contains(mode, DeleteRecursively) @@ -270,10 +270,10 @@ func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string, mode ... switch aerr.StatusCode { case http.StatusBadRequest: if aerr.ErrorCode == "DIRECTORY_NOT_EMPTY" { - return DirectoryNotEmptyError{absPath} + return directoryNotEmptyError{absPath} } case http.StatusNotFound: - return FileDoesNotExistError{absPath} + return fileDoesNotExistError{absPath} } return err @@ -290,7 +290,7 @@ func (w *WorkspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.D }) if len(objects) == 1 && objects[0].Path == absPath { - return nil, NotADirectory{absPath} + return nil, notADirectory{absPath} } if err != nil { @@ -303,7 +303,7 @@ func (w *WorkspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.D // NOTE: This API returns a 404 if the specified path does not exist, // but can also do so if we don't have read access. if aerr.StatusCode == http.StatusNotFound { - return nil, NoSuchDirectoryError{path.Dir(absPath)} + return nil, noSuchDirectoryError{path.Dir(absPath)} } return nil, err } @@ -354,7 +354,7 @@ func (w *WorkspaceFilesClient) Stat(ctx context.Context, name string) (fs.FileIn // This API returns a 404 if the specified path does not exist. if aerr.StatusCode == http.StatusNotFound { - return nil, FileDoesNotExistError{absPath} + return nil, fileDoesNotExistError{absPath} } } diff --git a/libs/filer/workspace_files_extensions_client.go b/libs/filer/workspace_files_extensions_client.go index 0127d180c6..debc8582eb 100644 --- a/libs/filer/workspace_files_extensions_client.go +++ b/libs/filer/workspace_files_extensions_client.go @@ -61,7 +61,7 @@ func (w *WorkspaceFilesExtensionsClient) getNotebookStatByNameWithExt(ctx contex stat, err := w.stat(ctx, nameWithoutExt) if err != nil { // If the file does not exist, return early. - if errors.As(err, &FileDoesNotExistError{}) { + if errors.Is(err, fs.ErrNotExist) { return nil, nil } log.Debugf(ctx, "attempting to determine if %s could be a notebook. Failed to fetch the status of object at %s: %s", name, path.Join(w.root, nameWithoutExt), err) @@ -259,7 +259,7 @@ func (w *WorkspaceFilesExtensionsClient) Read(ctx context.Context, name string) r, err := w.wsfs.Read(ctx, name) // If the file is not found, it might be a notebook. - if errors.As(err, &FileDoesNotExistError{}) { + if errors.Is(err, fs.ErrNotExist) { stat, serr := w.getNotebookStatByNameWithExt(ctx, name) if serr != nil { // Unable to stat. Return the stat error. @@ -302,7 +302,7 @@ func (w *WorkspaceFilesExtensionsClient) Delete(ctx context.Context, name string err = w.wsfs.Delete(ctx, name, mode...) // If the file is not found, it might be a notebook. - if errors.As(err, &FileDoesNotExistError{}) { + if errors.Is(err, fs.ErrNotExist) { stat, serr := w.getNotebookStatByNameWithExt(ctx, name) if serr != nil { // Unable to stat. Return the stat error. @@ -324,7 +324,7 @@ func (w *WorkspaceFilesExtensionsClient) Stat(ctx context.Context, name string) info, err := w.wsfs.Stat(ctx, name) // If the file is not found, it might be a notebook. - if errors.As(err, &FileDoesNotExistError{}) { + if errors.Is(err, fs.ErrNotExist) { stat, serr := w.getNotebookStatByNameWithExt(ctx, name) if serr != nil { // Unable to stat. Return the stat error. @@ -342,7 +342,7 @@ func (w *WorkspaceFilesExtensionsClient) Stat(ctx context.Context, name string) return nil, err } - // If an object is found and it is a notebook, return a FileDoesNotExistError. + // If an object is found and it is a notebook, return a fileDoesNotExistError. // If a notebook is found by the workspace files client, without having stripped // the extension, this implies that no file with the same name exists. // @@ -352,7 +352,7 @@ func (w *WorkspaceFilesExtensionsClient) Stat(ctx context.Context, name string) // To stat the metadata of a notebook called `foo` in the workspace the user // should use the name with the extension included like `foo.ipynb` or `foo.sql`. if info.Sys().(workspace.ObjectInfo).ObjectType == workspace.ObjectTypeNotebook { - return nil, FileDoesNotExistError{name} + return nil, fileDoesNotExistError{name} } return info, nil From 7fb4c9d1e46efebba11fae5c3b71d89dff14aeab Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 18 Dec 2025 10:20:00 +0100 Subject: [PATCH 2/3] Missing diffs --- bundle/deploy/lock/acquire.go | 4 +--- experimental/ssh/internal/client/releases.go | 3 ++- integration/cmd/fs/cat_test.go | 2 +- integration/cmd/fs/ls_test.go | 2 +- integration/cmd/fs/mkdir_test.go | 4 ++-- integration/cmd/fs/rm_test.go | 1 - integration/libs/filer/filer_test.go | 12 ++---------- libs/locker/locker.go | 2 +- 8 files changed, 10 insertions(+), 20 deletions(-) diff --git a/bundle/deploy/lock/acquire.go b/bundle/deploy/lock/acquire.go index ab1f1cba16..d4f788c3ca 100644 --- a/bundle/deploy/lock/acquire.go +++ b/bundle/deploy/lock/acquire.go @@ -8,7 +8,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/permissions" "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/filer" "github.com/databricks/cli/libs/locker" "github.com/databricks/cli/libs/log" ) @@ -57,8 +56,7 @@ func (m *acquire) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.StatePath) } - notExistsError := filer.NoSuchDirectoryError{} - if errors.As(err, ¬ExistsError) { + if errors.Is(err, fs.ErrNotExist) { // If we get a "doesn't exist" error from the API this indicates // we either don't have permissions or the path is invalid. return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.StatePath) diff --git a/experimental/ssh/internal/client/releases.go b/experimental/ssh/internal/client/releases.go index 89f64e742f..f147244e9e 100644 --- a/experimental/ssh/internal/client/releases.go +++ b/experimental/ssh/internal/client/releases.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" "os" "path/filepath" @@ -49,7 +50,7 @@ func uploadReleases(ctx context.Context, workspaceFiler filer.Filer, getRelease if err == nil { cmdio.LogString(ctx, fmt.Sprintf("File %s already exists in the workspace, skipping upload", remoteBinaryPath)) continue - } else if !errors.As(err, &filer.FileDoesNotExistError{}) { + } else if !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("failed to check if file %s exists in workspace: %w", remoteBinaryPath, err) } diff --git a/integration/cmd/fs/cat_test.go b/integration/cmd/fs/cat_test.go index 14ec8140e2..6f5ea4ded3 100644 --- a/integration/cmd/fs/cat_test.go +++ b/integration/cmd/fs/cat_test.go @@ -48,7 +48,7 @@ func TestFsCatOnADir(t *testing.T) { require.NoError(t, err) _, _, err = testcli.RequireErrorRun(t, ctx, "fs", "cat", path.Join(tmpDir, "dir1")) - assert.ErrorAs(t, err, &filer.NotAFile{}) + assert.ErrorIs(t, err, fs.ErrInvalid) }) } } diff --git a/integration/cmd/fs/ls_test.go b/integration/cmd/fs/ls_test.go index 0f53193bf9..d4d7cedef7 100644 --- a/integration/cmd/fs/ls_test.go +++ b/integration/cmd/fs/ls_test.go @@ -116,7 +116,7 @@ func TestFsLsOnFile(t *testing.T) { _, _, err := testcli.RequireErrorRun(t, ctx, "fs", "ls", path.Join(tmpDir, "a", "hello.txt"), "--output=json") assert.Regexp(t, "not a directory: .*/a/hello.txt", err.Error()) - assert.ErrorAs(t, err, &filer.NotADirectory{}) + assert.ErrorIs(t, err, fs.ErrInvalid) }) } } diff --git a/integration/cmd/fs/mkdir_test.go b/integration/cmd/fs/mkdir_test.go index 5cea0599c6..9b78748570 100644 --- a/integration/cmd/fs/mkdir_test.go +++ b/integration/cmd/fs/mkdir_test.go @@ -2,13 +2,13 @@ package fs_test import ( "context" + "io/fs" "path" "regexp" "strings" "testing" "github.com/databricks/cli/internal/testcli" - "github.com/databricks/cli/libs/filer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -129,6 +129,6 @@ func TestFsMkdirWhenFileExistsAtPath(t *testing.T) { // assert mkdir fails _, _, err = testcli.RequireErrorRun(t, ctx, "fs", "mkdir", path.Join(tmpDir, "hello")) - assert.ErrorAs(t, err, &filer.FileAlreadyExistsError{}) + assert.ErrorIs(t, err, fs.ErrExist) }) } diff --git a/integration/cmd/fs/rm_test.go b/integration/cmd/fs/rm_test.go index fc19bb5b53..736172f94b 100644 --- a/integration/cmd/fs/rm_test.go +++ b/integration/cmd/fs/rm_test.go @@ -95,7 +95,6 @@ func TestFsRmNonEmptyDirectory(t *testing.T) { // Run rm command _, _, err = testcli.RequireErrorRun(t, ctx, "fs", "rm", path.Join(tmpDir, "a")) assert.ErrorIs(t, err, fs.ErrInvalid) - assert.ErrorAs(t, err, &filer.DirectoryNotEmptyError{}) }) } } diff --git a/integration/libs/filer/filer_test.go b/integration/libs/filer/filer_test.go index e67495e2de..2222e4b56c 100644 --- a/integration/libs/filer/filer_test.go +++ b/integration/libs/filer/filer_test.go @@ -107,12 +107,12 @@ func commonFilerRecursiveDeleteTest(t *testing.T, ctx context.Context, f filer.F assert.Equal(t, []string{"file1", "file2", "subdir1", "subdir2"}, names) err = f.Delete(ctx, "dir") - assert.ErrorAs(t, err, &filer.DirectoryNotEmptyError{}) + assert.ErrorIs(t, err, fs.ErrInvalid) err = f.Delete(ctx, "dir", filer.DeleteRecursively) assert.NoError(t, err) _, err = f.ReadDir(ctx, "dir") - assert.ErrorAs(t, err, &filer.NoSuchDirectoryError{}) + assert.ErrorIs(t, err, fs.ErrNotExist) } func TestFilerRecursiveDelete(t *testing.T) { @@ -145,12 +145,10 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Write should fail because the intermediate directory doesn't exist. err = f.Write(ctx, "/foo/bar", strings.NewReader(`hello world`)) - assert.ErrorAs(t, err, &filer.NoSuchDirectoryError{}) assert.ErrorIs(t, err, fs.ErrNotExist) // Read should fail because the intermediate directory doesn't yet exist. _, err = f.Read(ctx, "/foo/bar") - assert.ErrorAs(t, err, &filer.FileDoesNotExistError{}) assert.ErrorIs(t, err, fs.ErrNotExist) // Read should fail because the path points to a directory @@ -166,7 +164,6 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Write should fail because there is an existing file at the specified path. err = f.Write(ctx, "/foo/bar", strings.NewReader(`hello universe`)) - assert.ErrorAs(t, err, &filer.FileAlreadyExistsError{}) assert.ErrorIs(t, err, fs.ErrExist) // Write with OverwriteIfExists should succeed. @@ -196,12 +193,10 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Delete should fail if the file doesn't exist. err = f.Delete(ctx, "/doesnt_exist") - assert.ErrorAs(t, err, &filer.FileDoesNotExistError{}) assert.ErrorIs(t, err, fs.ErrNotExist) // Stat should fail if the file doesn't exist. _, err = f.Stat(ctx, "/doesnt_exist") - assert.ErrorAs(t, err, &filer.FileDoesNotExistError{}) assert.ErrorIs(t, err, fs.ErrNotExist) // Delete should succeed for file that does exist. @@ -210,7 +205,6 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Delete should fail for a non-empty directory. err = f.Delete(ctx, "/foo") - assert.ErrorAs(t, err, &filer.DirectoryNotEmptyError{}) assert.ErrorIs(t, err, fs.ErrInvalid) // Delete should succeed for a non-empty directory if the DeleteRecursively flag is set. @@ -220,7 +214,6 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // 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.ErrorAs(t, err, &filer.CannotDeleteRootError{}) assert.ErrorIs(t, err, fs.ErrInvalid) } @@ -276,7 +269,6 @@ func commonFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { // Expect an error if the path doesn't exist. _, err = f.ReadDir(ctx, "/dir/a/b/c/d/e") - assert.ErrorAs(t, err, &filer.NoSuchDirectoryError{}, err) assert.ErrorIs(t, err, fs.ErrNotExist) // Expect two entries in the root. diff --git a/libs/locker/locker.go b/libs/locker/locker.go index aadc50b587..003f169cd3 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -156,7 +156,7 @@ func (locker *Locker) Lock(ctx context.Context, isForced bool) error { // the error and instead fall through to [assertLockHeld] below. // This function will return a more descriptive error message that includes // details about the current holder of the lock. - if !errors.As(err, &filer.FileAlreadyExistsError{}) { + if !errors.Is(err, fs.ErrExist) { return err } } From fb496110406a7aa259a037330064ce7a862f4266 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 18 Dec 2025 11:11:11 +0100 Subject: [PATCH 3/3] Fix --- libs/filer/dbfs_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/filer/dbfs_client.go b/libs/filer/dbfs_client.go index 1ae1119901..d7d79207fd 100644 --- a/libs/filer/dbfs_client.go +++ b/libs/filer/dbfs_client.go @@ -233,7 +233,7 @@ func (w *DbfsClient) Delete(ctx context.Context, name string, mode ...DeleteMode // Since 17th december we are observing the backend return an error_code of BAD_REQUEST // instead of IO_ERROR. Doing direct comparison on the error string instead. if strings.Contains(aerr.Message, "Directory is not empty. Use recursive delete or remove contents first.") { - return DirectoryNotEmptyError{absPath} + return directoryNotEmptyError{absPath} } }