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