go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/artifacts/copy.go (about)

     1  // Copyright 2019 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"flag"
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/google/subcommands"
    20  	"go.chromium.org/luci/auth/client/authcli"
    21  	"go.chromium.org/luci/hardcoded/chromeinfra"
    22  	"go.fuchsia.dev/infra/artifacts"
    23  	"go.fuchsia.dev/infra/buildbucket"
    24  	"golang.org/x/sync/errgroup"
    25  )
    26  
    27  var stdout io.Writer = os.Stdout
    28  
    29  type artifactsClient interface {
    30  	GetBuildDir(bucket, build string) artifacts.Directory
    31  }
    32  
    33  type CopyCommand struct {
    34  	authFlags authcli.Flags
    35  
    36  	build string
    37  	// The remote filepath with the target build's Cloud Storage directory.
    38  	source string
    39  
    40  	// The local path to write the artifact to.
    41  	dest string
    42  
    43  	// Whether source should be relative to the Cloud Storage bucket's root directory.
    44  	// If false, source will be relative to the target build's Cloud Storage directory.
    45  	fromRoot bool
    46  
    47  	// The file containing the source paths to download.
    48  	srcsFile string
    49  
    50  	// The maximum number of concurrent downloading processes.
    51  	j int
    52  
    53  	// Whether to print verbose logs.
    54  	verbose bool
    55  }
    56  
    57  func (*CopyCommand) Name() string {
    58  	return "cp"
    59  }
    60  
    61  func (*CopyCommand) Usage() string {
    62  	return "cp [flags...]"
    63  }
    64  
    65  func (*CopyCommand) Synopsis() string {
    66  	return "fetches an artifact produced by a Fuchsia builder"
    67  }
    68  
    69  func (cmd *CopyCommand) SetFlags(f *flag.FlagSet) {
    70  	cmd.authFlags.Register(flag.CommandLine, chromeinfra.DefaultAuthOptions())
    71  	f.StringVar(&cmd.build, "build", "", "the ID of the build that produced the artifacts")
    72  	f.StringVar(&cmd.source, "src", "", "The artifact file or directory to download from the build's Cloud Storage directory")
    73  	f.StringVar(&cmd.dest, "dst", "", "The local path to write the artifact(s) to")
    74  	f.BoolVar(&cmd.fromRoot, "root", false, "Whether src is relative to the root directory of the Cloud Storage bucket."+
    75  		"If false, src will be taken as a relative path to the build-specific directory under the Cloud Storage bucket.")
    76  	f.StringVar(&cmd.srcsFile, "srcs-file", "", "The file containing the source paths of the artifacts to download. These should be listed one path per line.")
    77  	f.IntVar(&cmd.j, "j", 30, "The maximum number of concurrent downloading processes.")
    78  	f.BoolVar(&cmd.verbose, "v", false, "Whether to print verbose logs.")
    79  }
    80  
    81  func (cmd *CopyCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...any) subcommands.ExitStatus {
    82  	if err := cmd.validateAndExecute(ctx); err != nil {
    83  		log.Println(err)
    84  		return subcommands.ExitFailure
    85  	}
    86  	return subcommands.ExitSuccess
    87  }
    88  
    89  func (cmd *CopyCommand) validateAndExecute(ctx context.Context) error {
    90  	opts, err := cmd.authFlags.Options()
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	if cmd.source == "" && cmd.srcsFile == "" {
    96  		return fmt.Errorf("must provide at least one of -src or -srcs-file")
    97  	}
    98  
    99  	if cmd.dest == "" {
   100  		return fmt.Errorf("missing -dst")
   101  	}
   102  
   103  	buildsCli, err := buildbucket.NewBuildsClient(ctx, buildbucket.DefaultHost, opts)
   104  	if err != nil {
   105  		return fmt.Errorf("failed to create builds client: %w", err)
   106  	}
   107  
   108  	artifactsCli, err := artifacts.NewClient(ctx, opts)
   109  	if err != nil {
   110  		return fmt.Errorf("failed to create artifacts client: %w", err)
   111  	}
   112  
   113  	return cmd.execute(ctx, buildsCli, artifactsCli)
   114  }
   115  
   116  type download struct {
   117  	src  string
   118  	dest string
   119  }
   120  
   121  func (cmd *CopyCommand) execute(ctx context.Context, buildsCli buildsClient, artifactsCli artifactsClient) error {
   122  	bucket, err := getStorageBucket(ctx, buildsCli, cmd.build)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	build := cmd.build
   128  	if cmd.fromRoot {
   129  		build = ""
   130  	}
   131  	dir := artifactsCli.GetBuildDir(bucket, build)
   132  
   133  	var sourceList []download
   134  	if cmd.srcsFile != "" {
   135  		sourceFiles, err := os.ReadFile(cmd.srcsFile)
   136  		if err != nil {
   137  			return fmt.Errorf("failed to read src file %s: %w", cmd.srcsFile, err)
   138  		}
   139  		for _, src := range strings.Split(string(sourceFiles), "\n") {
   140  			if src == "" {
   141  				continue
   142  			}
   143  			sourceList = append(sourceList, cmd.constructDownloads(cmd.source, []string{src})...)
   144  		}
   145  	} else {
   146  		objs, err := dir.List(ctx, cmd.source)
   147  		if err != nil {
   148  			return err
   149  		}
   150  		sourceList = append(sourceList, cmd.constructDownloads(cmd.source, objs)...)
   151  	}
   152  
   153  	eg, ctx := errgroup.WithContext(ctx)
   154  	queueDownloads := make(chan download, len(sourceList))
   155  	eg.Go(func() error {
   156  		defer func() {
   157  			close(queueDownloads)
   158  		}()
   159  		for _, s := range sourceList {
   160  			queueDownloads <- s
   161  		}
   162  		return nil
   163  	})
   164  
   165  	var mu sync.Mutex
   166  	var downloadedFileSizes []int64
   167  	downloadFile := func() error {
   168  		for download := range queueDownloads {
   169  			startTime := time.Now()
   170  			size, err := dir.CopyFile(ctx, download.src, download.dest)
   171  			duration := time.Now().Sub(startTime)
   172  			logStr := fmt.Sprintf("%s (%d bytes) to %s in %s", download.src, size, download.dest, duration)
   173  			if err != nil {
   174  				logStr = fmt.Sprintf("failed to download %s: %s", logStr, err)
   175  			}
   176  			mu.Lock()
   177  			if cmd.verbose || err != nil {
   178  				fmt.Fprintln(stdout, logStr)
   179  			}
   180  			downloadedFileSizes = append(downloadedFileSizes, size)
   181  			mu.Unlock()
   182  			if err != nil {
   183  				return err
   184  			}
   185  		}
   186  		return nil
   187  	}
   188  	startTime := time.Now()
   189  	for range cmd.j {
   190  		eg.Go(downloadFile)
   191  	}
   192  	err = eg.Wait()
   193  	totalDuration := time.Now().Sub(startTime)
   194  	var totalSize int64
   195  	for _, size := range downloadedFileSizes {
   196  		totalSize += size
   197  	}
   198  	fmt.Fprintf(stdout, "Num files: %d\nTotal bytes: %d\nDuration: %s\nSpeed: %.03fMB/s\n",
   199  		len(downloadedFileSizes), totalSize, totalDuration, float64(totalSize)/(1000000*totalDuration.Seconds()))
   200  	return err
   201  }
   202  
   203  func (cmd *CopyCommand) constructDownloads(src string, list []string) []download {
   204  	var downloads []download
   205  	for _, obj := range list {
   206  		relPath := strings.TrimPrefix(obj, src)
   207  		dest := filepath.Join(cmd.dest, relPath)
   208  		downloads = append(downloads, download{src: obj, dest: dest})
   209  	}
   210  	return downloads
   211  }