github.com/supabase/cli@v1.168.1/internal/storage/cp/cp.go (about) 1 package cp 2 3 import ( 4 "context" 5 "fmt" 6 "io/fs" 7 "net/url" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 13 "github.com/go-errors/errors" 14 "github.com/spf13/afero" 15 "github.com/supabase/cli/internal/storage/client" 16 "github.com/supabase/cli/internal/storage/ls" 17 "github.com/supabase/cli/internal/utils" 18 "github.com/supabase/cli/internal/utils/flags" 19 "github.com/supabase/cli/pkg/storage" 20 ) 21 22 var errUnsupportedOperation = errors.New("Unsupported operation") 23 24 func Run(ctx context.Context, src, dst string, recursive bool, maxJobs uint, fsys afero.Fs, opts ...func(*storage.FileOptions)) error { 25 srcParsed, err := url.Parse(src) 26 if err != nil { 27 return errors.Errorf("failed to parse src url: %w", err) 28 } 29 dstParsed, err := url.Parse(dst) 30 if err != nil { 31 return errors.Errorf("failed to parse dst url: %w", err) 32 } 33 api, err := client.NewStorageAPI(ctx, flags.ProjectRef) 34 if err != nil { 35 return err 36 } 37 if strings.ToLower(srcParsed.Scheme) == client.STORAGE_SCHEME && dstParsed.Scheme == "" { 38 localPath := dst 39 if !filepath.IsAbs(dst) { 40 localPath = filepath.Join(utils.CurrentDirAbs, dst) 41 } 42 if recursive { 43 return DownloadStorageObjectAll(ctx, api, srcParsed.Path, localPath, maxJobs, fsys) 44 } 45 return api.DownloadObject(ctx, srcParsed.Path, localPath, fsys) 46 } else if srcParsed.Scheme == "" && strings.ToLower(dstParsed.Scheme) == client.STORAGE_SCHEME { 47 localPath := src 48 if !filepath.IsAbs(localPath) { 49 localPath = filepath.Join(utils.CurrentDirAbs, localPath) 50 } 51 if recursive { 52 return UploadStorageObjectAll(ctx, api, dstParsed.Path, localPath, maxJobs, fsys, opts...) 53 } 54 return api.UploadObject(ctx, dstParsed.Path, src, fsys, opts...) 55 } else if strings.ToLower(srcParsed.Scheme) == client.STORAGE_SCHEME && strings.ToLower(dstParsed.Scheme) == client.STORAGE_SCHEME { 56 return errors.New("Copying between buckets is not supported") 57 } 58 utils.CmdSuggestion = fmt.Sprintf("Run %s to copy between local directories.", utils.Aqua("cp -r <src> <dst>")) 59 return errors.New(errUnsupportedOperation) 60 } 61 62 func DownloadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remotePath, localPath string, maxJobs uint, fsys afero.Fs) error { 63 // Prepare local directory for download 64 if fi, err := fsys.Stat(localPath); err == nil && fi.IsDir() { 65 localPath = filepath.Join(localPath, path.Base(remotePath)) 66 } 67 // No need to be atomic because it's incremented only on main thread 68 count := 0 69 jq := utils.NewJobQueue(maxJobs) 70 err := ls.IterateStoragePathsAll(ctx, api, remotePath, func(objectPath string) error { 71 relPath := strings.TrimPrefix(objectPath, remotePath) 72 dstPath := filepath.Join(localPath, filepath.FromSlash(relPath)) 73 fmt.Fprintln(os.Stderr, "Downloading:", objectPath, "=>", dstPath) 74 count++ 75 job := func() error { 76 if strings.HasSuffix(objectPath, "/") { 77 return utils.MkdirIfNotExistFS(fsys, dstPath) 78 } 79 if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(dstPath)); err != nil { 80 return err 81 } 82 return api.DownloadObject(ctx, objectPath, dstPath, fsys) 83 } 84 return jq.Put(job) 85 }) 86 if count == 0 { 87 return errors.New("Object not found: " + remotePath) 88 } 89 return errors.Join(err, jq.Collect()) 90 } 91 92 func UploadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remotePath, localPath string, maxJobs uint, fsys afero.Fs, opts ...func(*storage.FileOptions)) error { 93 noSlash := strings.TrimSuffix(remotePath, "/") 94 // Check if directory exists on remote 95 dirExists := false 96 fileExists := false 97 if err := ls.IterateStoragePaths(ctx, api, noSlash, func(objectName string) error { 98 if objectName == path.Base(noSlash) { 99 fileExists = true 100 } 101 if objectName == path.Base(noSlash)+"/" { 102 dirExists = true 103 } 104 return nil 105 }); err != nil { 106 return err 107 } 108 baseName := filepath.Base(localPath) 109 jq := utils.NewJobQueue(maxJobs) 110 err := afero.Walk(fsys, localPath, func(filePath string, info fs.FileInfo, err error) error { 111 if err != nil { 112 return errors.New(err) 113 } 114 if !info.Mode().IsRegular() { 115 return nil 116 } 117 relPath, err := filepath.Rel(localPath, filePath) 118 if err != nil { 119 return errors.Errorf("failed to resolve relative path: %w", err) 120 } 121 dstPath := remotePath 122 // Copying single file 123 if relPath == "." { 124 _, prefix := client.SplitBucketPrefix(dstPath) 125 if IsDir(prefix) || (dirExists && !fileExists) { 126 dstPath = path.Join(dstPath, info.Name()) 127 } 128 } else { 129 if baseName != "." && (dirExists || len(noSlash) == 0) { 130 dstPath = path.Join(dstPath, baseName) 131 } 132 dstPath = path.Join(dstPath, relPath) 133 } 134 fmt.Fprintln(os.Stderr, "Uploading:", filePath, "=>", dstPath) 135 job := func() error { 136 err := api.UploadObject(ctx, dstPath, filePath, fsys, opts...) 137 if err != nil && strings.Contains(err.Error(), `"error":"Bucket not found"`) { 138 // Retry after creating bucket 139 if bucket, prefix := client.SplitBucketPrefix(dstPath); len(prefix) > 0 { 140 if _, err := api.CreateBucket(ctx, bucket); err != nil { 141 return err 142 } 143 err = api.UploadObject(ctx, dstPath, filePath, fsys, opts...) 144 } 145 } 146 return err 147 } 148 return jq.Put(job) 149 }) 150 return errors.Join(err, jq.Collect()) 151 } 152 153 func IsDir(objectPrefix string) bool { 154 return len(objectPrefix) == 0 || strings.HasSuffix(objectPrefix, "/") 155 }