Skip to content

Commit ea7d45e

Browse files
authored
add rwx artifacts download command (#274)
1 parent e2927f0 commit ea7d45e

File tree

11 files changed

+1185
-17
lines changed

11 files changed

+1185
-17
lines changed

cmd/rwx/artifacts.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"github.com/rwx-cloud/cli/cmd/rwx/artifacts"
5+
"github.com/rwx-cloud/cli/internal/cli"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var artifactsCmd = &cobra.Command{
10+
GroupID: "results",
11+
Use: "artifacts",
12+
Short: "Manage task artifacts",
13+
}
14+
15+
func init() {
16+
artifacts.InitDownload(requireAccessToken, func() cli.Service { return service })
17+
artifactsCmd.AddCommand(artifacts.DownloadCmd)
18+
}

cmd/rwx/artifacts/download.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package artifacts
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
8+
"github.com/rwx-cloud/cli/internal/cli"
9+
"github.com/rwx-cloud/cli/internal/errors"
10+
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var (
15+
downloadOutputDir string
16+
downloadOutputFile string
17+
downloadJSON bool
18+
downloadAutoExtract bool
19+
downloadOpen bool
20+
21+
DownloadCmd *cobra.Command
22+
)
23+
24+
func InitDownload(requireAccessToken func() error, getService func() cli.Service) {
25+
DownloadCmd = &cobra.Command{
26+
Args: cobra.ExactArgs(2),
27+
PreRunE: func(cmd *cobra.Command, args []string) error {
28+
return requireAccessToken()
29+
},
30+
RunE: func(cmd *cobra.Command, args []string) error {
31+
taskID := args[0]
32+
artifactKey := args[1]
33+
34+
outputDirSet := cmd.Flags().Changed("output-dir")
35+
outputFileSet := cmd.Flags().Changed("output-file")
36+
if outputDirSet && outputFileSet {
37+
return errors.New("output-dir and output-file cannot be used together")
38+
}
39+
40+
var absOutputDir string
41+
var absOutputFile string
42+
var err error
43+
44+
if downloadOutputFile != "" {
45+
absOutputFile, err = filepath.Abs(downloadOutputFile)
46+
if err != nil {
47+
return errors.Wrapf(err, "unable to resolve absolute path for %s", downloadOutputFile)
48+
}
49+
} else {
50+
outputDir := downloadOutputDir
51+
if !outputDirSet {
52+
outputDir, err = getDefaultDownloadsDir()
53+
if err != nil {
54+
return errors.Wrap(err, "unable to determine default downloads directory")
55+
}
56+
}
57+
absOutputDir, err = filepath.Abs(outputDir)
58+
if err != nil {
59+
return errors.Wrapf(err, "unable to resolve absolute path for %s", outputDir)
60+
}
61+
}
62+
63+
err = getService().DownloadArtifact(cli.DownloadArtifactConfig{
64+
TaskID: taskID,
65+
ArtifactKey: artifactKey,
66+
OutputDir: absOutputDir,
67+
OutputFile: absOutputFile,
68+
OutputDirExplicitlySet: outputDirSet,
69+
Json: downloadJSON,
70+
AutoExtract: downloadAutoExtract,
71+
Open: downloadOpen,
72+
})
73+
if err != nil {
74+
return err
75+
}
76+
77+
return nil
78+
},
79+
Short: "Download an artifact from a task",
80+
Use: "download <task-id> <artifact-key> [flags]",
81+
}
82+
83+
DownloadCmd.Flags().StringVar(&downloadOutputDir, "output-dir", "", "output directory for the downloaded artifact (defaults to Downloads folder)")
84+
DownloadCmd.Flags().StringVar(&downloadOutputFile, "output-file", "", "output file path for the downloaded artifact")
85+
DownloadCmd.MarkFlagsMutuallyExclusive("output-dir", "output-file")
86+
DownloadCmd.Flags().BoolVar(&downloadJSON, "json", false, "output file locations as JSON")
87+
DownloadCmd.Flags().BoolVar(&downloadAutoExtract, "auto-extract", false, "automatically extract directory tar archives")
88+
DownloadCmd.Flags().BoolVar(&downloadOpen, "open", false, "automatically open the downloaded file(s)")
89+
}
90+
91+
func getDefaultDownloadsDir() (string, error) {
92+
if runtime.GOOS == "linux" {
93+
if xdgDownload := os.Getenv("XDG_DOWNLOAD_DIR"); xdgDownload != "" {
94+
return xdgDownload, nil
95+
}
96+
}
97+
98+
homeDir, err := os.UserHomeDir()
99+
if err != nil {
100+
return "", errors.Wrap(err, "unable to determine user home directory")
101+
}
102+
103+
return filepath.Join(homeDir, "Downloads"), nil
104+
}

cmd/rwx/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func init() {
117117
rootCmd.SetCompletionCommandGroupID("setup")
118118

119119
// Add commands (GroupID is set in each command's definition)
120+
rootCmd.AddCommand(artifactsCmd)
120121
rootCmd.AddCommand(debugCmd)
121122
rootCmd.AddCommand(dispatchCmd)
122123
rootCmd.AddCommand(imageCmd)

internal/api/client.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,60 @@ func (c Client) DownloadLogs(request LogDownloadRequestResult, maxRetryDurationS
701701
}
702702
}
703703

704+
func (c Client) GetArtifactDownloadRequest(taskId, artifactKey string) (ArtifactDownloadRequestResult, error) {
705+
endpoint := fmt.Sprintf("/mint/api/tasks/%s/artifact_downloads/%s", url.PathEscape(taskId), url.PathEscape(artifactKey))
706+
result := ArtifactDownloadRequestResult{}
707+
708+
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
709+
if err != nil {
710+
return result, errors.Wrap(err, "unable to create new HTTP request")
711+
}
712+
req.Header.Set("Content-Type", "application/json")
713+
req.Header.Set("Accept", "application/json")
714+
715+
resp, err := c.RoundTrip(req)
716+
if err != nil {
717+
return result, errors.Wrap(err, "HTTP request failed")
718+
}
719+
defer resp.Body.Close()
720+
721+
if err = decodeResponseJSON(resp, &result); err != nil {
722+
return result, err
723+
}
724+
725+
return result, nil
726+
}
727+
728+
func (c Client) DownloadArtifact(request ArtifactDownloadRequestResult) ([]byte, error) {
729+
req, err := http.NewRequest(http.MethodGet, request.URL, nil)
730+
if err != nil {
731+
return nil, errors.Wrap(err, "unable to create new HTTP request")
732+
}
733+
req.Header.Set("Accept", "application/octet-stream")
734+
735+
// Use http.DefaultClient directly since the artifact will come from storage (S3, etc.)
736+
resp, err := http.DefaultClient.Do(req)
737+
if err != nil {
738+
return nil, errors.Wrap(err, "HTTP request failed")
739+
}
740+
defer resp.Body.Close()
741+
742+
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
743+
artifactBytes, err := io.ReadAll(resp.Body)
744+
if err != nil {
745+
return nil, errors.Wrap(err, "unable to read response body")
746+
}
747+
return artifactBytes, nil
748+
}
749+
750+
bodyBytes, _ := io.ReadAll(resp.Body)
751+
errMsg := extractErrorMessage(bytes.NewReader(bodyBytes))
752+
if errMsg == "" {
753+
errMsg = fmt.Sprintf("Unable to download artifact - %s", resp.Status)
754+
}
755+
return nil, errors.New(errMsg)
756+
}
757+
704758
func decodeResponseJSON(resp *http.Response, result any) error {
705759
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
706760
if result == nil {

0 commit comments

Comments
 (0)