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  }