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 }