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  }