mirror of https://github.com/databricks/cli.git
Add Mkdir and ReadDir functions to filer.Filer interface (#414)
## Changes This cherry-picks the filer changes from #408. ## Tests Manually ran integration tests.
This commit is contained in:
parent
05eaf7ff50
commit
92cb52041d
|
@ -65,7 +65,7 @@ func temporaryWorkspaceDir(t *testing.T, w *databricks.WorkspaceClient) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccFilerWorkspaceFiles(t *testing.T) {
|
func setupWorkspaceFilesTest(t *testing.T) (context.Context, filer.Filer) {
|
||||||
t.Log(GetEnvOrSkipTest(t, "CLOUD_ENV"))
|
t.Log(GetEnvOrSkipTest(t, "CLOUD_ENV"))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -81,6 +81,14 @@ func TestAccFilerWorkspaceFiles(t *testing.T) {
|
||||||
t.Skip(aerr.Message)
|
t.Skip(aerr.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ctx, f
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccFilerWorkspaceFilesReadWrite(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx, f := setupWorkspaceFilesTest(t)
|
||||||
|
|
||||||
// Write should fail because the root path doesn't yet exist.
|
// Write should fail because the root path doesn't yet exist.
|
||||||
err = f.Write(ctx, "/foo/bar", strings.NewReader(`hello world`))
|
err = f.Write(ctx, "/foo/bar", strings.NewReader(`hello world`))
|
||||||
assert.True(t, errors.As(err, &filer.NoSuchDirectoryError{}))
|
assert.True(t, errors.As(err, &filer.NoSuchDirectoryError{}))
|
||||||
|
@ -111,3 +119,56 @@ func TestAccFilerWorkspaceFiles(t *testing.T) {
|
||||||
err = f.Delete(ctx, "/foo/bar")
|
err = f.Delete(ctx, "/foo/bar")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccFilerWorkspaceFilesReadDir(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx, f := setupWorkspaceFilesTest(t)
|
||||||
|
|
||||||
|
// We start with an empty directory.
|
||||||
|
entries, err := f.ReadDir(ctx, ".")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, entries, 0)
|
||||||
|
|
||||||
|
// Write a file.
|
||||||
|
err = f.Write(ctx, "/hello.txt", strings.NewReader(`hello world`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a directory.
|
||||||
|
err = f.Mkdir(ctx, "/dir")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Write a file.
|
||||||
|
err = f.Write(ctx, "/dir/world.txt", strings.NewReader(`hello world`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a nested directory (check that it creates intermediate directories).
|
||||||
|
err = f.Mkdir(ctx, "/dir/a/b/c")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Expect an error if the path doesn't exist.
|
||||||
|
_, err = f.ReadDir(ctx, "/dir/a/b/c/d/e")
|
||||||
|
assert.True(t, errors.As(err, &filer.NoSuchDirectoryError{}))
|
||||||
|
|
||||||
|
// Expect two entries in the root.
|
||||||
|
entries, err = f.ReadDir(ctx, ".")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, entries, 2)
|
||||||
|
assert.Equal(t, "dir", entries[0].Name)
|
||||||
|
assert.Equal(t, "hello.txt", entries[1].Name)
|
||||||
|
assert.Greater(t, entries[1].ModTime.Unix(), int64(0))
|
||||||
|
|
||||||
|
// Expect two entries in the directory.
|
||||||
|
entries, err = f.ReadDir(ctx, "/dir")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, entries, 2)
|
||||||
|
assert.Equal(t, "a", entries[0].Name)
|
||||||
|
assert.Equal(t, "world.txt", entries[1].Name)
|
||||||
|
assert.Greater(t, entries[1].ModTime.Unix(), int64(0))
|
||||||
|
|
||||||
|
// Expect a single entry in the nested path.
|
||||||
|
entries, err = f.ReadDir(ctx, "/dir/a/b")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, entries, 1)
|
||||||
|
assert.Equal(t, "c", entries[0].Name)
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WriteMode int
|
type WriteMode int
|
||||||
|
@ -13,6 +14,22 @@ const (
|
||||||
CreateParentDirectories = iota << 1
|
CreateParentDirectories = iota << 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FileInfo abstracts over file information from different file systems.
|
||||||
|
// Inspired by https://pkg.go.dev/io/fs#FileInfo.
|
||||||
|
type FileInfo struct {
|
||||||
|
// The type of the file in workspace.
|
||||||
|
Type string
|
||||||
|
|
||||||
|
// Base name.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Size in bytes.
|
||||||
|
Size int64
|
||||||
|
|
||||||
|
// Modification time.
|
||||||
|
ModTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type FileAlreadyExistsError struct {
|
type FileAlreadyExistsError struct {
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
@ -41,4 +58,10 @@ type Filer interface {
|
||||||
|
|
||||||
// Delete file at `path`.
|
// Delete file at `path`.
|
||||||
Delete(ctx context.Context, path string) error
|
Delete(ctx context.Context, path string) error
|
||||||
|
|
||||||
|
// Return contents of directory at `path`.
|
||||||
|
ReadDir(ctx context.Context, path string) ([]FileInfo, error)
|
||||||
|
|
||||||
|
// Creates directory at `path`, creating any intermediate directories as required.
|
||||||
|
Mkdir(ctx context.Context, path string) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,5 @@ func (p *RootPath) Join(name string) (string, error) {
|
||||||
return "", fmt.Errorf("relative path escapes root: %s", name)
|
return "", fmt.Errorf("relative path escapes root: %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow name to resolve to the root path.
|
|
||||||
if strings.TrimPrefix(absPath, p.rootPath) == "" {
|
|
||||||
return "", fmt.Errorf("relative path resolves to root: %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return absPath, nil
|
return absPath, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,26 @@ func testRootPath(t *testing.T, uncleanRoot string) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, cleanRoot+"/a/b/f/g", remotePath)
|
assert.Equal(t, cleanRoot+"/a/b/f/g", remotePath)
|
||||||
|
|
||||||
|
remotePath, err = rp.Join(".//a/..//./b/..")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, cleanRoot, remotePath)
|
||||||
|
|
||||||
|
remotePath, err = rp.Join("a/b/../..")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, cleanRoot, remotePath)
|
||||||
|
|
||||||
|
remotePath, err = rp.Join("")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, cleanRoot, remotePath)
|
||||||
|
|
||||||
|
remotePath, err = rp.Join(".")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, cleanRoot, remotePath)
|
||||||
|
|
||||||
|
remotePath, err = rp.Join("/")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, cleanRoot, remotePath)
|
||||||
|
|
||||||
_, err = rp.Join("..")
|
_, err = rp.Join("..")
|
||||||
assert.ErrorContains(t, err, `relative path escapes root: ..`)
|
assert.ErrorContains(t, err, `relative path escapes root: ..`)
|
||||||
|
|
||||||
|
@ -57,21 +77,6 @@ func testRootPath(t *testing.T, uncleanRoot string) {
|
||||||
|
|
||||||
_, err = rp.Join("../..")
|
_, err = rp.Join("../..")
|
||||||
assert.ErrorContains(t, err, `relative path escapes root: ../..`)
|
assert.ErrorContains(t, err, `relative path escapes root: ../..`)
|
||||||
|
|
||||||
_, err = rp.Join(".//a/..//./b/..")
|
|
||||||
assert.ErrorContains(t, err, `relative path resolves to root: .//a/..//./b/..`)
|
|
||||||
|
|
||||||
_, err = rp.Join("a/b/../..")
|
|
||||||
assert.ErrorContains(t, err, "relative path resolves to root: a/b/../..")
|
|
||||||
|
|
||||||
_, err = rp.Join("")
|
|
||||||
assert.ErrorContains(t, err, "relative path resolves to root: ")
|
|
||||||
|
|
||||||
_, err = rp.Join(".")
|
|
||||||
assert.ErrorContains(t, err, "relative path resolves to root: .")
|
|
||||||
|
|
||||||
_, err = rp.Join("/")
|
|
||||||
assert.ErrorContains(t, err, "relative path resolves to root: /")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRootPathClean(t *testing.T) {
|
func TestRootPathClean(t *testing.T) {
|
||||||
|
|
|
@ -9,7 +9,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/databricks/databricks-sdk-go"
|
"github.com/databricks/databricks-sdk-go"
|
||||||
"github.com/databricks/databricks-sdk-go/apierr"
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
|
@ -128,3 +130,52 @@ func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string) error {
|
||||||
Recursive: false,
|
Recursive: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *WorkspaceFilesClient) ReadDir(ctx context.Context, name string) ([]FileInfo, error) {
|
||||||
|
absPath, err := w.root.Join(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
objects, err := w.workspaceClient.Workspace.ListAll(ctx, workspace.ListWorkspaceRequest{
|
||||||
|
Path: absPath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// If we got an API error we deal with it below.
|
||||||
|
var aerr *apierr.APIError
|
||||||
|
if !errors.As(err, &aerr) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This API returns a 404 if the specified path does not exist.
|
||||||
|
if aerr.StatusCode == http.StatusNotFound {
|
||||||
|
return nil, NoSuchDirectoryError{path.Dir(absPath)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := make([]FileInfo, len(objects))
|
||||||
|
for i, v := range objects {
|
||||||
|
info[i] = FileInfo{
|
||||||
|
Type: string(v.ObjectType),
|
||||||
|
Name: path.Base(v.Path),
|
||||||
|
Size: v.Size,
|
||||||
|
ModTime: time.UnixMilli(v.ModifiedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by name for parity with os.ReadDir.
|
||||||
|
sort.Slice(info, func(i, j int) bool { return info[i].Name < info[j].Name })
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorkspaceFilesClient) Mkdir(ctx context.Context, name string) error {
|
||||||
|
dirPath, err := w.root.Join(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return w.workspaceClient.Workspace.Mkdirs(ctx, workspace.Mkdirs{
|
||||||
|
Path: dirPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue