diff --git a/cmd/dropbox_reference.go b/cmd/dropbox_reference.go new file mode 100644 index 0000000..9b4a51a --- /dev/null +++ b/cmd/dropbox_reference.go @@ -0,0 +1,58 @@ +// Copyright © 2026 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import "strings" + +type dropboxReferenceKind uint8 + +const ( + dropboxPathReference dropboxReferenceKind = iota + dropboxIDReference + dropboxRevisionReference + dropboxNamespaceReference +) + +// dropboxReference preserves Dropbox API references as opaque strings. Only +// ordinary paths are normalized; Dropbox remains responsible for validating +// identifier, revision, and namespace reference values. +type dropboxReference struct { + value string + kind dropboxReferenceKind +} + +func newDropboxReference(value string) dropboxReference { + switch { + case strings.HasPrefix(value, "id:"): + return dropboxReference{value: value, kind: dropboxIDReference} + case strings.HasPrefix(value, "rev:"): + return dropboxReference{value: value, kind: dropboxRevisionReference} + case strings.HasPrefix(value, "ns:"): + return dropboxReference{value: value, kind: dropboxNamespaceReference} + default: + if !strings.HasPrefix(value, "/") { + value = "/" + value + } + return dropboxReference{value: cleanDropboxPath(value), kind: dropboxPathReference} + } +} + +func (r dropboxReference) String() string { + return r.value +} + +func (r dropboxReference) isPath() bool { + return r.kind == dropboxPathReference +} diff --git a/cmd/dropbox_reference_test.go b/cmd/dropbox_reference_test.go new file mode 100644 index 0000000..499ec09 --- /dev/null +++ b/cmd/dropbox_reference_test.go @@ -0,0 +1,44 @@ +// Copyright © 2026 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import "testing" + +func TestNewDropboxReference(t *testing.T) { + tests := []struct { + name string + input string + want string + kind dropboxReferenceKind + }{ + {name: "absolute path", input: "/folder/file.txt", want: "/folder/file.txt", kind: dropboxPathReference}, + {name: "relative path", input: "folder/file.txt", want: "/folder/file.txt", kind: dropboxPathReference}, + {name: "file id", input: "id:opaque-value", want: "id:opaque-value", kind: dropboxIDReference}, + {name: "revision", input: "rev:opaque-value", want: "rev:opaque-value", kind: dropboxRevisionReference}, + {name: "namespace path", input: "ns:123/folder/file.txt", want: "ns:123/folder/file.txt", kind: dropboxNamespaceReference}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := newDropboxReference(tt.input) + if got.String() != tt.want { + t.Fatalf("String() = %q, want %q", got.String(), tt.want) + } + if got.kind != tt.kind { + t.Fatalf("kind = %d, want %d", got.kind, tt.kind) + } + }) + } +} diff --git a/cmd/format.go b/cmd/format.go index e502956..cef9857 100644 --- a/cmd/format.go +++ b/cmd/format.go @@ -80,15 +80,7 @@ func compareLess(a, b files.IsMetadata, opts listOptions) bool { } func entryPath(e files.IsMetadata) string { - switch f := e.(type) { - case *files.FileMetadata: - return f.PathDisplay - case *files.FolderMetadata: - return f.PathDisplay - case *files.DeletedMetadata: - return f.PathDisplay - } - return "" + return metadataPathDisplay(e) } func entrySize(e files.IsMetadata) uint64 { diff --git a/cmd/get.go b/cmd/get.go index 68121a2..3ae5cf3 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -68,12 +68,13 @@ func get(cmd *cobra.Command, args []string) (err error) { return invalidArgumentsErrorWithDetails("`get` requires `src` and/or `dst` arguments", argumentsErrorDetails("src", "dst")) } - src, err := validatePath(args[0]) - if err != nil { - return - } + srcRef := newDropboxReference(args[0]) + src := srcRef.String() - dst := path.Base(src) + dst := "" + if srcRef.isPath() { + dst = path.Base(src) + } if len(args) == 2 { dst = args[1] } @@ -92,11 +93,15 @@ func get(cmd *cobra.Command, args []string) (err error) { meta, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(src)) if err != nil { - if recursive { - return withJSONErrorDetails(fmt.Errorf("get metadata for %s: %v", src, err), operationErrorDetails("download"), pathErrorDetails(src)) + metadataErr := withJSONErrorDetails(fmt.Errorf("get metadata for %s: %v", src, err), operationErrorDetails("download"), pathErrorDetails(src)) + if recursive || dst == "" { + return metadataErr } // For non-recursive, fall through to download (will fail with proper error) if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() { + if !srcRef.isPath() { + return metadataErr + } dst = filepath.Join(dst, path.Base(src)) } result, err := downloadFileWithResult(dbx, src, dst, opts) @@ -111,15 +116,26 @@ func get(cmd *cobra.Command, args []string) (err error) { }, []getResult{result}) } + sourceName := path.Base(src) + if !srcRef.isPath() { + sourceName = metadataName(meta) + if sourceName == "" { + return withJSONErrorDetails(fmt.Errorf("get metadata for %s did not include a name", src), operationErrorDetails("download"), pathErrorDetails(src)) + } + if dst == "" { + dst = sourceName + } + } + if _, ok := meta.(*files.FolderMetadata); ok { if !recursive { return invalidArgumentsErrorfWithDetails("%s is a folder (use --recursive to download folders)", mergeJSONErrorDetails(operationErrorDetails("download"), pathErrorDetails(src)), src) } if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() { - dst = filepath.Join(dst, path.Base(src)) + dst = filepath.Join(dst, sourceName) } if commandOutputFormat(cmd) == output.FormatText { - return withJSONErrorDetails(getRecursive(dbx, src, dst), operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst)) + return withJSONErrorDetails(getRecursiveWithRootMetadata(dbx, src, dst, meta), operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst)) } results, err := getRecursiveWithResults(dbx, src, dst, meta, opts) if err != nil { @@ -134,7 +150,7 @@ func get(cmd *cobra.Command, args []string) (err error) { } if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() { - dst = filepath.Join(dst, path.Base(src)) + dst = filepath.Join(dst, sourceName) } result, err := downloadFileWithResult(dbx, src, dst, opts) @@ -214,32 +230,16 @@ func getStdout(cmd *cobra.Command, src string, recursive bool) error { return withJSONErrorDetails(downloadToStdout(dbx, src, cmd.OutOrStdout()), operationErrorDetails("download"), pathErrorDetails(src)) } -func getWithClient(dbx filesClient, args []string) (err error) { - if len(args) == 0 || len(args) > 2 { - return invalidArgumentsErrorWithDetails("`get` requires `src` and/or `dst` arguments", argumentsErrorDetails("src", "dst")) - } - - src, err := validatePath(args[0]) - if err != nil { - return - } - - dst := path.Base(src) - if len(args) == 2 { - dst = args[1] - } - if f, err := os.Stat(dst); err == nil && f.IsDir() { - dst = filepath.Join(dst, path.Base(src)) - } - - return withJSONErrorDetails(downloadFile(dbx, src, dst), operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst)) -} - func getRecursive(dbx filesClient, src, dst string) error { _, err := getRecursiveInternal(dbx, src, dst, nil, getOptions{}, false) return err } +func getRecursiveWithRootMetadata(dbx filesClient, src, dst string, rootMeta files.IsMetadata) error { + _, err := getRecursiveInternal(dbx, src, dst, rootMeta, getOptions{}, false) + return err +} + func getRecursiveWithResults(dbx filesClient, src, dst string, rootMeta files.IsMetadata, opts getOptions) ([]getResult, error) { return getRecursiveInternal(dbx, src, dst, rootMeta, opts, true) } @@ -265,6 +265,10 @@ func getRecursiveInternal(dbx filesClient, src, dst string, rootMeta files.IsMet } var results []getResult + rootPath := src + if metadataPath := metadataPathDisplay(rootMeta); metadataPath != "" { + rootPath = metadataPath + } if collectResults { result, err := ensureLocalDirectoryResult(src, dst, rootMeta) @@ -283,7 +287,7 @@ func getRecursiveInternal(dbx filesClient, src, dst string, rootMeta files.IsMet for _, entry := range entries { switch f := entry.(type) { case *files.FolderMetadata: - relPath, err := relativeTo(src, f.PathDisplay) + relPath, err := relativeTo(rootPath, f.PathDisplay) if err != nil { downloadErrors = append(downloadErrors, err) continue @@ -305,7 +309,7 @@ func getRecursiveInternal(dbx filesClient, src, dst string, rootMeta files.IsMet } } case *files.FileMetadata: - relPath, err := relativeTo(src, f.PathDisplay) + relPath, err := relativeTo(rootPath, f.PathDisplay) if err != nil { downloadErrors = append(downloadErrors, err) continue @@ -493,11 +497,14 @@ var getCmd = &cobra.Command{ Use: "get [flags] []", Short: "Download a file or folder", Long: `Download a file or folder from Dropbox. + - Source may be a Dropbox path, file ID (id:), revision (rev:), or + namespace-relative path (ns:). - Use --recursive (-r) to download entire directories. - Use - as target to write file bytes to stdout. Stdout is byte-clean: all progress and errors go to stderr. `, Example: ` dbxcli get /remote/file.txt ./local-file.txt + dbxcli get rev:a1c10ce0dd78 ./historical-file.txt dbxcli get -r /remote/folder ./local-folder dbxcli get /backups/src.tgz - | tar tz dbxcli get /file.txt - > local-copy.txt`, diff --git a/cmd/get_test.go b/cmd/get_test.go index 422726c..78d853d 100644 --- a/cmd/get_test.go +++ b/cmd/get_test.go @@ -3,6 +3,7 @@ package cmd import ( "bytes" "encoding/json" + "errors" "io" "os" "path/filepath" @@ -107,12 +108,16 @@ func TestGetArgValidation(t *testing.T) { func TestGetDstDefaultsToBasename(t *testing.T) { tmpDir := t.TempDir() - origDir, _ := os.Getwd() - _ = os.Chdir(tmpDir) - defer func() { _ = os.Chdir(origDir) }() + t.Chdir(tmpDir) content := "downloaded content" mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return &files.FileMetadata{ + Metadata: files.Metadata{PathDisplay: arg.Path}, + Size: uint64(len(content)), + }, nil + }, downloadFn: func(arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { if arg.Path != "/some/path/file.txt" { t.Errorf("download path = %q, want %q", arg.Path, "/some/path/file.txt") @@ -124,8 +129,9 @@ func TestGetDstDefaultsToBasename(t *testing.T) { return meta, io.NopCloser(strings.NewReader(content)), nil }, } + stubFilesClient(t, mock) - err := getWithClient(mock, []string{"/some/path/file.txt"}) + err := get(testGetCmd(), []string{"/some/path/file.txt"}) if err != nil { t.Fatalf("expected nil error, got %v", err) } @@ -143,6 +149,12 @@ func TestGetDstAppendsToDirectory(t *testing.T) { content := "pdf content" mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return &files.FileMetadata{ + Metadata: files.Metadata{PathDisplay: arg.Path}, + Size: uint64(len(content)), + }, nil + }, downloadFn: func(arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { if arg.Path != "/remote/doc.pdf" { t.Errorf("download path = %q, want %q", arg.Path, "/remote/doc.pdf") @@ -154,8 +166,9 @@ func TestGetDstAppendsToDirectory(t *testing.T) { return meta, io.NopCloser(strings.NewReader(content)), nil }, } + stubFilesClient(t, mock) - err := getWithClient(mock, []string{"/remote/doc.pdf", tmpDir}) + err := get(testGetCmd(), []string{"/remote/doc.pdf", tmpDir}) if err != nil { t.Fatalf("expected nil error, got %v", err) } @@ -591,6 +604,159 @@ func TestGetFileCommandDownloadsAfterMetadata(t *testing.T) { } } +func TestGetDropboxReferencesPassThroughUnchanged(t *testing.T) { + tests := []struct { + name string + reference string + }{ + {name: "revision", reference: "rev:opaque-revision"}, + {name: "file id", reference: "id:opaque-id"}, + {name: "namespace path", reference: "ns:123/remote/file.txt"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dst := filepath.Join(t.TempDir(), "file.txt") + var metadataPath, downloadPath string + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + metadataPath = arg.Path + return getTestFileMetadata("/remote/file.txt", 4), nil + }, + downloadFn: func(arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { + downloadPath = arg.Path + return getTestFileMetadata("/remote/file.txt", 4), io.NopCloser(strings.NewReader("data")), nil + }, + } + stubFilesClient(t, mock) + + cmd := testGetCmd() + if err := get(cmd, []string{tt.reference, dst}); err != nil { + t.Fatalf("get error: %v", err) + } + if metadataPath != tt.reference { + t.Fatalf("metadata path = %q, want %q", metadataPath, tt.reference) + } + if downloadPath != tt.reference { + t.Fatalf("download path = %q, want %q", downloadPath, tt.reference) + } + }) + } +} + +func TestGetDropboxReferenceDefaultTargetUsesMetadataName(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return &files.FileMetadata{ + Metadata: files.Metadata{Name: "historical.txt", PathDisplay: "/remote/historical.txt"}, + Size: 4, + }, nil + }, + downloadFn: func(arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { + if arg.Path != "rev:opaque-revision" { + t.Fatalf("download path = %q, want rev:opaque-revision", arg.Path) + } + return &files.FileMetadata{ + Metadata: files.Metadata{Name: "historical.txt", PathDisplay: "/remote/historical.txt"}, + Size: 4, + }, io.NopCloser(strings.NewReader("data")), nil + }, + } + stubFilesClient(t, mock) + + cmd := testGetCmd() + if err := get(cmd, []string{"rev:opaque-revision"}); err != nil { + t.Fatalf("get error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "historical.txt")) + if err != nil { + t.Fatalf("read downloaded file: %v", err) + } + if string(content) != "data" { + t.Fatalf("downloaded file = %q, want data", content) + } +} + +func TestGetDropboxReferenceDirectoryTargetRequiresMetadataName(t *testing.T) { + dst := t.TempDir() + downloadCalled := false + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + return nil, errors.New("metadata unavailable") + }, + downloadFn: func(arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { + downloadCalled = true + return nil, nil, errors.New("unexpected download") + }, + } + stubFilesClient(t, mock) + + err := get(testGetCmd(), []string{"rev:opaque-revision", dst}) + if err == nil || !strings.Contains(err.Error(), "metadata unavailable") { + t.Fatalf("error = %v, want metadata error", err) + } + if downloadCalled { + t.Fatal("download called without a metadata-derived filename") + } +} + +func TestGetRecursiveDropboxReferenceUsesMetadataPathForLayout(t *testing.T) { + dst := filepath.Join(t.TempDir(), "output") + const reference = "id:opaque-folder-id" + + mock := &mockFilesClient{ + getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { + if arg.Path != reference { + t.Fatalf("metadata path = %q, want %q", arg.Path, reference) + } + return &files.FolderMetadata{ + Metadata: files.Metadata{Name: "folder", PathDisplay: "/remote/folder"}, + }, nil + }, + listFolderFn: func(arg *files.ListFolderArg) (*files.ListFolderResult, error) { + if arg.Path != reference { + t.Fatalf("list path = %q, want %q", arg.Path, reference) + } + return &files.ListFolderResult{ + Entries: []files.IsMetadata{ + &files.FileMetadata{ + Metadata: files.Metadata{Name: "file.txt", PathDisplay: "/remote/folder/file.txt"}, + Size: 4, + }, + }, + }, nil + }, + downloadFn: func(arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { + if arg.Path != "/remote/folder/file.txt" { + t.Fatalf("download path = %q, want /remote/folder/file.txt", arg.Path) + } + return &files.FileMetadata{ + Metadata: files.Metadata{Name: "file.txt", PathDisplay: arg.Path}, + Size: 4, + }, io.NopCloser(strings.NewReader("data")), nil + }, + } + stubFilesClient(t, mock) + + cmd := testGetCmd() + if err := cmd.Flags().Set("recursive", "true"); err != nil { + t.Fatalf("set recursive flag: %v", err) + } + if err := get(cmd, []string{reference, dst}); err != nil { + t.Fatalf("get error: %v", err) + } + content, err := os.ReadFile(filepath.Join(dst, "file.txt")) + if err != nil { + t.Fatalf("read downloaded file: %v", err) + } + if string(content) != "data" { + t.Fatalf("downloaded file = %q, want data", content) + } +} + func TestGetJSONFileOutputsDownloadedResult(t *testing.T) { tmpDir := t.TempDir() dst := filepath.Join(tmpDir, "file.txt") @@ -647,14 +813,7 @@ func TestGetJSONFileOutputsDownloadedResult(t *testing.T) { func TestGetJSONDefaultTargetUsesBasename(t *testing.T) { tmpDir := t.TempDir() - origDir, err := os.Getwd() - if err != nil { - t.Fatalf("get working directory: %v", err) - } - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("chdir temp dir: %v", err) - } - defer func() { _ = os.Chdir(origDir) }() + t.Chdir(tmpDir) mock := &mockFilesClient{ getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) { diff --git a/cmd/help_manifest.go b/cmd/help_manifest.go index d7f6b26..3b179a2 100644 --- a/cmd/help_manifest.go +++ b/cmd/help_manifest.go @@ -124,10 +124,13 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{ }, "get": { Args: []jsonCommandArg{ - commandArg("source", true, false, "dropbox_path", "Dropbox file or folder path"), + commandArg("source", true, false, "dropbox_path", "Dropbox path, file ID, revision, or namespace-relative path"), streamCommandArg("target", false, false, "local_path", "Local destination path, or - for stdout"), }, - Examples: []jsonCommandExample{{Description: "Download a file", Command: "dbxcli get /remote.txt ./remote.txt"}}, + Examples: []jsonCommandExample{ + {Description: "Download a file", Command: "dbxcli get /remote.txt ./remote.txt"}, + {Description: "Download a file revision", Command: "dbxcli get rev:a1c10ce0dd78 ./historical.txt"}, + }, Flags: map[string]jsonCommandFlagMetadata{"recursive": {ValueKind: "boolean"}}, DropboxScopes: []string{"files.content.read", "files.metadata.read"}, StdinStdout: jsonCommandStdinStdout{WritesBinaryStdout: true}, diff --git a/cmd/metadata.go b/cmd/metadata.go index 05afe2a..b2957d2 100644 --- a/cmd/metadata.go +++ b/cmd/metadata.go @@ -14,7 +14,11 @@ package cmd -import "strings" +import ( + "strings" + + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files" +) func metadataDisplayPath(inputPath, metadataPath string) string { if metadataPath != "" { @@ -36,3 +40,37 @@ func sameDropboxMetadataPath(pathDisplay, pathLower, requested string) bool { } return true } + +func baseMetadata(metadata files.IsMetadata) *files.Metadata { + switch entry := metadata.(type) { + case *files.Metadata: + return entry + case *files.FileMetadata: + if entry != nil { + return &entry.Metadata + } + case *files.FolderMetadata: + if entry != nil { + return &entry.Metadata + } + case *files.DeletedMetadata: + if entry != nil { + return &entry.Metadata + } + } + return nil +} + +func metadataName(metadata files.IsMetadata) string { + if base := baseMetadata(metadata); base != nil { + return base.Name + } + return "" +} + +func metadataPathDisplay(metadata files.IsMetadata) string { + if base := baseMetadata(metadata); base != nil { + return base.PathDisplay + } + return "" +} diff --git a/cmd/metadata_test.go b/cmd/metadata_test.go new file mode 100644 index 0000000..db10b2d --- /dev/null +++ b/cmd/metadata_test.go @@ -0,0 +1,54 @@ +// Copyright © 2026 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files" +) + +func TestMetadataCommonFields(t *testing.T) { + tests := []struct { + name string + metadata files.IsMetadata + }{ + {name: "metadata", metadata: &files.Metadata{Name: "metadata", PathDisplay: "/metadata"}}, + {name: "file", metadata: &files.FileMetadata{Metadata: files.Metadata{Name: "file", PathDisplay: "/file"}}}, + {name: "folder", metadata: &files.FolderMetadata{Metadata: files.Metadata{Name: "folder", PathDisplay: "/folder"}}}, + {name: "deleted", metadata: &files.DeletedMetadata{Metadata: files.Metadata{Name: "deleted", PathDisplay: "/deleted"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := metadataName(tt.metadata); got != tt.name { + t.Fatalf("metadataName() = %q, want %q", got, tt.name) + } + if got := metadataPathDisplay(tt.metadata); got != "/"+tt.name { + t.Fatalf("metadataPathDisplay() = %q, want %q", got, "/"+tt.name) + } + }) + } +} + +func TestMetadataCommonFieldsHandleNil(t *testing.T) { + var metadata *files.FileMetadata + if got := metadataName(metadata); got != "" { + t.Fatalf("metadataName() = %q, want empty", got) + } + if got := metadataPathDisplay(metadata); got != "" { + t.Fatalf("metadataPathDisplay() = %q, want empty", got) + } +} diff --git a/cmd/pipe_test.go b/cmd/pipe_test.go index 0548ae4..f8e2f95 100644 --- a/cmd/pipe_test.go +++ b/cmd/pipe_test.go @@ -585,9 +585,7 @@ func TestGetStdout_RejectsRecursive(t *testing.T) { func TestGetStdout_NoLocalFilesCreated(t *testing.T) { tmpDir := t.TempDir() - origDir, _ := os.Getwd() - _ = os.Chdir(tmpDir) - defer func() { _ = os.Chdir(origDir) }() + t.Chdir(tmpDir) content := "file data" mock := &mockFilesClient{ diff --git a/docs/commands/dbxcli_get.md b/docs/commands/dbxcli_get.md index 6459fcd..1afdf7f 100644 --- a/docs/commands/dbxcli_get.md +++ b/docs/commands/dbxcli_get.md @@ -7,6 +7,8 @@ Download a file or folder ### Synopsis Download a file or folder from Dropbox. + - Source may be a Dropbox path, file ID (id:), revision (rev:), or + namespace-relative path (ns:). - Use --recursive (-r) to download entire directories. - Use - as target to write file bytes to stdout. Stdout is byte-clean: all progress and errors go to stderr. @@ -20,6 +22,7 @@ dbxcli get [flags] [] ``` dbxcli get /remote/file.txt ./local-file.txt + dbxcli get rev:a1c10ce0dd78 ./historical-file.txt dbxcli get -r /remote/folder ./local-folder dbxcli get /backups/src.tgz - | tar tz dbxcli get /file.txt - > local-copy.txt