Skip to content

Commit d7c23c7

Browse files
Merge pull request #232 from dropbox/get-recursive
Add recursive folder download (get -r)
2 parents 4a8191a + 39725c1 commit d7c23c7

4 files changed

Lines changed: 496 additions & 7 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* Supports search with sorting and flexible time formatting
1616
* Supports file revisions and file restore
1717
* Chunked uploads for large files, paginated listing for large directories
18-
* Recursive directory uploads (`put -r`)
18+
* Recursive directory uploads (`put -r`) and downloads (`get -r`)
1919
* Retry with exponential backoff for uploads and downloads
2020
* Supports a growing set of Team operations
2121

@@ -80,7 +80,7 @@ Available Commands:
8080
completion Generate the autocompletion script for the specified shell
8181
cp Copy a file or folder to a different location
8282
du Display usage information
83-
get Download a file
83+
get Download a file or folder
8484
logout Log out of the current session
8585
ls List files and folders
8686
mkdir Create a new directory
@@ -183,6 +183,13 @@ $ dbxcli put -r ./project /backup/project # recursively upload a direct
183183
$ dbxcli put -r -w 8 ./large-folder /backup/large # use 8 workers per large file
184184
```
185185

186+
### Downloading files and directories
187+
188+
```sh
189+
$ dbxcli get /remote/file.txt ./local-file.txt # download a single file
190+
$ dbxcli get -r /remote/folder ./local-folder # recursively download a folder
191+
```
192+
186193
### Creating directories
187194

188195
```sh

cmd/get.go

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222
"path"
2323
"path/filepath"
24+
"strings"
2425
"time"
2526

2627
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
@@ -30,8 +31,51 @@ import (
3031
)
3132

3233
func get(cmd *cobra.Command, args []string) (err error) {
33-
dbx := files.New(config)
34-
return getWithClient(dbx, args)
34+
if len(args) == 0 || len(args) > 2 {
35+
return errors.New("`get` requires `src` and/or `dst` arguments")
36+
}
37+
38+
src, err := validatePath(args[0])
39+
if err != nil {
40+
return
41+
}
42+
43+
dst := path.Base(src)
44+
if len(args) == 2 {
45+
dst = args[1]
46+
}
47+
48+
recursive, _ := cmd.Flags().GetBool("recursive")
49+
50+
dbx := filesNewFunc(config)
51+
52+
meta, err := dbx.GetMetadata(files.NewGetMetadataArg(src))
53+
if err != nil {
54+
if recursive {
55+
return fmt.Errorf("get metadata for %s: %v", src, err)
56+
}
57+
// For non-recursive, fall through to download (will fail with proper error)
58+
if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() {
59+
dst = filepath.Join(dst, path.Base(src))
60+
}
61+
return downloadFile(dbx, src, dst)
62+
}
63+
64+
if _, ok := meta.(*files.FolderMetadata); ok {
65+
if !recursive {
66+
return fmt.Errorf("%s is a folder (use --recursive to download folders)", src)
67+
}
68+
if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() {
69+
dst = filepath.Join(dst, path.Base(src))
70+
}
71+
return getRecursive(dbx, src, dst)
72+
}
73+
74+
if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() {
75+
dst = filepath.Join(dst, path.Base(src))
76+
}
77+
78+
return downloadFile(dbx, src, dst)
3579
}
3680

3781
func getWithClient(dbx files.Client, args []string) (err error) {
@@ -44,19 +88,94 @@ func getWithClient(dbx files.Client, args []string) (err error) {
4488
return
4589
}
4690

47-
// Default `dst` to the base segment of the source path; use the second argument if provided.
4891
dst := path.Base(src)
4992
if len(args) == 2 {
5093
dst = args[1]
5194
}
52-
// If `dst` is a directory, append the source filename.
5395
if f, err := os.Stat(dst); err == nil && f.IsDir() {
5496
dst = filepath.Join(dst, path.Base(src))
5597
}
5698

5799
return downloadFile(dbx, src, dst)
58100
}
59101

102+
func getRecursive(dbx files.Client, src, dst string) error {
103+
arg := files.NewListFolderArg(src)
104+
arg.Recursive = true
105+
106+
res, err := dbx.ListFolder(arg)
107+
if err != nil {
108+
return fmt.Errorf("list folder %s: %v", src, err)
109+
}
110+
111+
var entries []files.IsMetadata
112+
entries = append(entries, res.Entries...)
113+
for res.HasMore {
114+
cont := files.NewListFolderContinueArg(res.Cursor)
115+
res, err = dbx.ListFolderContinue(cont)
116+
if err != nil {
117+
return fmt.Errorf("list folder continue: %v", err)
118+
}
119+
entries = append(entries, res.Entries...)
120+
}
121+
122+
if err := os.MkdirAll(dst, 0755); err != nil {
123+
return err
124+
}
125+
126+
var downloadErrors []error
127+
128+
for _, entry := range entries {
129+
switch f := entry.(type) {
130+
case *files.FolderMetadata:
131+
relPath, err := relativeTo(src, f.PathDisplay)
132+
if err != nil {
133+
downloadErrors = append(downloadErrors, err)
134+
continue
135+
}
136+
localDir := filepath.Join(dst, filepath.FromSlash(relPath))
137+
if err := os.MkdirAll(localDir, 0755); err != nil {
138+
downloadErrors = append(downloadErrors, fmt.Errorf("mkdir %s: %w", localDir, err))
139+
}
140+
case *files.FileMetadata:
141+
relPath, err := relativeTo(src, f.PathDisplay)
142+
if err != nil {
143+
downloadErrors = append(downloadErrors, err)
144+
continue
145+
}
146+
localPath := filepath.Join(dst, filepath.FromSlash(relPath))
147+
if err := os.MkdirAll(filepath.Dir(localPath), 0755); err != nil {
148+
downloadErrors = append(downloadErrors, fmt.Errorf("mkdir %s: %w", filepath.Dir(localPath), err))
149+
continue
150+
}
151+
fmt.Fprintf(os.Stderr, "Downloading %s -> %s\n", f.PathDisplay, localPath)
152+
if err := downloadFile(dbx, f.PathDisplay, localPath); err != nil {
153+
downloadErrors = append(downloadErrors, fmt.Errorf("%s: %w", f.PathDisplay, err))
154+
}
155+
}
156+
}
157+
158+
if len(downloadErrors) > 0 {
159+
for _, e := range downloadErrors {
160+
fmt.Fprintf(os.Stderr, "Error: %v\n", e)
161+
}
162+
return fmt.Errorf("get: %d error(s)", len(downloadErrors))
163+
}
164+
165+
return nil
166+
}
167+
168+
func relativeTo(base, full string) (string, error) {
169+
baseLower := strings.ToLower(base)
170+
fullLower := strings.ToLower(full)
171+
if fullLower != baseLower && !strings.HasPrefix(fullLower, baseLower+"/") {
172+
return "", fmt.Errorf("path %q is not under %q", full, base)
173+
}
174+
rel := full[len(base):]
175+
rel = strings.TrimPrefix(rel, "/")
176+
return rel, nil
177+
}
178+
60179
func downloadFile(dbx files.Client, src string, dst string) error {
61180
arg := files.NewDownloadArg(src)
62181

@@ -155,10 +274,11 @@ func downloadFileOnce(dbx files.Client, arg *files.DownloadArg, dst string) erro
155274
// getCmd represents the get command
156275
var getCmd = &cobra.Command{
157276
Use: "get [flags] <source> [<target>]",
158-
Short: "Download a file",
277+
Short: "Download a file or folder",
159278
RunE: get,
160279
}
161280

162281
func init() {
163282
RootCmd.AddCommand(getCmd)
283+
getCmd.Flags().BoolP("recursive", "r", false, "Recursively download a folder")
164284
}

0 commit comments

Comments
 (0)