github.com/cobalt77/jfrog-client-go@v0.14.5/artifactory/services/download.go (about)

     1  package services
     2  
     3  import (
     4  	"errors"
     5  	"net/http"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"sort"
    10  
    11  	rthttpclient "github.com/cobalt77/jfrog-client-go/artifactory/httpclient"
    12  	"github.com/cobalt77/jfrog-client-go/artifactory/services/utils"
    13  	"github.com/cobalt77/jfrog-client-go/auth"
    14  	"github.com/cobalt77/jfrog-client-go/httpclient"
    15  	clientutils "github.com/cobalt77/jfrog-client-go/utils"
    16  	"github.com/cobalt77/jfrog-client-go/utils/errorutils"
    17  	clientio "github.com/cobalt77/jfrog-client-go/utils/io"
    18  	"github.com/cobalt77/jfrog-client-go/utils/io/content"
    19  	"github.com/cobalt77/jfrog-client-go/utils/io/fileutils"
    20  	"github.com/cobalt77/jfrog-client-go/utils/io/fileutils/checksum"
    21  	"github.com/cobalt77/jfrog-client-go/utils/log"
    22  	"github.com/jfrog/gofrog/parallel"
    23  	"github.com/mholt/archiver/v3"
    24  )
    25  
    26  type DownloadService struct {
    27  	client       *rthttpclient.ArtifactoryHttpClient
    28  	Progress     clientio.Progress
    29  	ArtDetails   auth.ServiceDetails
    30  	DryRun       bool
    31  	Threads      int
    32  	ResultWriter *content.ContentWriter
    33  }
    34  
    35  func NewDownloadService(client *rthttpclient.ArtifactoryHttpClient) *DownloadService {
    36  	return &DownloadService{client: client}
    37  }
    38  
    39  func (ds *DownloadService) GetArtifactoryDetails() auth.ServiceDetails {
    40  	return ds.ArtDetails
    41  }
    42  
    43  func (ds *DownloadService) SetArtifactoryDetails(rt auth.ServiceDetails) {
    44  	ds.ArtDetails = rt
    45  }
    46  
    47  func (ds *DownloadService) IsDryRun() bool {
    48  	return ds.DryRun
    49  }
    50  
    51  func (ds *DownloadService) GetJfrogHttpClient() (*rthttpclient.ArtifactoryHttpClient, error) {
    52  	return ds.client, nil
    53  }
    54  
    55  func (ds *DownloadService) GetThreads() int {
    56  	return ds.Threads
    57  }
    58  
    59  func (ds *DownloadService) SetThreads(threads int) {
    60  	ds.Threads = threads
    61  }
    62  
    63  func (ds *DownloadService) SetServiceDetails(artDetails auth.ServiceDetails) {
    64  	ds.ArtDetails = artDetails
    65  }
    66  
    67  func (ds *DownloadService) SetDryRun(isDryRun bool) {
    68  	ds.DryRun = isDryRun
    69  }
    70  
    71  func (ds *DownloadService) DownloadFiles(downloadParams ...DownloadParams) (int, int, error) {
    72  	producerConsumer := parallel.NewBounedRunner(ds.GetThreads(), false)
    73  	errorsQueue := clientutils.NewErrorsQueue(1)
    74  	expectedChan := make(chan int, 1)
    75  	successCounters := make([]int, ds.GetThreads())
    76  	ds.prepareTasks(producerConsumer, expectedChan, successCounters, errorsQueue, downloadParams...)
    77  
    78  	err := ds.performTasks(producerConsumer, errorsQueue)
    79  	totalSuccess := 0
    80  	for _, v := range successCounters {
    81  		totalSuccess += v
    82  	}
    83  	return totalSuccess, <-expectedChan, err
    84  }
    85  
    86  func (ds *DownloadService) prepareTasks(producer parallel.Runner, expectedChan chan int, successCounters []int, errorsQueue *clientutils.ErrorsQueue, downloadParamsSlice ...DownloadParams) {
    87  	go func() {
    88  		defer producer.Done()
    89  		defer close(expectedChan)
    90  		totalTasks := 0
    91  		// Iterate over file-spec groups and produce download tasks.
    92  		// When encountering an error, log and move to next group.
    93  		for _, downloadParams := range downloadParamsSlice {
    94  			var err error
    95  			var reader *content.ContentReader
    96  			// Create handler function for the current group.
    97  			fileHandlerFunc := ds.createFileHandlerFunc(downloadParams, successCounters)
    98  			// Search items.
    99  			log.Info("Searching items to download...")
   100  			switch downloadParams.GetSpecType() {
   101  			case utils.WILDCARD:
   102  				reader, err = ds.collectFilesUsingWildcardPattern(downloadParams)
   103  			case utils.BUILD:
   104  				reader, err = utils.SearchBySpecWithBuild(downloadParams.GetFile(), ds)
   105  			case utils.AQL:
   106  				reader, err = utils.SearchBySpecWithAql(downloadParams.GetFile(), ds, utils.SYMLINK)
   107  			}
   108  			// Check for search errors.
   109  			if err != nil {
   110  				log.Error(err)
   111  				errorsQueue.AddError(err)
   112  				continue
   113  			}
   114  			// Produce download tasks for the download consumers.
   115  			totalTasks += produceTasks(reader, downloadParams, producer, fileHandlerFunc, errorsQueue)
   116  			reader.Close()
   117  		}
   118  		expectedChan <- totalTasks
   119  	}()
   120  }
   121  
   122  func (ds *DownloadService) collectFilesUsingWildcardPattern(downloadParams DownloadParams) (*content.ContentReader, error) {
   123  	return utils.SearchBySpecWithPattern(downloadParams.GetFile(), ds, utils.SYMLINK)
   124  }
   125  
   126  func produceTasks(reader *content.ContentReader, downloadParams DownloadParams, producer parallel.Runner, fileHandler fileHandlerFunc, errorsQueue *clientutils.ErrorsQueue) int {
   127  	flat := downloadParams.IsFlat()
   128  	// Collect all folders path which might be needed to create.
   129  	// key = folder path, value = the necessary data for producing create folder task.
   130  	directoriesData := make(map[string]DownloadData)
   131  	// Store all the paths which was created implicitly due to file upload.
   132  	alreadyCreatedDirs := make(map[string]bool)
   133  	// Store all the keys of directoriesData as an array.
   134  	var directoriesDataKeys []string
   135  	// Task counter
   136  	var tasksCount int
   137  	for resultItem := new(utils.ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(utils.ResultItem) {
   138  		tempData := DownloadData{
   139  			Dependency:   *resultItem,
   140  			DownloadPath: downloadParams.GetPattern(),
   141  			Target:       downloadParams.GetTarget(),
   142  			Flat:         flat,
   143  		}
   144  		if resultItem.Type != "folder" {
   145  			// Add a task. A task is a function of type TaskFunc which later on will be executed by other go routine, the communication is done using channels.
   146  			// The second argument is an error handling func in case the taskFunc return an error.
   147  			tasksCount++
   148  			producer.AddTaskWithError(fileHandler(tempData), errorsQueue.AddError)
   149  			// We don't want to create directories which are created explicitly by download files when ArtifactoryCommonParams.IncludeDirs is used.
   150  			alreadyCreatedDirs[resultItem.Path] = true
   151  		} else {
   152  			directoriesData, directoriesDataKeys = collectDirPathsToCreate(*resultItem, directoriesData, tempData, directoriesDataKeys)
   153  		}
   154  	}
   155  	if err := reader.GetError(); err != nil {
   156  		errorsQueue.AddError(errorutils.CheckError(err))
   157  		return tasksCount
   158  	}
   159  	reader.Reset()
   160  	addCreateDirsTasks(directoriesDataKeys, alreadyCreatedDirs, producer, fileHandler, directoriesData, errorsQueue, flat)
   161  	return tasksCount
   162  }
   163  
   164  // Extract for the aqlResultItem the directory path, store the path the directoriesDataKeys and in the directoriesData map.
   165  // In addition directoriesData holds the correlate DownloadData for each key, later on this DownloadData will be used to create a create dir tasks if needed.
   166  // This function append the new data to directoriesDataKeys and to directoriesData and return the new map and the new []string
   167  // We are storing all the keys of directoriesData in additional array(directoriesDataKeys) so we could sort the keys and access the maps in the sorted order.
   168  func collectDirPathsToCreate(aqlResultItem utils.ResultItem, directoriesData map[string]DownloadData, tempData DownloadData, directoriesDataKeys []string) (map[string]DownloadData, []string) {
   169  	key := aqlResultItem.Name
   170  	if aqlResultItem.Path != "." {
   171  		key = path.Join(aqlResultItem.Path, aqlResultItem.Name)
   172  	}
   173  	directoriesData[key] = tempData
   174  	directoriesDataKeys = append(directoriesDataKeys, key)
   175  	return directoriesData, directoriesDataKeys
   176  }
   177  
   178  func addCreateDirsTasks(directoriesDataKeys []string, alreadyCreatedDirs map[string]bool, producer parallel.Runner, fileHandler fileHandlerFunc, directoriesData map[string]DownloadData, errorsQueue *clientutils.ErrorsQueue, isFlat bool) {
   179  	// Longest path first
   180  	// We are going to create the longest path first by doing so all sub paths of the longest path will be created implicitly.
   181  	sort.Sort(sort.Reverse(sort.StringSlice(directoriesDataKeys)))
   182  	for index, v := range directoriesDataKeys {
   183  		// In order to avoid duplication we need to check the path wasn't already created by the previous action.
   184  		if v != "." && // For some files the returned path can be the root path, ".", in that case we doing need to create any directory.
   185  			(index == 0 || !utils.IsSubPath(directoriesDataKeys, index, "/")) { // directoriesDataKeys store all the path which might needed to be created, that's include duplicated paths.
   186  			// By sorting the directoriesDataKeys we can assure that the longest path was created and therefore no need to create all it's sub paths.
   187  
   188  			// Some directories were created due to file download when we aren't in flat download flow.
   189  			if isFlat {
   190  				producer.AddTaskWithError(fileHandler(directoriesData[v]), errorsQueue.AddError)
   191  			} else if !alreadyCreatedDirs[v] {
   192  				producer.AddTaskWithError(fileHandler(directoriesData[v]), errorsQueue.AddError)
   193  			}
   194  		}
   195  	}
   196  	return
   197  }
   198  
   199  func (ds *DownloadService) performTasks(consumer parallel.Runner, errorsQueue *clientutils.ErrorsQueue) error {
   200  	// Blocked until finish consuming
   201  	consumer.Run()
   202  	if ds.ResultWriter != nil {
   203  		err := ds.ResultWriter.Close()
   204  		if err != nil {
   205  			return err
   206  		}
   207  	}
   208  	return errorsQueue.GetError()
   209  }
   210  
   211  func createDependencyFileInfo(resultItem utils.ResultItem, localPath, localFileName string) utils.FileInfo {
   212  	fileInfo := utils.FileInfo{
   213  		ArtifactoryPath: resultItem.GetItemRelativePath(),
   214  		FileHashes: &utils.FileHashes{
   215  			Sha1: resultItem.Actual_Sha1,
   216  			Md5:  resultItem.Actual_Md5,
   217  		},
   218  	}
   219  	fileInfo.LocalPath = filepath.Join(localPath, localFileName)
   220  	return fileInfo
   221  }
   222  
   223  func createDownloadFileDetails(downloadPath, localPath, localFileName string, downloadData DownloadData) (details *httpclient.DownloadFileDetails) {
   224  	details = &httpclient.DownloadFileDetails{
   225  		FileName:      downloadData.Dependency.Name,
   226  		DownloadPath:  downloadPath,
   227  		RelativePath:  downloadData.Dependency.GetItemRelativePath(),
   228  		LocalPath:     localPath,
   229  		LocalFileName: localFileName,
   230  		Size:          downloadData.Dependency.Size,
   231  		ExpectedSha1:  downloadData.Dependency.Actual_Sha1}
   232  	return
   233  }
   234  
   235  func (ds *DownloadService) downloadFile(downloadFileDetails *httpclient.DownloadFileDetails, logMsgPrefix string, downloadParams DownloadParams) error {
   236  	httpClientsDetails := ds.ArtDetails.CreateHttpClientDetails()
   237  	bulkDownload := downloadParams.SplitCount == 0 || downloadParams.MinSplitSize < 0 || downloadParams.MinSplitSize*1000 > downloadFileDetails.Size
   238  	if !bulkDownload {
   239  		acceptRange, err := ds.isFileAcceptRange(downloadFileDetails)
   240  		if err != nil {
   241  			return err
   242  		}
   243  		bulkDownload = !acceptRange
   244  	}
   245  	if bulkDownload {
   246  		var resp *http.Response
   247  		resp, err := ds.client.DownloadFileWithProgress(downloadFileDetails, logMsgPrefix, &httpClientsDetails,
   248  			downloadParams.GetRetries(), downloadParams.IsExplode(), ds.Progress)
   249  		if err != nil {
   250  			return err
   251  		}
   252  		log.Debug(logMsgPrefix, "Artifactory response:", resp.Status)
   253  		return errorutils.CheckResponseStatus(resp, http.StatusOK)
   254  	}
   255  
   256  	concurrentDownloadFlags := httpclient.ConcurrentDownloadFlags{
   257  		FileName:      downloadFileDetails.FileName,
   258  		DownloadPath:  downloadFileDetails.DownloadPath,
   259  		RelativePath:  downloadFileDetails.RelativePath,
   260  		LocalFileName: downloadFileDetails.LocalFileName,
   261  		LocalPath:     downloadFileDetails.LocalPath,
   262  		ExpectedSha1:  downloadFileDetails.ExpectedSha1,
   263  		FileSize:      downloadFileDetails.Size,
   264  		SplitCount:    downloadParams.SplitCount,
   265  		Explode:       downloadParams.IsExplode(),
   266  		Retries:       downloadParams.GetRetries()}
   267  
   268  	resp, err := ds.client.DownloadFileConcurrently(concurrentDownloadFlags, logMsgPrefix, &httpClientsDetails, ds.Progress)
   269  	if err != nil {
   270  		return err
   271  	}
   272  	return errorutils.CheckResponseStatus(resp, http.StatusPartialContent)
   273  }
   274  
   275  func (ds *DownloadService) isFileAcceptRange(downloadFileDetails *httpclient.DownloadFileDetails) (bool, error) {
   276  	httpClientsDetails := ds.ArtDetails.CreateHttpClientDetails()
   277  	isAcceptRange, resp, err := ds.client.IsAcceptRanges(downloadFileDetails.DownloadPath, &httpClientsDetails)
   278  	if err != nil {
   279  		return false, err
   280  	}
   281  	err = errorutils.CheckResponseStatus(resp, http.StatusOK)
   282  	if err != nil {
   283  		return false, err
   284  	}
   285  	return isAcceptRange, err
   286  }
   287  
   288  func removeIfSymlink(localSymlinkPath string) error {
   289  	if fileutils.IsPathSymlink(localSymlinkPath) {
   290  		if err := os.Remove(localSymlinkPath); errorutils.CheckError(err) != nil {
   291  			return err
   292  		}
   293  	}
   294  	return nil
   295  }
   296  
   297  func createLocalSymlink(localPath, localFileName, symlinkArtifact string, symlinkChecksum bool, symlinkContentChecksum string, logMsgPrefix string) error {
   298  	if symlinkChecksum && symlinkContentChecksum != "" {
   299  		if !fileutils.IsPathExists(symlinkArtifact, false) {
   300  			return errorutils.CheckError(errors.New("Symlink validation failed, target doesn't exist: " + symlinkArtifact))
   301  		}
   302  		file, err := os.Open(symlinkArtifact)
   303  		if err = errorutils.CheckError(err); err != nil {
   304  			return err
   305  		}
   306  		defer file.Close()
   307  		checksumInfo, err := checksum.Calc(file, checksum.SHA1)
   308  		if err != nil {
   309  			return err
   310  		}
   311  		sha1 := checksumInfo[checksum.SHA1]
   312  		if sha1 != symlinkContentChecksum {
   313  			return errorutils.CheckError(errors.New("Symlink validation failed for target: " + symlinkArtifact))
   314  		}
   315  	}
   316  	localSymlinkPath := filepath.Join(localPath, localFileName)
   317  	isFileExists, err := fileutils.IsFileExists(localSymlinkPath, false)
   318  	if err != nil {
   319  		return err
   320  	}
   321  	// We can't create symlink in case a file with the same name already exist, we must remove the file before creating the symlink
   322  	if isFileExists {
   323  		if err := os.Remove(localSymlinkPath); err != nil {
   324  			return err
   325  		}
   326  	}
   327  	// Need to prepare the directories hierarchy
   328  	_, err = fileutils.CreateFilePath(localPath, localFileName)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	err = os.Symlink(symlinkArtifact, localSymlinkPath)
   333  	if errorutils.CheckError(err) != nil {
   334  		return err
   335  	}
   336  	log.Debug(logMsgPrefix, "Creating symlink file.")
   337  	return nil
   338  }
   339  
   340  func getArtifactPropertyByKey(properties []utils.Property, key string) string {
   341  	for _, v := range properties {
   342  		if v.Key == key {
   343  			return v.Value
   344  		}
   345  	}
   346  	return ""
   347  }
   348  
   349  func getArtifactSymlinkPath(properties []utils.Property) string {
   350  	return getArtifactPropertyByKey(properties, utils.ARTIFACTORY_SYMLINK)
   351  }
   352  
   353  func getArtifactSymlinkChecksum(properties []utils.Property) string {
   354  	return getArtifactPropertyByKey(properties, utils.SYMLINK_SHA1)
   355  }
   356  
   357  type fileHandlerFunc func(DownloadData) parallel.TaskFunc
   358  
   359  func (ds *DownloadService) createFileHandlerFunc(downloadParams DownloadParams, successCounters []int) fileHandlerFunc {
   360  	return func(downloadData DownloadData) parallel.TaskFunc {
   361  		return func(threadId int) error {
   362  			logMsgPrefix := clientutils.GetLogMsgPrefix(threadId, ds.DryRun)
   363  			downloadPath, e := utils.BuildArtifactoryUrl(ds.ArtDetails.GetUrl(), downloadData.Dependency.GetItemRelativePath(), make(map[string]string))
   364  			if e != nil {
   365  				return e
   366  			}
   367  			log.Info(logMsgPrefix+"Downloading", downloadData.Dependency.GetItemRelativePath())
   368  			if ds.DryRun {
   369  				return nil
   370  			}
   371  			target, e := clientutils.BuildTargetPath(downloadData.DownloadPath, downloadData.Dependency.GetItemRelativePath(), downloadData.Target, true)
   372  			if e != nil {
   373  				return e
   374  			}
   375  			localPath, localFileName := fileutils.GetLocalPathAndFile(downloadData.Dependency.Name, downloadData.Dependency.Path, target, downloadData.Flat)
   376  			if downloadData.Dependency.Type == "folder" {
   377  				return createDir(localPath, localFileName, logMsgPrefix)
   378  			}
   379  			e = removeIfSymlink(filepath.Join(localPath, localFileName))
   380  			if e != nil {
   381  				return e
   382  			}
   383  			if downloadParams.IsSymlink() {
   384  				if isSymlink, e := createSymlinkIfNeeded(localPath, localFileName, logMsgPrefix, downloadData, successCounters, ds.ResultWriter, threadId, downloadParams); isSymlink {
   385  					return e
   386  				}
   387  			}
   388  			dependency := createDependencyFileInfo(downloadData.Dependency, localPath, localFileName)
   389  			e = ds.downloadFileIfNeeded(downloadPath, localPath, localFileName, logMsgPrefix, downloadData, downloadParams)
   390  			if e != nil {
   391  				log.Error(logMsgPrefix, "Received an error: "+e.Error())
   392  				return e
   393  			}
   394  			successCounters[threadId]++
   395  			if ds.ResultWriter != nil {
   396  				ds.ResultWriter.Write(dependency)
   397  			}
   398  			return nil
   399  		}
   400  	}
   401  }
   402  
   403  func (ds *DownloadService) downloadFileIfNeeded(downloadPath, localPath, localFileName, logMsgPrefix string, downloadData DownloadData, downloadParams DownloadParams) error {
   404  	isEqual, e := fileutils.IsEqualToLocalFile(filepath.Join(localPath, localFileName), downloadData.Dependency.Actual_Md5, downloadData.Dependency.Actual_Sha1)
   405  	if e != nil {
   406  		return e
   407  	}
   408  	if isEqual {
   409  		log.Debug(logMsgPrefix, "File already exists locally.")
   410  		if downloadParams.IsExplode() {
   411  			e = explodeLocalFile(localPath, localFileName)
   412  		}
   413  		return e
   414  	}
   415  	downloadFileDetails := createDownloadFileDetails(downloadPath, localPath, localFileName, downloadData)
   416  	return ds.downloadFile(downloadFileDetails, logMsgPrefix, downloadParams)
   417  }
   418  
   419  func explodeLocalFile(localPath, localFileName string) (err error) {
   420  	log.Info("Extracting archive:", localFileName, "to", localPath)
   421  	arch, err := archiver.ByExtension(localFileName)
   422  	absolutePath := filepath.Join(localPath, localFileName)
   423  
   424  	// The file is indeed an archive
   425  	if err == nil {
   426  		err := arch.(archiver.Unarchiver).Unarchive(absolutePath, localPath)
   427  		if err != nil {
   428  			return errorutils.CheckError(err)
   429  		}
   430  		// If the file was extracted successfully, remove it from the file system
   431  		err = os.Remove(absolutePath)
   432  	}
   433  
   434  	return errorutils.CheckError(err)
   435  }
   436  
   437  func createDir(localPath, localFileName, logMsgPrefix string) error {
   438  	folderPath := filepath.Join(localPath, localFileName)
   439  	e := fileutils.CreateDirIfNotExist(folderPath)
   440  	if e != nil {
   441  		return e
   442  	}
   443  	log.Info(logMsgPrefix + "Creating folder: " + folderPath)
   444  	return nil
   445  }
   446  
   447  func createSymlinkIfNeeded(localPath, localFileName, logMsgPrefix string, downloadData DownloadData, successCounters []int, responseWriter *content.ContentWriter, threadId int, downloadParams DownloadParams) (bool, error) {
   448  	symlinkArtifact := getArtifactSymlinkPath(downloadData.Dependency.Properties)
   449  	isSymlink := len(symlinkArtifact) > 0
   450  	if isSymlink {
   451  		symlinkChecksum := getArtifactSymlinkChecksum(downloadData.Dependency.Properties)
   452  		if e := createLocalSymlink(localPath, localFileName, symlinkArtifact, downloadParams.ValidateSymlinks(), symlinkChecksum, logMsgPrefix); e != nil {
   453  			return isSymlink, e
   454  		}
   455  		dependency := createDependencyFileInfo(downloadData.Dependency, localPath, localFileName)
   456  		successCounters[threadId]++
   457  		if responseWriter != nil {
   458  			responseWriter.Write(dependency)
   459  		}
   460  		return isSymlink, nil
   461  	}
   462  	return isSymlink, nil
   463  }
   464  
   465  type DownloadData struct {
   466  	Dependency   utils.ResultItem
   467  	DownloadPath string
   468  	Target       string
   469  	Flat         bool
   470  }
   471  
   472  type DownloadParams struct {
   473  	*utils.ArtifactoryCommonParams
   474  	Symlink         bool
   475  	ValidateSymlink bool
   476  	Flat            bool
   477  	Explode         bool
   478  	MinSplitSize    int64
   479  	SplitCount      int
   480  	Retries         int
   481  }
   482  
   483  func (ds *DownloadParams) IsFlat() bool {
   484  	return ds.Flat
   485  }
   486  
   487  func (ds *DownloadParams) IsExplode() bool {
   488  	return ds.Explode
   489  }
   490  
   491  func (ds *DownloadParams) GetFile() *utils.ArtifactoryCommonParams {
   492  	return ds.ArtifactoryCommonParams
   493  }
   494  
   495  func (ds *DownloadParams) IsSymlink() bool {
   496  	return ds.Symlink
   497  }
   498  
   499  func (ds *DownloadParams) ValidateSymlinks() bool {
   500  	return ds.ValidateSymlink
   501  }
   502  
   503  func (ds *DownloadParams) GetRetries() int {
   504  	return ds.Retries
   505  }
   506  
   507  func NewDownloadParams() DownloadParams {
   508  	return DownloadParams{ArtifactoryCommonParams: &utils.ArtifactoryCommonParams{}, MinSplitSize: 5120, SplitCount: 3, Retries: 3}
   509  }