go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/client/cmd/gitiles/archive.go (about) 1 // Copyright 2018 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package main 16 17 import ( 18 "context" 19 "fmt" 20 "os" 21 "sort" 22 "strings" 23 24 humanize "github.com/dustin/go-humanize" 25 "github.com/maruel/subcommands" 26 27 "go.chromium.org/luci/auth" 28 "go.chromium.org/luci/common/api/gitiles" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/logging" 31 gitilespb "go.chromium.org/luci/common/proto/gitiles" 32 "go.chromium.org/luci/common/retry" 33 "go.chromium.org/luci/common/retry/transient" 34 "go.chromium.org/luci/grpc/grpcutil" 35 ) 36 37 func cmdArchive(authOpts auth.Options) *subcommands.Command { 38 39 return &subcommands.Command{ 40 UsageLine: "archive <options> repository-url committish", 41 ShortDesc: "downloads an archive at the given repo committish", 42 LongDesc: `Downloads an archive of a repo or a given path in the repo at 43 committish. 44 45 This tool does not stream the archive, so the full contents are stored in 46 memory before being written to disk. 47 `, 48 CommandRun: func() subcommands.CommandRun { 49 c := archiveRun{} 50 c.commonFlags.Init(authOpts) 51 c.Flags.StringVar(&c.rawFormat, "format", "GZIP", 52 fmt.Sprintf("Format of the archive requested. One of %s", formatChoices())) 53 c.Flags.StringVar(&c.output, "output", "", "Path to write archive to.") 54 c.Flags.StringVar(&c.path, "path", "", "Relative path to the repo project root.") 55 return &c 56 }, 57 } 58 } 59 60 type archiveRun struct { 61 commonFlags 62 format gitilespb.ArchiveRequest_Format 63 output string 64 path string 65 66 rawFormat string 67 } 68 69 func (c *archiveRun) Parse(a subcommands.Application, args []string) error { 70 if err := c.commonFlags.Parse(); err != nil { 71 return err 72 } 73 if len(args) != 2 { 74 return errors.New("exactly 2 position arguments are expected") 75 } 76 if c.format = parseFormat(c.rawFormat); c.format == gitilespb.ArchiveRequest_Invalid { 77 return errors.New("invalid archive format requested") 78 } 79 return nil 80 } 81 82 func formatChoices() []string { 83 cs := make([]string, 0, len(gitilespb.ArchiveRequest_Format_value)) 84 for k := range gitilespb.ArchiveRequest_Format_value { 85 cs = append(cs, k) 86 } 87 sort.Strings(cs) 88 return cs 89 } 90 91 func parseFormat(f string) gitilespb.ArchiveRequest_Format { 92 return gitilespb.ArchiveRequest_Format(gitilespb.ArchiveRequest_Format_value[strings.ToUpper(f)]) 93 } 94 95 func (c *archiveRun) main(a subcommands.Application, args []string) error { 96 ctx := c.defaultFlags.MakeLoggingContext(os.Stderr) 97 host, project, err := gitiles.ParseRepoURL(args[0]) 98 if err != nil { 99 return errors.Annotate(err, "invalid repo URL %q", args[0]).Err() 100 } 101 ref := args[1] 102 req := &gitilespb.ArchiveRequest{ 103 Format: c.format, 104 Project: project, 105 Ref: ref, 106 Path: c.path, 107 } 108 109 authCl, err := c.createAuthClient() 110 if err != nil { 111 return err 112 } 113 g, err := gitiles.NewRESTClient(authCl, host, true) 114 if err != nil { 115 return err 116 } 117 118 var res *gitilespb.ArchiveResponse 119 if err := retry.Retry(ctx, transient.Only(retry.Default), func() error { 120 var err error 121 res, err = g.Archive(ctx, req) 122 return grpcutil.WrapIfTransient(err) 123 }, nil); err != nil { 124 return err 125 } 126 127 return c.dumpArchive(ctx, res) 128 } 129 130 func (c *archiveRun) dumpArchive(ctx context.Context, res *gitilespb.ArchiveResponse) error { 131 var oPath string 132 switch { 133 case c.output != "": 134 oPath = c.output 135 case res.Filename != "": 136 oPath = res.Filename 137 default: 138 return errors.New("No output path specified and no suggested archive name from remote") 139 } 140 141 f, err := os.Create(oPath) 142 if err != nil { 143 return errors.Annotate(err, "failed to open file to write archive").Err() 144 } 145 defer f.Close() 146 147 l, err := f.Write(res.Contents) 148 logging.Infof(ctx, "Archive written to %s (size: %s)", oPath, humanize.Bytes(uint64(l))) 149 return err 150 } 151 152 func (c *archiveRun) Run(a subcommands.Application, args []string, _ subcommands.Env) int { 153 if err := c.Parse(a, args); err != nil { 154 fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) 155 return 1 156 } 157 if err := c.main(a, args); err != nil { 158 fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) 159 return 1 160 } 161 return 0 162 }