-
Notifications
You must be signed in to change notification settings - Fork 154
Add databricks fs ls command for WSFS #408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e1780a4
53f0228
8a9175e
7c46b35
c49df32
d876eaf
c3bc6fd
95d8590
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,76 @@ | ||
| package fs | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "path" | ||
|
|
||
| "github.com/databricks/cli/cmd/root" | ||
| "github.com/databricks/cli/libs/cmdio" | ||
| "github.com/databricks/cli/libs/filer" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func expandPath(i filer.FileInfo, root string) filer.FileInfo { | ||
| i.Name = path.Join(root, i.Name) | ||
| return i | ||
| } | ||
|
|
||
| // lsCmd represents the ls command | ||
| var lsCmd = &cobra.Command{ | ||
| Use: "ls <dir-name>", | ||
| Short: "Lists files", | ||
| Long: `Lists files`, | ||
| Hidden: true, | ||
| Use: "ls <dir-name>", | ||
| Short: "Lists files", | ||
| Long: `Lists files in a DBFS or WSFS directory`, | ||
| Args: cobra.MaximumNArgs(1), | ||
| Annotations: map[string]string{}, | ||
| PreRunE: root.MustWorkspaceClient, | ||
|
|
||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return fmt.Errorf("TODO") | ||
| // Assign template according to whether -l is specified | ||
| template := cmdio.Heredoc(` | ||
| {{range .}}{{.Name}} | ||
| {{end}} | ||
| `) | ||
| if longMode { | ||
| template = cmdio.Heredoc(` | ||
| {{range .}}{{.Type|printf "%-10s"}} {{.Size}} {{.ModTime|unix_date}} {{.Name}} | ||
| {{end}} | ||
| `) | ||
| } | ||
|
|
||
| // Path to list files from. Defaults to`/` | ||
| rootPath := "/" | ||
| if len(args) > 0 { | ||
| rootPath = args[0] | ||
| } | ||
|
|
||
| // Initialize workspace client | ||
| ctx := cmd.Context() | ||
| w := root.WorkspaceClient(ctx) | ||
| f, err := filer.NewWorkspaceFilesClient(w, rootPath) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Get file info | ||
| filesInfo, err := f.ReadDir(ctx, "") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should pass
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well no right? since the filer is rooted at the arg[0] already
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ack, you're right. Could you modify this to |
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // compute output with expanded paths if necessary | ||
| if absolute { | ||
| for i := range filesInfo { | ||
| filesInfo[i] = expandPath(filesInfo[i], rootPath) | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could alias the |
||
| return cmdio.RenderWithTemplate(ctx, filesInfo, template) | ||
| }, | ||
| } | ||
|
|
||
| var longMode bool | ||
| var absolute bool | ||
|
|
||
| func init() { | ||
| lsCmd.Flags().BoolVarP(&longMode, "long", "l", false, "Displays full information including size, file type and modification time since Epoch in milliseconds.") | ||
| lsCmd.Flags().BoolVar(&absolute, "absolute", false, "Displays absolute paths.") | ||
| fsCmd.AddCommand(lsCmd) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| package fs | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "path" | ||
| "testing" | ||
|
|
||
| _ "github.com/databricks/cli/cmd/fs" | ||
| "github.com/databricks/cli/internal" | ||
| "github.com/databricks/cli/internal/helpers" | ||
| "github.com/databricks/cli/libs/cmdio" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func assertObjectListed(t *testing.T, parsedLogs []map[string]string, name string, objectType string) { | ||
| foundFile := false | ||
| for _, v := range parsedLogs { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How are these parsed logs? Looking at the code I see |
||
| if v["Name"] != name { | ||
| continue | ||
| } | ||
| foundFile = true | ||
| assert.Equal(t, objectType, v["Type"]) | ||
| } | ||
| assert.True(t, foundFile, fmt.Sprintf("failed to find file %s in output logs", name)) | ||
| } | ||
|
|
||
| func TestAccFsLs(t *testing.T) { | ||
| t.Log(internal.GetEnvOrSkipTest(t, "CLOUD_ENV")) | ||
|
|
||
| // setup some testdata in the workspace | ||
| w := helpers.NewWorkspaceTestdata(t) | ||
| w.AddFile("foo.txt", `hello, world`) | ||
| w.AddFile("python_notebook.py", cmdio.Heredoc(` | ||
| #Databricks notebook source | ||
| print(2)`)) | ||
| w.AddFile("python_file.py", `print(1)`) | ||
| w.Mkdir("my_directory") | ||
| w.AddFile("my_directory/.gitkeep", "") | ||
|
|
||
| // run list command | ||
| stdout, stderr := internal.RequireSuccessfulRun(t, "fs", "ls", w.RootPath(), "--output=json") | ||
|
|
||
| // read and parse the output logs | ||
| parsedLogs := make([]map[string]string, 0) | ||
| err := json.Unmarshal(stdout.Bytes(), &parsedLogs) | ||
| require.NoError(t, err) | ||
|
|
||
| // make assertions on the output logs | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please refer to just "output". The "logs" suffix confuses it with our logging infrastructure.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1, we need a couple of type-safe CLI run test helpers |
||
| assert.Equal(t, stderr.String(), "") | ||
| assertObjectListed(t, parsedLogs, "python_file.py", "FILE") | ||
| assertObjectListed(t, parsedLogs, "foo.txt", "FILE") | ||
| assertObjectListed(t, parsedLogs, "python_notebook", "NOTEBOOK") | ||
| assertObjectListed(t, parsedLogs, "my_directory", "DIRECTORY") | ||
| } | ||
|
|
||
| func TestAccFsLsWithAbsoluteFlag(t *testing.T) { | ||
| t.Log(internal.GetEnvOrSkipTest(t, "CLOUD_ENV")) | ||
|
|
||
| // setup some testdata in the workspace | ||
| w := helpers.NewWorkspaceTestdata(t) | ||
| w.AddFile("foo.txt", `hello, world`) | ||
| w.AddFile("python_notebook.py", cmdio.Heredoc(` | ||
| #Databricks notebook source | ||
| print(2)`)) | ||
| w.AddFile("python_file.py", `print(1)`) | ||
| w.Mkdir("my_directory") | ||
| w.AddFile("my_directory/.gitkeep", "") | ||
|
|
||
| // run list command | ||
| stdout, stderr := internal.RequireSuccessfulRun(t, "fs", "ls", w.RootPath(), "--output=json", "--absolute") | ||
|
|
||
| // read and parse the output logs | ||
| parsedLogs := make([]map[string]string, 0) | ||
| err := json.Unmarshal(stdout.Bytes(), &parsedLogs) | ||
| require.NoError(t, err) | ||
|
|
||
| // make assertions on the output logs | ||
| assert.Equal(t, stderr.String(), "") | ||
| assertObjectListed(t, parsedLogs, path.Join(w.RootPath(), "python_file.py"), "FILE") | ||
| assertObjectListed(t, parsedLogs, path.Join(w.RootPath(), "foo.txt"), "FILE") | ||
| assertObjectListed(t, parsedLogs, path.Join(w.RootPath(), "python_notebook"), "NOTEBOOK") | ||
| assertObjectListed(t, parsedLogs, path.Join(w.RootPath(), "my_directory"), "DIRECTORY") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package helpers | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/http" | ||
| "net/url" | ||
| "path" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/databricks/cli/internal" | ||
| "github.com/databricks/databricks-sdk-go" | ||
| "github.com/databricks/databricks-sdk-go/apierr" | ||
| "github.com/databricks/databricks-sdk-go/client" | ||
| "github.com/databricks/databricks-sdk-go/service/workspace" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| type workspaceTestdata struct { | ||
| root string | ||
| t *testing.T | ||
| client *databricks.WorkspaceClient | ||
| } | ||
|
|
||
| func NewWorkspaceTestdata(t *testing.T) *workspaceTestdata { | ||
| ctx := context.Background() | ||
| w := databricks.Must(databricks.NewWorkspaceClient()) | ||
|
|
||
| me, err := w.CurrentUser.Me(ctx) | ||
| require.NoError(t, err) | ||
| path := fmt.Sprintf("/Users/%s/%s", me.UserName, internal.RandomName("wsfs-files-")) | ||
|
|
||
| // Ensure directory exists, but doesn't exist YET! | ||
| // Otherwise we could inadvertently remove a directory that already exists on cleanup. | ||
| t.Logf("mkdir %s", path) | ||
| err = w.Workspace.MkdirsByPath(ctx, path) | ||
| require.NoError(t, err) | ||
|
|
||
| // Remove test directory on test completion. | ||
| t.Cleanup(func() { | ||
| t.Logf("rm -rf %s", path) | ||
| err := w.Workspace.Delete(ctx, workspace.Delete{ | ||
| Path: path, | ||
| Recursive: true, | ||
| }) | ||
| if err == nil || apierr.IsMissing(err) { | ||
| return | ||
| } | ||
| t.Logf("unable to remove temporary workspace path %s: %#v", path, err) | ||
| }) | ||
|
|
||
| return &workspaceTestdata{ | ||
| root: path, | ||
| t: t, | ||
| client: w, | ||
| } | ||
| } | ||
|
|
||
| func (w *workspaceTestdata) RootPath() string { | ||
| return w.root | ||
| } | ||
|
|
||
| func (w *workspaceTestdata) AddFile(name string, content string) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you also add a test for DBFS? |
||
| path := path.Join(w.root, name) | ||
| ctx := context.Background() | ||
|
|
||
| // url path for uploading file API | ||
| urlPath := fmt.Sprintf( | ||
| "/api/2.0/workspace-files/import-file/%s?overwrite=true", | ||
| url.PathEscape(strings.TrimLeft(path, "/")), | ||
| ) | ||
|
|
||
| // initialize API client | ||
| apiClient, err := client.New(w.client.Config) | ||
| require.NoError(w.t, err) | ||
|
|
||
| // Make API request | ||
| err = apiClient.Do(ctx, http.MethodPost, urlPath, content, nil) | ||
| require.NoError(w.t, err) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is equivalent to what |
||
| } | ||
|
|
||
| func (w *workspaceTestdata) Mkdir(name string) { | ||
| ctx := context.Background() | ||
| path := path.Join(w.root, name) | ||
| err := w.client.Workspace.MkdirsByPath(ctx, path) | ||
| require.NoError(w.t, err) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,6 +71,18 @@ func (c *cmdIO) Render(v any) error { | |
| } | ||
| } | ||
|
|
||
| func RenderWithTemplate(ctx context.Context, v any, template string) error { | ||
| c := fromContext(ctx) | ||
| switch c.outputFormat { | ||
| case flags.OutputJSON: | ||
| return renderJson(c.out, v) | ||
| case flags.OutputText: | ||
| return renderTemplate(c.out, template, v) | ||
| default: | ||
| return fmt.Errorf("invalid output format: %s", c.outputFormat) | ||
| } | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can set the "template" annotation on the command to achieve the same result. Then this function doesn't need to be added.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tried that first, does not work because the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @shreyas-goenka please add this as a comment in the command and to this function. Also, please make Render to call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in followup |
||
| func Render(ctx context.Context, v any) error { | ||
| c := fromContext(ctx) | ||
| return c.Render(v) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |
| "strings" | ||
| "text/tabwriter" | ||
| "text/template" | ||
| "time" | ||
|
|
||
| "github.com/fatih/color" | ||
| "github.com/nwidger/jsoncolor" | ||
|
|
@@ -86,6 +87,9 @@ func renderTemplate(w io.Writer, tmpl string, v any) error { | |
| } | ||
| return string(b), nil | ||
| }, | ||
| "unix_date": func(t time.Time) string { | ||
| return t.UTC().Format(time.UnixDate) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i've looked at the output - it's very difficult to parse when piped through
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in the followup PR
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in the followup PR |
||
| }, | ||
| }).Parse(tmpl) | ||
| if err != nil { | ||
| return err | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we support just the long mode? then the code is more maintainable and template ends up in the annotation