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  }