@@ -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
3233func 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
3781func 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+
60179func 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
156275var 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
162281func init () {
163282 RootCmd .AddCommand (getCmd )
283+ getCmd .Flags ().BoolP ("recursive" , "r" , false , "Recursively download a folder" )
164284}
0 commit comments