Skip to content

Commit da83386

Browse files
authored
feat(cli): Allow to pass --output flag to artifact download (#726)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent 2712794 commit da83386

File tree

2 files changed

+48
-13
lines changed

2 files changed

+48
-13
lines changed

app/cli/cmd/artifact_download.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -16,14 +16,16 @@
1616
package cmd
1717

1818
import (
19+
"errors"
20+
1921
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
2022
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2123
"github.com/spf13/cobra"
2224
"google.golang.org/grpc"
2325
)
2426

2527
func newArtifactDownloadCmd() *cobra.Command {
26-
var digest, downloadPath string
28+
var digest, downloadPath, outputFile string
2729
var artifactCASConn *grpc.ClientConn
2830

2931
cmd := &cobra.Command{
@@ -32,6 +34,10 @@ func newArtifactDownloadCmd() *cobra.Command {
3234
PreRunE: func(cmd *cobra.Command, args []string) error {
3335
var err error
3436

37+
if err := validateFlags(downloadPath, outputFile); err != nil {
38+
return err
39+
}
40+
3541
// Retrieve temporary credentials for uploading
3642
artifactCASConn, err = wrappedArtifactConn(actionOpts.CPConnection,
3743
pb.CASCredentialsServiceGetRequest_ROLE_DOWNLOADER, digest)
@@ -45,9 +51,10 @@ func newArtifactDownloadCmd() *cobra.Command {
4551
opts := &action.ArtifactDownloadOpts{
4652
ActionsOpts: actionOpts,
4753
ArtifactsCASConn: artifactCASConn,
54+
Stdout: cmd.OutOrStdout(),
4855
}
4956

50-
return action.NewArtifactDownload(opts).Run(downloadPath, digest)
57+
return action.NewArtifactDownload(opts).Run(downloadPath, outputFile, digest)
5158
},
5259
PostRunE: func(cmd *cobra.Command, args []string) error {
5360
if artifactCASConn != nil {
@@ -62,6 +69,16 @@ func newArtifactDownloadCmd() *cobra.Command {
6269
err := cmd.MarkFlagRequired("digest")
6370
cobra.CheckErr(err)
6471
cmd.Flags().StringVar(&downloadPath, "path", "", "download path, default to current directory")
72+
cmd.Flags().StringVar(&outputFile, "output", "", "The `file` to write a single asset to (use \"-\" to write to standard output")
6573

6674
return cmd
6775
}
76+
77+
// validateFlags checks if the flags are valid
78+
func validateFlags(downloadPath, outputFile string) error {
79+
if downloadPath != "" && outputFile != "" {
80+
return errors.New("cannot specify both --path and --output flags at the same time")
81+
}
82+
83+
return nil
84+
}

app/cli/internal/action/artifact_download.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,20 @@ import (
3535
type ArtifactDownload struct {
3636
*ActionsOpts
3737
artifactsCASConn *grpc.ClientConn
38+
stdout io.Writer
3839
}
3940

4041
type ArtifactDownloadOpts struct {
4142
*ActionsOpts
4243
ArtifactsCASConn *grpc.ClientConn
44+
Stdout io.Writer
4345
}
4446

4547
func NewArtifactDownload(opts *ArtifactDownloadOpts) *ArtifactDownload {
46-
return &ArtifactDownload{opts.ActionsOpts, opts.ArtifactsCASConn}
48+
return &ArtifactDownload{opts.ActionsOpts, opts.ArtifactsCASConn, opts.Stdout}
4749
}
4850

49-
func (a *ArtifactDownload) Run(downloadPath, digest string) error {
51+
func (a *ArtifactDownload) Run(downloadPath, outputFile, digest string) error {
5052
h, err := crv1.NewHash(digest)
5153
if err != nil {
5254
return fmt.Errorf("invalid digest: %w", err)
@@ -59,27 +61,41 @@ func (a *ArtifactDownload) Run(downloadPath, digest string) error {
5961
return fmt.Errorf("artifact with digest %s not found", h)
6062
}
6163

64+
if downloadPath != "" && outputFile != "" {
65+
return errors.New("both downloadPath and outputFile cannot be set at the same time")
66+
}
67+
6268
if downloadPath == "" {
63-
var err error
6469
downloadPath, err = os.Getwd()
6570
if err != nil {
6671
return fmt.Errorf("getting current dir: %w", err)
6772
}
6873
}
6974

70-
downloadPath = path.Join(downloadPath, info.Filename)
71-
f, err := os.Create(downloadPath)
72-
if err != nil {
73-
return fmt.Errorf("creating destination file: %w", err)
75+
// Determine output destination
76+
outputPath := path.Join(downloadPath, info.Filename)
77+
if outputFile != "" && outputFile != "-" {
78+
outputPath = outputFile
79+
}
80+
81+
// Open output file
82+
var f io.Writer
83+
if outputFile == "-" {
84+
f = a.stdout
85+
} else {
86+
f, err = os.Create(outputPath)
87+
if err != nil {
88+
return fmt.Errorf("creating destination file: %w", err)
89+
}
90+
defer f.(*os.File).Close()
91+
92+
a.Logger.Info().Str("name", outputFile).Str("to", outputPath).Msg("downloading file")
7493
}
75-
defer f.Close()
7694

7795
// Calculate the checksum as we write it to a file
7896
hash := sha256.New()
7997
w := io.MultiWriter(f, hash)
8098

81-
a.Logger.Info().Str("name", info.Filename).Str("to", downloadPath).Msg("downloading file")
82-
8399
// render progress bar
84100
go renderOperationStatus(ctx, client.ProgressStatus, info.Size)
85101
defer close(client.ProgressStatus)
@@ -108,6 +124,8 @@ func renderOperationStatus(ctx context.Context, progressChan casclient.ProgressS
108124
pw.Style().Visibility.ETA = true
109125
pw.Style().Visibility.Speed = true
110126
pw.SetUpdateFrequency(progress.DefaultUpdateFrequency)
127+
// Render to stderr to avoid interfering with the output if the flag --output is set to "-"
128+
pw.SetOutputWriter(os.Stderr)
111129

112130
var tracker *progress.Tracker
113131
go pw.Render()

0 commit comments

Comments
 (0)