diff --git a/cmd/help_manifest.go b/cmd/help_manifest.go index 3b179a2..b45dfeb 100644 --- a/cmd/help_manifest.go +++ b/cmd/help_manifest.go @@ -153,8 +153,13 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{ Known: true, }, "ls": { - Args: []jsonCommandArg{commandArg("path", false, false, "dropbox_path", "Dropbox folder or file path")}, - Examples: []jsonCommandExample{{Description: "List the root folder", Command: "dbxcli ls /"}}, + Args: []jsonCommandArg{ + commandArg("path", false, false, "dropbox_path", "Dropbox folder, file path, or revision"), + }, + Examples: []jsonCommandExample{ + {Description: "List the root folder", Command: "dbxcli ls /"}, + {Description: "Show a file revision", Command: "dbxcli ls rev:a1c10ce0dd78"}, + }, Flags: mergeCommandFlagMetadata(commonListFlagMetadata, map[string]jsonCommandFlagMetadata{ "include-deleted": {ValueKind: "boolean"}, "only-deleted": {ValueKind: "boolean"}, diff --git a/cmd/logout.go b/cmd/logout.go index 6ed4bd6..9cc9f4e 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -44,7 +44,7 @@ var revokeAccessToken = func(domain string, token string) error { AsMemberID: "", Domain: domain, Client: nil, - HeaderGenerator: nil, + HeaderGenerator: dbxcliHeaderGenerator, URLGenerator: nil, } client := auth.NewContext(cfg) diff --git a/cmd/ls.go b/cmd/ls.go index 26b8854..44020d7 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -45,7 +45,7 @@ type lsInput struct { const lsJSONStatusListed = "listed" -// Sends a get_metadata request for a given path and returns the response +// Sends a get_metadata request for a given path or Dropbox reference and returns the response. func getFileMetadata(c filesClient, path string) (files.IsMetadata, error) { arg := files.NewGetMetadataArg(path) @@ -108,9 +108,7 @@ func ls(cmd *cobra.Command, args []string) (err error) { path := "" if len(args) > 0 { - if path, err = validatePath(args[0]); err != nil { - return err - } + path = newDropboxReference(args[0]).String() } arg := files.NewListFolderArg(path) @@ -397,11 +395,12 @@ func finishListOutput(w *tabwriter.Writer, itemCounter int, opts listOptions) er // lsCmd represents the ls command var lsCmd = &cobra.Command{ - Use: "ls [flags] []", + Use: "ls [flags] []", Short: "List files and folders", Example: ` dbxcli ls / # Or just 'ls' dbxcli ls /some-folder # Or 'ls some-folder' dbxcli ls /some-folder/some-file.pdf + dbxcli ls rev:a1c10ce0dd78 dbxcli ls -l`, RunE: ls, } diff --git a/cmd/ls_test.go b/cmd/ls_test.go index 8026d0f..5170a41 100644 --- a/cmd/ls_test.go +++ b/cmd/ls_test.go @@ -556,6 +556,45 @@ func TestLsJSONFilePathUsesMetadata(t *testing.T) { } } +func TestLsJSONRevisionReferenceUsesMetadata(t *testing.T) { + cmd, stdout := testLsCmd(t) + setLsOutputJSON(t, cmd) + const revision = "rev:opaque-revision" + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + if arg.Path != revision { + t.Fatalf("metadata path = %q, want %q", arg.Path, revision) + } + return &files.FileMetadata{ + Metadata: files.Metadata{ + PathDisplay: "/file.txt", + PathLower: "/file.txt", + }, + Id: "id:file", + Rev: "rev-file", + Size: 7, + }, nil + }, + listFolderFn: func(arg *files.ListFolderArg) (*files.ListFolderResult, error) { + t.Fatalf("ListFolder called for revision reference: %v", arg) + return nil, nil + }, + } + stubFilesClient(t, mock) + + if err := ls(cmd, []string{revision}); err != nil { + t.Fatalf("ls error: %v", err) + } + got := decodeLsOutput(t, stdout) + if got.Input.Path != revision { + t.Fatalf("input path = %q, want %q", got.Input.Path, revision) + } + if len(got.Results) != 1 || got.Results[0].Status != lsJSONStatusListed || got.Results[0].Kind != "file" || got.Results[0].Result.Type != "file" || got.Results[0].Result.Rev != "rev-file" { + t.Fatalf("results = %#v, want one listed file revision", got.Results) + } +} + func TestLsJSONDeletedEntryIsStructured(t *testing.T) { cmd, stdout := testLsCmd(t) setLsOutputJSON(t, cmd) diff --git a/cmd/root.go b/cmd/root.go index 64f83bd..2e7f9c8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -54,6 +54,13 @@ var ( ) var config dropbox.Config + +const dbxcliUserAgent = "dbxcli" + +func dbxcliHeaderGenerator(_ string, _ string, _ string) map[string]string { + return map[string]string{"User-Agent": dbxcliUserAgent} +} + var commandContext context.Context = context.Background() var commandContextCancel context.CancelFunc @@ -198,7 +205,7 @@ func makeDropboxConfig(token string, verbose bool, asMember string, domain strin AsMemberID: asMember, Domain: domain, Client: nil, - HeaderGenerator: nil, + HeaderGenerator: dbxcliHeaderGenerator, URLGenerator: nil, } } diff --git a/cmd/root_test.go b/cmd/root_test.go index cc407c7..895af92 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -663,6 +663,16 @@ func TestWithRootNamespaceSkipsTeamManage(t *testing.T) { } } +func TestMakeDropboxConfigSetsUserAgent(t *testing.T) { + cfg := makeDropboxConfig("token", false, "", "") + if cfg.HeaderGenerator == nil { + t.Fatal("HeaderGenerator = nil, want dbxcli User-Agent generator") + } + if got := cfg.HeaderGenerator("api", "files", "get_metadata")["User-Agent"]; got != dbxcliUserAgent { + t.Fatalf("User-Agent = %q, want %q", got, dbxcliUserAgent) + } +} + func assertCurrentAuthContext(t *testing.T, source string, refreshable bool, authFile string) { t.Helper() diff --git a/cmd/testdata/json_contract/success_outputs.json b/cmd/testdata/json_contract/success_outputs.json index 1e41599..17432f8 100644 --- a/cmd/testdata/json_contract/success_outputs.json +++ b/cmd/testdata/json_contract/success_outputs.json @@ -18,7 +18,7 @@ "input": {}, "result": { "path": "ls", - "use": "dbxcli ls [flags] []", + "use": "dbxcli ls [flags] []", "short": "List files and folders", "aliases": [], "runnable": true, @@ -298,7 +298,7 @@ }, "path": { "type": "string", - "description": "Dropbox folder or file path", + "description": "Dropbox folder, file path, or revision", "x-cli-kind": "arg", "x-cli-name": "path", "x-value-kind": "dropbox_path" @@ -400,7 +400,7 @@ "variadic": false, "placement": "positional", "value_kind": "dropbox_path", - "description": "Dropbox folder or file path", + "description": "Dropbox folder, file path, or revision", "stream_dash": false, "enum_values": [] } @@ -409,6 +409,10 @@ { "description": "List the root folder", "command": "dbxcli ls /" + }, + { + "description": "Show a file revision", + "command": "dbxcli ls rev:a1c10ce0dd78" } ], "schema_refs": { diff --git a/docs/commands/dbxcli_ls.md b/docs/commands/dbxcli_ls.md index dc7dda4..a650a56 100644 --- a/docs/commands/dbxcli_ls.md +++ b/docs/commands/dbxcli_ls.md @@ -5,7 +5,7 @@ List files and folders ``` -dbxcli ls [flags] [] +dbxcli ls [flags] [] ``` ### Examples @@ -14,6 +14,7 @@ dbxcli ls [flags] [] dbxcli ls / # Or just 'ls' dbxcli ls /some-folder # Or 'ls some-folder' dbxcli ls /some-folder/some-file.pdf + dbxcli ls rev:a1c10ce0dd78 dbxcli ls -l ```