github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferfiles/delayedartifactshandler.go (about)

     1  package transferfiles
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  
    11  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    13  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    14  	"github.com/jfrog/jfrog-client-go/utils/io/content"
    15  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    16  	"github.com/jfrog/jfrog-client-go/utils/log"
    17  )
    18  
    19  var maxDelayedArtifactsInFile = 50000
    20  
    21  // TransferDelayedArtifactsMng takes care of the multi-threaded-writing of artifacts to be transferred, while maintaining the correct order of the deployment.
    22  // This is needed because, for example, for maven repositories, pom file should be deployed last.
    23  type TransferDelayedArtifactsMng struct {
    24  	// All go routines will write delayedArtifacts to the same channel
    25  	delayedArtifactsChannelMng *DelayedArtifactsChannelMng
    26  	// The Information needed to determine the file names created by the writer
    27  	repoKey        string
    28  	phaseStartTime string
    29  	// Writes delayed artifacts from channel to files
    30  	delayedWriter *SplitContentWriter
    31  }
    32  
    33  // Create transfer delays directory inside the JFrog CLI home directory.
    34  func initTransferDelaysDir(repoKey string) error {
    35  	// Create transfer directory (if it doesn't exist)
    36  	transferDir, err := coreutils.GetJfrogTransferDir()
    37  	if err != nil {
    38  		return err
    39  	}
    40  	if err = fileutils.CreateDirIfNotExist(transferDir); err != nil {
    41  		return err
    42  	}
    43  	// Create delays directory
    44  	delaysDirPath, err := getJfrogTransferRepoDelaysDir(repoKey)
    45  	if err != nil {
    46  		return err
    47  	}
    48  	return fileutils.CreateDirIfNotExist(delaysDirPath)
    49  }
    50  
    51  // Creates a manager for the process of transferring delayed files. Delayed files are files that should be transferred at the very end of the transfer process, such as pom.xml and manifest.json files.
    52  func newTransferDelayedArtifactsManager(delayedArtifactsChannelMng *DelayedArtifactsChannelMng, repoKey string, phaseStartTime string) (*TransferDelayedArtifactsMng, error) {
    53  	if err := initTransferDelaysDir(repoKey); err != nil {
    54  		return nil, err
    55  	}
    56  	return &TransferDelayedArtifactsMng{delayedArtifactsChannelMng: delayedArtifactsChannelMng, repoKey: repoKey, phaseStartTime: phaseStartTime}, nil
    57  }
    58  
    59  func getDelaysFilePrefix(repoKey string, phaseStartTime string) string {
    60  	return fmt.Sprintf("%s-%s", repoKey, phaseStartTime)
    61  }
    62  
    63  func (mng *TransferDelayedArtifactsMng) start() (err error) {
    64  	defer func() {
    65  		if mng.delayedWriter != nil {
    66  			err = errors.Join(err, errorutils.CheckError(mng.delayedWriter.close()))
    67  		}
    68  	}()
    69  
    70  	delaysDirPath, err := getJfrogTransferRepoDelaysDir(mng.repoKey)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	mng.delayedWriter = newSplitContentWriter("delayed_artifacts", maxDelayedArtifactsInFile, delaysDirPath, getDelaysFilePrefix(mng.repoKey, mng.phaseStartTime))
    76  
    77  	for file := range mng.delayedArtifactsChannelMng.channel {
    78  		log.Debug(fmt.Sprintf("Delaying the upload of file '%s'. Writing it to be uploaded later...", path.Join(file.Repo, file.Path, file.Name)))
    79  		if err = mng.delayedWriter.writeRecord(file); err != nil {
    80  			return err
    81  		}
    82  	}
    83  	return nil
    84  }
    85  
    86  type DelayedArtifactsFile struct {
    87  	DelayedArtifacts []api.FileRepresentation `json:"delayed_artifacts,omitempty"`
    88  }
    89  
    90  // Collect all the delayed artifact files that were created up to this point for the repository and transfer their artifacts using handleDelayedArtifactsFiles
    91  func consumeAllDelayFiles(base phaseBase) (err error) {
    92  	filesToConsume, err := getDelayFiles([]string{base.repoKey})
    93  	if err != nil || len(filesToConsume) == 0 {
    94  		return
    95  	}
    96  	delayFunctions := getDelayUploadComparisonFunctions(base.repoSummary.PackageType)
    97  	if len(delayFunctions) == 0 {
    98  		return
    99  	}
   100  
   101  	log.Info("Starting to handle delayed artifacts uploads...")
   102  	// Each delay function causes the transfer to skip a specific group of files.
   103  	// Within the handleDelayedArtifactsFiles function, we recursively remove the first delay function from the slice to transfer the first set of files every time.
   104  	if err = handleDelayedArtifactsFiles(filesToConsume, base, delayFunctions[1:]); err != nil {
   105  		return
   106  	}
   107  
   108  	log.Info("Done handling delayed artifacts uploads.")
   109  	return deleteAllFiles(filesToConsume)
   110  }
   111  
   112  // Call consumeAllDelayFiles only if there are no failed transferred files for the repository up to this point.
   113  // In case failed files exists, we reduce the count of files for the given phase by the amount of delayed artifacts.
   114  func consumeDelayFilesIfNoErrors(phase phaseBase, addedDelayFiles []string) error {
   115  	errCount, err := getRetryErrorCount([]string{phase.repoKey})
   116  	if err != nil {
   117  		return err
   118  	}
   119  	// No errors - we can handle all the delayed files created up to this point.
   120  	if errCount == 0 {
   121  		return consumeAllDelayFiles(phase)
   122  	}
   123  	// There were files which we failed to transferred, and therefore we had error files.
   124  	// Therefore, the delayed files should be handled later, as part of Phase 3. We also reduce the count of files of this phase by the amount of files which were delayed.
   125  	if len(addedDelayFiles) > 0 && phase.progressBar != nil {
   126  		phaseTaskProgressBar := phase.progressBar.phases[phase.phaseId].GetTasksProgressBar()
   127  		oldTotal := phaseTaskProgressBar.GetTotal()
   128  		delayCount, _, err := countDelayFilesContent(addedDelayFiles)
   129  		if err != nil {
   130  			return err
   131  		}
   132  		phaseTaskProgressBar.SetGeneralProgressTotal(oldTotal - int64(delayCount))
   133  	}
   134  	return nil
   135  }
   136  
   137  func countDelayFilesContent(filePaths []string) (count int, storage int64, err error) {
   138  	for _, file := range filePaths {
   139  		delayFile, err := readDelayFile(file)
   140  		if err != nil {
   141  			return 0, storage, err
   142  		}
   143  		count += len(delayFile.DelayedArtifacts)
   144  		for _, delay := range delayFile.DelayedArtifacts {
   145  			storage += delay.Size
   146  		}
   147  	}
   148  	return
   149  }
   150  
   151  func handleDelayedArtifactsFiles(filesToConsume []string, base phaseBase, delayUploadComparisonFunctions []shouldDelayUpload) error {
   152  	manager := newTransferManager(base, delayUploadComparisonFunctions)
   153  	action := func(pcWrapper *producerConsumerWrapper, uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng) error {
   154  		if ShouldStop(&base, &delayHelper, errorsChannelMng) {
   155  			return nil
   156  		}
   157  		return consumeDelayedArtifactsFiles(pcWrapper, filesToConsume, uploadChunkChan, base, delayHelper, errorsChannelMng)
   158  	}
   159  	delayAction := func(pBase phaseBase, addedDelayFiles []string) error {
   160  		// We call this method as a recursion in order to have inner order base on the comparison function list.
   161  		// Remove the first delay comparison function one by one to no longer delay it until the list is empty.
   162  		if len(addedDelayFiles) > 0 && len(delayUploadComparisonFunctions) > 0 {
   163  			return handleDelayedArtifactsFiles(addedDelayFiles, pBase, delayUploadComparisonFunctions[1:])
   164  		}
   165  		return nil
   166  	}
   167  	return manager.doTransferWithProducerConsumer(action, delayAction)
   168  }
   169  
   170  func consumeDelayedArtifactsFiles(pcWrapper *producerConsumerWrapper, filesToConsume []string, uploadChunkChan chan UploadedChunk, base phaseBase, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng) error {
   171  	log.Debug(fmt.Sprintf("Starting to handle delayed artifacts files. Found %d files.", len(filesToConsume)))
   172  	for _, filePath := range filesToConsume {
   173  		log.Debug("Handling delayed artifacts file: '" + filePath + "'")
   174  		delayedArtifactsFile, err := readDelayFile(filePath)
   175  		if err != nil {
   176  			return err
   177  		}
   178  
   179  		shouldStop, err := uploadByChunks(delayedArtifactsFile.DelayedArtifacts, uploadChunkChan, base, delayHelper, errorsChannelMng, pcWrapper)
   180  		if err != nil || shouldStop {
   181  			return err
   182  		}
   183  
   184  		if base.progressBar != nil {
   185  			base.progressBar.changeNumberOfDelayedFiles(-1 * len(delayedArtifactsFile.DelayedArtifacts))
   186  		}
   187  		if err = base.stateManager.ChangeDelayedFilesCountBy(uint64(len(delayedArtifactsFile.DelayedArtifacts)), false); err != nil {
   188  			log.Warn("Couldn't decrease the delayed files counter", err.Error())
   189  		}
   190  	}
   191  	return nil
   192  }
   193  
   194  // Reads a delay file from a given path, parses and populate a given DelayedArtifactsFile instance with the file information
   195  func readDelayFile(path string) (DelayedArtifactsFile, error) {
   196  	// Stores the errors read from the errors file.
   197  	var delayedArtifactsFile DelayedArtifactsFile
   198  
   199  	fContent, err := os.ReadFile(path)
   200  	if err != nil {
   201  		return delayedArtifactsFile, errorutils.CheckError(err)
   202  	}
   203  
   204  	err = json.Unmarshal(fContent, &delayedArtifactsFile)
   205  	return delayedArtifactsFile, errorutils.CheckError(err)
   206  }
   207  
   208  // Gets a list of all delay files from the CLI's cache for a specific repo
   209  func getDelayFiles(repoKeys []string) (filesPaths []string, err error) {
   210  	return getErrorOrDelayFiles(repoKeys, getJfrogTransferRepoDelaysDir)
   211  }
   212  
   213  func getDelayedFilesCount(repoKeys []string) (int, error) {
   214  	files, err := getDelayFiles(repoKeys)
   215  	if err != nil {
   216  		return -1, err
   217  	}
   218  
   219  	count := 0
   220  	for _, file := range files {
   221  		delayedFiles, err := readDelayFile(file)
   222  		if err != nil {
   223  			return -1, err
   224  		}
   225  		count += len(delayedFiles.DelayedArtifacts)
   226  	}
   227  	return count, nil
   228  }
   229  
   230  const (
   231  	maven  = "Maven"
   232  	gradle = "Gradle"
   233  	ivy    = "Ivy"
   234  	docker = "Docker"
   235  	conan  = "Conan"
   236  	nuget  = "NuGet"
   237  	sbt    = "SBT"
   238  )
   239  
   240  // A function to determine whether the file deployment should be delayed.
   241  type shouldDelayUpload func(string) bool
   242  
   243  // Returns an array of functions to control the order of deployment.
   244  func getDelayUploadComparisonFunctions(packageType string) []shouldDelayUpload {
   245  	switch packageType {
   246  	case maven, gradle, ivy:
   247  		return []shouldDelayUpload{func(fileName string) bool {
   248  			return filepath.Ext(fileName) == ".pom" || fileName == "pom.xml"
   249  		}}
   250  	case docker:
   251  		return []shouldDelayUpload{func(fileName string) bool {
   252  			return fileName == "manifest.json"
   253  		}, func(fileName string) bool {
   254  			return fileName == "list.manifest.json"
   255  		}}
   256  	case conan:
   257  		return []shouldDelayUpload{func(fileName string) bool {
   258  			return fileName == "conanfile.py"
   259  		}, func(fileName string) bool {
   260  			return fileName == "conaninfo.txt"
   261  		}, func(fileName string) bool {
   262  			return fileName == ".timestamp"
   263  		}}
   264  	}
   265  	return []shouldDelayUpload{}
   266  }
   267  
   268  type delayUploadHelper struct {
   269  	shouldDelayFunctions       []shouldDelayUpload
   270  	delayedArtifactsChannelMng *DelayedArtifactsChannelMng
   271  }
   272  
   273  // Decide whether to delay the deployment of a file by running over the shouldDelayUpload array.
   274  // When there are multiple levels of requirements in the deployment order, the first comparison function in the array can be removed each time in order to no longer delay by that rule.
   275  func (delayHelper delayUploadHelper) delayUploadIfNecessary(phase phaseBase, file api.FileRepresentation) (delayed, stopped bool) {
   276  	for _, shouldDelay := range delayHelper.shouldDelayFunctions {
   277  		if ShouldStop(&phase, &delayHelper, nil) {
   278  			return delayed, true
   279  		}
   280  		if shouldDelay(file.Name) {
   281  			delayed = true
   282  			delayHelper.delayedArtifactsChannelMng.add(file)
   283  			if phase.progressBar != nil {
   284  				phase.progressBar.changeNumberOfDelayedFiles(1)
   285  			}
   286  			if err := phase.stateManager.ChangeDelayedFilesCountBy(1, true); err != nil {
   287  				log.Warn("Couldn't increase the delayed files counter", err.Error())
   288  			}
   289  		}
   290  	}
   291  	return
   292  }
   293  
   294  // DelayedArtifactsChannelMng is used when writing 'delayed artifacts' to a common channel.
   295  // If an error occurs while handling the files - this message is used to stop adding elements to the channel.
   296  type DelayedArtifactsChannelMng struct {
   297  	channel chan api.FileRepresentation
   298  	err     error
   299  }
   300  
   301  func (mng DelayedArtifactsChannelMng) add(element api.FileRepresentation) {
   302  	mng.channel <- element
   303  }
   304  
   305  func (mng DelayedArtifactsChannelMng) shouldStop() bool {
   306  	// Stop adding elements to the channel if a 'blocking' error occurred in a different go routine.
   307  	return mng.err != nil
   308  }
   309  
   310  // Close channel
   311  func (mng DelayedArtifactsChannelMng) close() {
   312  	close(mng.channel)
   313  }
   314  
   315  func createdDelayedArtifactsChannelMng() DelayedArtifactsChannelMng {
   316  	channel := make(chan api.FileRepresentation, fileWritersChannelSize)
   317  	return DelayedArtifactsChannelMng{channel: channel}
   318  }
   319  
   320  // SplitContentWriter writes to files a single JSON object that holds a list of records added as stream.
   321  // It can limit the amount of records per file and splits the content to several files if needed.
   322  type SplitContentWriter struct {
   323  	writer *content.ContentWriter
   324  	// JSON array key of the object
   325  	arrayKey string
   326  	// Limit for the amount of records allowed per file
   327  	maxRecordsAllowed int
   328  	// The path for the directory that will hold the files of the content
   329  	dirPath string
   330  	// The name for the files that will be generated (a counter will added as a suffix to the files by this writer)
   331  	filePrefix string
   332  	// Counter for the amount of records at the current file
   333  	recordCount int
   334  	// Counter for amount if files generated for the content
   335  	fileIndex int
   336  	// List of all the paths of the files that were generated for the content
   337  	contentFiles []string
   338  }
   339  
   340  func newSplitContentWriter(key string, maxRecordsPerFile int, directoryPath string, prefix string) *SplitContentWriter {
   341  	return &SplitContentWriter{arrayKey: key, maxRecordsAllowed: maxRecordsPerFile, dirPath: directoryPath, filePrefix: prefix, contentFiles: []string{}}
   342  }
   343  
   344  // Create new file if needed, writes a record and closes a file if it reached its maxRecord
   345  func (w *SplitContentWriter) writeRecord(record interface{}) error {
   346  	// Init the content writer, which is responsible for writing to the current file
   347  	if w.writer == nil {
   348  		writer, err := content.NewContentWriter(w.arrayKey, true, false)
   349  		if err != nil {
   350  			return err
   351  		}
   352  		w.writer = writer
   353  	}
   354  	// Write
   355  	w.writer.Write(record)
   356  	w.recordCount++
   357  	// If file contains maximum number of records - reset for next write
   358  	if w.recordCount == w.maxRecordsAllowed {
   359  		return w.closeCurrentFile()
   360  	}
   361  	return nil
   362  }
   363  
   364  func (w *SplitContentWriter) closeCurrentFile() error {
   365  	// Close current file
   366  	if w.writer != nil {
   367  		if err := w.writer.Close(); err != nil {
   368  			return err
   369  		}
   370  		defer func() {
   371  			// Reset writer and counter.
   372  			w.recordCount = 0
   373  			w.writer = nil
   374  		}()
   375  		if w.writer.GetFilePath() != "" {
   376  			fullPath, err := getUniqueErrorOrDelayFilePath(w.dirPath, func() string {
   377  				return w.filePrefix
   378  			})
   379  			if err != nil {
   380  				return err
   381  			}
   382  			log.Debug(fmt.Sprintf("Saving split content JSON file to: %s.", fullPath))
   383  			if err := fileutils.MoveFile(w.writer.GetFilePath(), fullPath); err != nil {
   384  				return fmt.Errorf("saving file failed! failed moving %s to %s: %w", w.writer.GetFilePath(), fullPath, err)
   385  			}
   386  			w.contentFiles = append(w.contentFiles, fullPath)
   387  			w.fileIndex++
   388  		}
   389  	}
   390  	return nil
   391  }
   392  
   393  func (w *SplitContentWriter) close() error {
   394  	return w.closeCurrentFile()
   395  }