github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/artifacts/download.go (about) 1 package artifacts 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/url" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 13 "github.com/ActiveState/cli/internal/analytics" 14 "github.com/ActiveState/cli/internal/config" 15 "github.com/ActiveState/cli/internal/errs" 16 "github.com/ActiveState/cli/internal/fileutils" 17 "github.com/ActiveState/cli/internal/httputil" 18 "github.com/ActiveState/cli/internal/locale" 19 "github.com/ActiveState/cli/internal/output" 20 "github.com/ActiveState/cli/pkg/buildplan" 21 "github.com/ActiveState/cli/pkg/platform/api/buildplanner/request" 22 "github.com/ActiveState/cli/pkg/platform/authentication" 23 "github.com/ActiveState/cli/pkg/platform/model" 24 rtProgress "github.com/ActiveState/cli/pkg/platform/runtime/setup/events/progress" 25 "github.com/ActiveState/cli/pkg/project" 26 ) 27 28 type DownloadParams struct { 29 BuildID string 30 OutputDir string 31 Namespace *project.Namespaced 32 CommitID string 33 Target string 34 } 35 36 type Download struct { 37 out output.Outputer 38 project *project.Project 39 analytics analytics.Dispatcher 40 svcModel *model.SvcModel 41 auth *authentication.Auth 42 config *config.Instance 43 } 44 45 func NewDownload(prime primeable) *Download { 46 return &Download{ 47 out: prime.Output(), 48 project: prime.Project(), 49 analytics: prime.Analytics(), 50 svcModel: prime.SvcModel(), 51 auth: prime.Auth(), 52 config: prime.Config(), 53 } 54 } 55 56 type errArtifactExists struct { 57 error 58 Path string 59 } 60 61 func rationalizeDownloadError(err *error, auth *authentication.Auth) { 62 var artifactExistsErr *errArtifactExists 63 64 switch { 65 case err == nil: 66 return 67 68 case errors.As(*err, &artifactExistsErr): 69 *err = errs.WrapUserFacing(*err, 70 locale.Tl("err_builds_download_artifact_exists", "The artifact '[ACTIONABLE]{{.V0}}[/RESET]' has already been downloaded", artifactExistsErr.Path), 71 errs.SetInput()) 72 73 default: 74 rationalizeCommonError(err, auth) 75 } 76 } 77 78 func (d *Download) Run(params *DownloadParams) (rerr error) { 79 defer rationalizeDownloadError(&rerr, d.auth) 80 81 if d.project != nil && !params.Namespace.IsValid() { 82 d.out.Notice(locale.Tr("operating_message", d.project.NamespaceString(), d.project.Dir())) 83 } 84 85 target := request.TargetAll 86 if params.Target != "" { 87 target = params.Target 88 } 89 90 bp, err := getBuildPlan( 91 d.project, params.Namespace, params.CommitID, target, d.auth, d.out) 92 if err != nil { 93 return errs.Wrap(err, "Could not get build plan map") 94 } 95 96 var artifact *buildplan.Artifact 97 for _, a := range bp.Artifacts() { 98 if strings.HasPrefix(strings.ToLower(string(a.ArtifactID)), strings.ToLower(params.BuildID)) { 99 artifact = a 100 break 101 } 102 } 103 104 if artifact == nil { 105 return locale.NewInputError("err_build_id_not_found", "Could not find artifact with ID: '[ACTIONABLE]{{.V0}}[/RESET]", params.BuildID) 106 } 107 108 targetDir := params.OutputDir 109 if targetDir == "" { 110 targetDir, err = os.Getwd() 111 if err != nil { 112 return errs.Wrap(err, "Could not get current working directory") 113 } 114 } 115 116 if err := d.downloadArtifact(artifact, targetDir); err != nil { 117 return errs.Wrap(err, "Could not download artifact %s", artifact.ArtifactID.String()) 118 } 119 120 return nil 121 } 122 123 func (d *Download) downloadArtifact(artifact *buildplan.Artifact, targetDir string) (rerr error) { 124 artifactURL, err := url.Parse(artifact.URL) 125 if err != nil { 126 return errs.Wrap(err, "Could not parse artifact URL %s.", artifact.URL) 127 } 128 129 // Determine an appropriate basename for the artifact. 130 // Most platform artifact URLs are just "artifact.tar.gz", so use "<name>-<version>.<ext>" format. 131 // Some URLs are more complex like "<name>-<hash>.<ext>", so just leave them alone. 132 basename := path.Base(artifactURL.Path) 133 if basename == "artifact.tar.gz" { 134 basename = fmt.Sprintf("%s-%s.tar.gz", artifact.Name(), artifact.Version()) 135 } 136 137 downloadPath := filepath.Join(targetDir, basename) 138 if fileutils.TargetExists(downloadPath) { 139 return &errArtifactExists{Path: downloadPath} 140 } 141 142 ctx, cancel := context.WithCancel(context.Background()) 143 pg := newDownloadProgress(ctx, d.out, artifact.Name(), targetDir) 144 defer cancel() 145 146 b, err := httputil.GetWithProgress(artifactURL.String(), &rtProgress.Report{ 147 ReportSizeCb: func(size int) error { 148 pg.Start(int64(size)) 149 return nil 150 }, 151 ReportIncrementCb: func(inc int) error { 152 pg.Inc(inc) 153 return nil 154 }, 155 }) 156 if err != nil { 157 // Abort and display the error message 158 pg.Abort() 159 return errs.Wrap(err, "Download %s failed", artifactURL.String()) 160 } 161 pg.Stop() 162 163 if err := fileutils.WriteFile(downloadPath, b); err != nil { 164 return errs.Wrap(err, "Writing download to target file %s failed", downloadPath) 165 } 166 167 d.out.Notice(locale.Tl("msg_download_success", "[SUCCESS]Downloaded {{.V0}} to {{.V1}}[/RESET]", artifact.Name(), downloadPath)) 168 169 return nil 170 }