github.com/osievert/jfrog-cli-core@v1.2.7/artifactory/commands/generic/download.go (about)

     1  package generic
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"path/filepath"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/jfrog/jfrog-cli-core/artifactory/spec"
    11  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
    12  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
    13  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    14  	clientutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    15  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    16  	ioUtils "github.com/jfrog/jfrog-client-go/utils/io"
    17  	"github.com/jfrog/jfrog-client-go/utils/io/content"
    18  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    19  	"github.com/jfrog/jfrog-client-go/utils/log"
    20  )
    21  
    22  type DownloadCommand struct {
    23  	buildConfiguration *utils.BuildConfiguration
    24  	GenericCommand
    25  	configuration *utils.DownloadConfiguration
    26  	progress      ioUtils.ProgressMgr
    27  }
    28  
    29  func NewDownloadCommand() *DownloadCommand {
    30  	return &DownloadCommand{GenericCommand: *NewGenericCommand()}
    31  }
    32  
    33  func (dc *DownloadCommand) SetBuildConfiguration(buildConfiguration *utils.BuildConfiguration) *DownloadCommand {
    34  	dc.buildConfiguration = buildConfiguration
    35  	return dc
    36  }
    37  
    38  func (dc *DownloadCommand) Configuration() *utils.DownloadConfiguration {
    39  	return dc.configuration
    40  }
    41  
    42  func (dc *DownloadCommand) SetConfiguration(configuration *utils.DownloadConfiguration) *DownloadCommand {
    43  	dc.configuration = configuration
    44  	return dc
    45  }
    46  
    47  func (dc *DownloadCommand) SetProgress(progress ioUtils.ProgressMgr) {
    48  	dc.progress = progress
    49  }
    50  
    51  func (dc *DownloadCommand) ShouldPrompt() bool {
    52  	return !dc.DryRun() && dc.SyncDeletesPath() != "" && !dc.Quiet()
    53  }
    54  
    55  func (dc *DownloadCommand) CommandName() string {
    56  	return "rt_download"
    57  }
    58  
    59  func (dc *DownloadCommand) Run() error {
    60  	return dc.download()
    61  }
    62  
    63  func (dc *DownloadCommand) download() error {
    64  	// Create Service Manager:
    65  	servicesManager, err := utils.CreateDownloadServiceManager(dc.rtDetails, dc.configuration.Threads, dc.DryRun(), dc.progress)
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	// Build Info Collection:
    71  	isCollectBuildInfo := len(dc.buildConfiguration.BuildName) > 0 && len(dc.buildConfiguration.BuildNumber) > 0
    72  	if isCollectBuildInfo && !dc.DryRun() {
    73  		if err = utils.SaveBuildGeneralDetails(dc.buildConfiguration.BuildName, dc.buildConfiguration.BuildNumber); err != nil {
    74  			return err
    75  		}
    76  	}
    77  
    78  	var errorOccurred = false
    79  	var downloadParamsArray []services.DownloadParams
    80  	// Create DownloadParams for all File-Spec groups.
    81  	for i := 0; i < len(dc.Spec().Files); i++ {
    82  		downParams, err := getDownloadParams(dc.Spec().Get(i), dc.configuration)
    83  		if err != nil {
    84  			errorOccurred = true
    85  			log.Error(err)
    86  			continue
    87  		}
    88  		downloadParamsArray = append(downloadParamsArray, downParams)
    89  	}
    90  	// Perform download.
    91  	// In case of build-info collection/sync-deletes operation/a detailed summary is required, we use the download service which provides results file reader,
    92  	// otherwise we use the download service which provides only general counters.
    93  	var totalDownloaded, totalExpected int
    94  	var resultsReader *content.ContentReader = nil
    95  	if isCollectBuildInfo || dc.SyncDeletesPath() != "" || dc.DetailedSummary() {
    96  		resultsReader, totalDownloaded, totalExpected, err = servicesManager.DownloadFilesWithResultReader(downloadParamsArray...)
    97  		dc.result.SetReader(resultsReader)
    98  	} else {
    99  		totalDownloaded, totalExpected, err = servicesManager.DownloadFiles(downloadParamsArray...)
   100  	}
   101  	if err != nil {
   102  		errorOccurred = true
   103  		log.Error(err)
   104  	}
   105  	dc.result.SetSuccessCount(totalDownloaded)
   106  	dc.result.SetFailCount(totalExpected - totalDownloaded)
   107  	// If the 'details summary' was requested, then the reader should not be closed now.
   108  	// It will be closed after it will be used to generate the summary.
   109  	if resultsReader != nil && !dc.DetailedSummary() {
   110  		defer func() {
   111  			resultsReader.Close()
   112  			dc.result.SetReader(nil)
   113  		}()
   114  	}
   115  	// Check for errors.
   116  	if errorOccurred {
   117  		return errors.New("Download finished with errors, please review the logs.")
   118  	}
   119  	if dc.DryRun() {
   120  		dc.result.SetSuccessCount(totalExpected)
   121  		dc.result.SetFailCount(0)
   122  		return err
   123  	} else if dc.SyncDeletesPath() != "" {
   124  		absSyncDeletesPath, err := filepath.Abs(dc.SyncDeletesPath())
   125  		if err != nil {
   126  			return errorutils.CheckError(err)
   127  		}
   128  		if _, err = os.Stat(absSyncDeletesPath); err == nil {
   129  			// Unmarshal the local paths of the downloaded files from the results file reader
   130  			tmpRoot, err := createDownloadResultEmptyTmpReflection(resultsReader)
   131  			defer fileutils.RemoveTempDir(tmpRoot)
   132  			if err != nil {
   133  				return err
   134  			}
   135  			walkFn := createSyncDeletesWalkFunction(tmpRoot)
   136  			err = fileutils.Walk(dc.SyncDeletesPath(), walkFn, false)
   137  			if err != nil {
   138  				return errorutils.CheckError(err)
   139  			}
   140  		} else if os.IsNotExist(err) {
   141  			log.Info("Sync-deletes path", absSyncDeletesPath, "does not exists.")
   142  		}
   143  	}
   144  	log.Debug("Downloaded", strconv.Itoa(totalDownloaded), "artifacts.")
   145  
   146  	// Build Info
   147  	if isCollectBuildInfo {
   148  		err, resultBuildInfo := utils.ReadResultBuildInfo(resultsReader)
   149  		if err != nil {
   150  			return err
   151  		}
   152  		buildDependencies := convertFileInfoToBuildDependencies(resultBuildInfo.FilesInfo)
   153  		populateFunc := func(partial *buildinfo.Partial) {
   154  			partial.Dependencies = buildDependencies
   155  			partial.ModuleId = dc.buildConfiguration.Module
   156  			partial.ModuleType = buildinfo.Generic
   157  		}
   158  		err = utils.SavePartialBuildInfo(dc.buildConfiguration.BuildName, dc.buildConfiguration.BuildNumber, populateFunc)
   159  	}
   160  
   161  	return err
   162  }
   163  
   164  func convertFileInfoToBuildDependencies(filesInfo []clientutils.FileInfo) []buildinfo.Dependency {
   165  	buildDependencies := make([]buildinfo.Dependency, len(filesInfo))
   166  	for i, fileInfo := range filesInfo {
   167  		dependency := buildinfo.Dependency{Checksum: &buildinfo.Checksum{}}
   168  		dependency.Md5 = fileInfo.Md5
   169  		dependency.Sha1 = fileInfo.Sha1
   170  		// Artifact name in build info as the name in artifactory
   171  		filename, _ := fileutils.GetFileAndDirFromPath(fileInfo.ArtifactoryPath)
   172  		dependency.Id = filename
   173  		buildDependencies[i] = dependency
   174  	}
   175  	return buildDependencies
   176  }
   177  
   178  func getDownloadParams(f *spec.File, configuration *utils.DownloadConfiguration) (downParams services.DownloadParams, err error) {
   179  	downParams = services.NewDownloadParams()
   180  	downParams.ArtifactoryCommonParams = f.ToArtifactoryCommonParams()
   181  	downParams.Symlink = configuration.Symlink
   182  	downParams.MinSplitSize = configuration.MinSplitSize
   183  	downParams.SplitCount = configuration.SplitCount
   184  	downParams.Retries = configuration.Retries
   185  
   186  	downParams.Recursive, err = f.IsRecursive(true)
   187  	if err != nil {
   188  		return
   189  	}
   190  
   191  	downParams.IncludeDirs, err = f.IsIncludeDirs(false)
   192  	if err != nil {
   193  		return
   194  	}
   195  
   196  	downParams.Flat, err = f.IsFlat(false)
   197  	if err != nil {
   198  		return
   199  	}
   200  
   201  	downParams.Explode, err = f.IsExplode(false)
   202  	if err != nil {
   203  		return
   204  	}
   205  
   206  	downParams.ValidateSymlink, err = f.IsVlidateSymlinks(false)
   207  	if err != nil {
   208  		return
   209  	}
   210  
   211  	downParams.ExcludeArtifacts, err = f.IsExcludeArtifacts(false)
   212  	if err != nil {
   213  		return
   214  	}
   215  
   216  	downParams.IncludeDeps, err = f.IsIncludeDeps(false)
   217  	if err != nil {
   218  		return
   219  	}
   220  
   221  	return
   222  }
   223  
   224  // We will create the same downloaded hierarchies under a temp directory with 0-size files.
   225  // We will use this "empty reflection" of the download operation to determine whether a file was downloaded or not while walking the real filesystem from sync-deletes root.
   226  func createDownloadResultEmptyTmpReflection(reader *content.ContentReader) (tmpRoot string, err error) {
   227  	tmpRoot, err = fileutils.CreateTempDir()
   228  	if errorutils.CheckError(err) != nil {
   229  		return
   230  	}
   231  	for path := new(localPath); reader.NextRecord(path) == nil; path = new(localPath) {
   232  		var absDownloadPath string
   233  		absDownloadPath, err = filepath.Abs(path.LocalPath)
   234  		if errorutils.CheckError(err) != nil {
   235  			return
   236  		}
   237  		legalPath := createLegalPath(tmpRoot, absDownloadPath)
   238  		tmpFileRoot := filepath.Dir(legalPath)
   239  		err = os.MkdirAll(tmpFileRoot, os.ModePerm)
   240  		if errorutils.CheckError(err) != nil {
   241  			return
   242  		}
   243  		var tmpFile *os.File
   244  		tmpFile, err = os.Create(legalPath)
   245  		if errorutils.CheckError(err) != nil {
   246  			return
   247  		}
   248  		err = tmpFile.Close()
   249  		if errorutils.CheckError(err) != nil {
   250  			return
   251  		}
   252  	}
   253  	return
   254  }
   255  
   256  // Creates absolute path for temp file suitable for all environments
   257  func createLegalPath(root, path string) string {
   258  	// Avoid concatenating the volume name (e.g "C://") in Windows environment.
   259  	volumeName := filepath.VolumeName(path)
   260  	if volumeName != "" && strings.HasPrefix(path, volumeName) {
   261  		alternativeVolumeName := "VolumeName" + string(volumeName[0])
   262  		path = strings.Replace(path, volumeName, alternativeVolumeName, 1)
   263  	}
   264  	// Join the current path to the temp root provided.
   265  	path = filepath.Join(root, path)
   266  	return path
   267  }
   268  
   269  func createSyncDeletesWalkFunction(tempRoot string) fileutils.WalkFunc {
   270  	return func(path string, info os.FileInfo, err error) error {
   271  		// Convert path to absolute path
   272  		path, err = filepath.Abs(path)
   273  		if errorutils.CheckError(err) != nil {
   274  			return err
   275  		}
   276  		pathToCheck := createLegalPath(tempRoot, path)
   277  
   278  		// If the path exists under the temp root directory, it means it's been downloaded during the last operations, and cannot be deleted.
   279  		if fileutils.IsPathExists(pathToCheck, false) {
   280  			return nil
   281  		}
   282  		log.Info("Deleting:", path)
   283  		if info.IsDir() {
   284  			// If current path is a dir - remove all content and return SkipDir to stop walking this path
   285  			err = os.RemoveAll(path)
   286  			if err == nil {
   287  				return fileutils.SkipDir
   288  			}
   289  		} else {
   290  			// Path is a file
   291  			err = os.Remove(path)
   292  		}
   293  
   294  		return errorutils.CheckError(err)
   295  	}
   296  }
   297  
   298  type localPath struct {
   299  	LocalPath string `json:"localPath,omitempty"`
   300  }