diff --git a/cmd/fs/fs.go b/cmd/fs/fs.go index 74d725d4e..8c6d03410 100644 --- a/cmd/fs/fs.go +++ b/cmd/fs/fs.go @@ -10,7 +10,6 @@ var fsCmd = &cobra.Command{ Use: "fs", Short: "Filesystem related commands", Long: `Commands to do DBFS operations.`, - Hidden: true, } func init() { diff --git a/cmd/fs/ls.go b/cmd/fs/ls.go index ac1923857..d8626f299 100644 --- a/cmd/fs/ls.go +++ b/cmd/fs/ls.go @@ -2,22 +2,76 @@ package fs import ( "fmt" + "net/url" + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/filer" "github.com/spf13/cobra" ) // lsCmd represents the ls command var lsCmd = &cobra.Command{ - Use: "ls ", - Short: "Lists files", - Long: `Lists files`, - Hidden: true, + Use: "ls ", + Short: "Lists files", + Long: `Lists files`, + Args: cobra.ExactArgs(1), + PreRunE: root.MustWorkspaceClient, + Annotations: map[string]string{ + "template_long": cmdio.Heredoc(` + {{range .}}{{if .IsDir}}DIRECTORY {{else}}FILE {{end}}{{.Size}} {{.ModTime|pretty_date}} {{.Name}} + {{end}} + `), + "template": cmdio.Heredoc(` + {{range .}}{{.Name}} + {{end}} + `), + }, RunE: func(cmd *cobra.Command, args []string) error { - return fmt.Errorf("TODO") + ctx := cmd.Context() + w := root.WorkspaceClient(ctx) + + fileUri, err := url.Parse(args[0]) + if err != nil { + return err + } + + // Only dbfs file scheme is supported + if fileUri.Scheme != filer.DbfsScheme { + return fmt.Errorf("expected dbfs path (with the dbfs:/ prefix): %s", args[0]) + } + + f, err := filer.NewDbfsClient(w, fileUri.Path) + if err != nil { + return err + } + + entries, err := f.ReadDir(ctx, "") + if err != nil { + return err + } + + lsOutputs := make([]lsOutput, 0) + for _, entry := range entries { + parsedEntry, err := toLsOutput(entry) + if err != nil { + return err + } + lsOutputs = append(lsOutputs, *parsedEntry) + } + + // Use template for long mode if the flag is set + if longMode { + return cmdio.RenderWithTemplate(ctx, lsOutputs, "template_long") + } + return cmdio.Render(ctx, lsOutputs) }, } +var longMode bool + func init() { + lsCmd.Flags().BoolVarP(&longMode, "long", "l", false, "Displays full information including size, file type and modification time since Epoch in milliseconds.") fsCmd.AddCommand(lsCmd) } diff --git a/cmd/fs/ls_output.go b/cmd/fs/ls_output.go new file mode 100644 index 000000000..e8728b046 --- /dev/null +++ b/cmd/fs/ls_output.go @@ -0,0 +1,27 @@ +package fs + +import ( + "io/fs" + "time" +) + +type lsOutput struct { + Name string `json:"name"` + IsDir bool `json:"is_directory"` + Size int64 `json:"size"` + ModTime time.Time `json:"last_modified"` +} + +func toLsOutput(f fs.DirEntry) (*lsOutput, error) { + info, err := f.Info() + if err != nil { + return nil, err + } + + return &lsOutput{ + Name: f.Name(), + IsDir: f.IsDir(), + Size: info.Size(), + ModTime: info.ModTime(), + }, nil +} diff --git a/cmd/root/io.go b/cmd/root/io.go index 93830c804..07db9e0a0 100644 --- a/cmd/root/io.go +++ b/cmd/root/io.go @@ -2,6 +2,7 @@ package root import ( "os" + "strings" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/flags" @@ -27,13 +28,14 @@ func OutputType() flags.Output { } func initializeIO(cmd *cobra.Command) error { - var template string - if cmd.Annotations != nil { - // rely on zeroval being an empty string - template = cmd.Annotations["template"] + templates := make(map[string]string, 0) + for k, v := range cmd.Annotations { + if strings.Contains(k, "template") { + templates[k] = v + } } - cmdIO := cmdio.NewIO(outputType, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), template) + cmdIO := cmdio.NewIO(outputType, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), templates) ctx := cmdio.InContext(cmd.Context(), cmdIO) cmd.SetContext(ctx) diff --git a/internal/ls_test.go b/internal/ls_test.go new file mode 100644 index 000000000..8807a5d62 --- /dev/null +++ b/internal/ls_test.go @@ -0,0 +1,2 @@ +package internal + diff --git a/libs/cmdio/io.go b/libs/cmdio/io.go index beaa85717..4f134c510 100644 --- a/libs/cmdio/io.go +++ b/libs/cmdio/io.go @@ -24,17 +24,17 @@ type cmdIO struct { // e.g. if stdout is a terminal interactive bool outputFormat flags.Output - template string + templates map[string]string in io.Reader out io.Writer err io.Writer } -func NewIO(outputFormat flags.Output, in io.Reader, out io.Writer, err io.Writer, template string) *cmdIO { +func NewIO(outputFormat flags.Output, in io.Reader, out io.Writer, err io.Writer, templates map[string]string) *cmdIO { return &cmdIO{ interactive: !color.NoColor, outputFormat: outputFormat, - template: template, + templates: templates, in: in, out: out, err: err, @@ -66,14 +66,14 @@ func (c *cmdIO) IsTTY() bool { return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) } -func (c *cmdIO) Render(v any) error { +func (c *cmdIO) Render(v any, templateName string) error { // TODO: add terminal width & white/dark theme detection switch c.outputFormat { case flags.OutputJSON: return renderJson(c.out, v) case flags.OutputText: - if c.template != "" { - return renderTemplate(c.out, c.template, v) + if c.templates[templateName] != "" { + return renderTemplate(c.out, c.templates[templateName], v) } return renderJson(c.out, v) default: @@ -83,7 +83,12 @@ func (c *cmdIO) Render(v any) error { func Render(ctx context.Context, v any) error { c := fromContext(ctx) - return c.Render(v) + return c.Render(v, "template") +} + +func RenderWithTemplate(ctx context.Context, v any, templateName string) error { + c := fromContext(ctx) + return c.Render(v, templateName) } type tuple struct{ Name, Id string } diff --git a/libs/cmdio/render.go b/libs/cmdio/render.go index 8aff2b8d2..063d7cbcb 100644 --- a/libs/cmdio/render.go +++ b/libs/cmdio/render.go @@ -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 }, + "pretty_date": func(t time.Time) string { + return t.UTC().Format("2006-01-02T15:04:05Z") + }, }).Parse(tmpl) if err != nil { return err diff --git a/libs/filer/dbfs_client.go b/libs/filer/dbfs_client.go index 67878136b..293ea3996 100644 --- a/libs/filer/dbfs_client.go +++ b/libs/filer/dbfs_client.go @@ -16,6 +16,8 @@ import ( "golang.org/x/exp/slices" ) +const DbfsScheme = "dbfs" + // Type that implements fs.DirEntry for DBFS. type dbfsDirEntry struct { dbfsFileInfo