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

     1  package transferfiles
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api"
     8  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/state"
     9  	cmdutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
    10  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    11  	"github.com/jfrog/jfrog-client-go/utils/io/content"
    12  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    13  	"github.com/jfrog/jfrog-client-go/utils/log"
    14  	"os"
    15  	"path"
    16  	"time"
    17  )
    18  
    19  // Max errors that will be written in a file
    20  var maxErrorsInFile = 50000
    21  
    22  // TransferErrorsMng manages multi threads writing errors.
    23  // We want to create a file which contains all upload error statuses for each repository and phase.
    24  // Those files will serve us in 2 cases:
    25  // 1. Whenever we re-run 'transfer-files' command, we want to attempt to upload failed files again.
    26  // 2. As part of the transfer process, we generate a csv file that contains all upload errors.
    27  // In case an error occurs when creating those upload errors files, we would like to stop the transfer right away.
    28  type TransferErrorsMng struct {
    29  	// All go routines will write errors to the same channel
    30  	errorsChannelMng *ErrorsChannelMng
    31  	// Current repository that is being transferred
    32  	repoKey string
    33  	// Transfer current phase
    34  	phaseId        int
    35  	phaseStartTime string
    36  	errorWriterMng errorWriterMng
    37  	// Update state when changes occur
    38  	stateManager *state.TransferStateManager
    39  	// Update progressBar when changes occur
    40  	progressBar *TransferProgressMng
    41  }
    42  
    43  type errorWriter struct {
    44  	writer     *content.ContentWriter
    45  	errorCount int
    46  	filePath   string
    47  }
    48  
    49  type errorWriterMng struct {
    50  	retryable errorWriter
    51  	skipped   errorWriter
    52  }
    53  
    54  // newTransferErrorsToFile creates a manager for the files transferring process.
    55  // localPath - Path to the dir which error files will be written to.
    56  // repoKey - the repo that is being transferred
    57  // phase - the phase number
    58  // errorsChannelMng - all go routines will write to the same channel
    59  func newTransferErrorsToFile(repoKey string, phaseId int, phaseStartTime string, errorsChannelMng *ErrorsChannelMng, progressBar *TransferProgressMng, stateManager *state.TransferStateManager) (*TransferErrorsMng, error) {
    60  	err := initTransferErrorsDir(repoKey)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	mng := TransferErrorsMng{errorsChannelMng: errorsChannelMng, repoKey: repoKey, phaseId: phaseId, phaseStartTime: phaseStartTime, progressBar: progressBar, stateManager: stateManager}
    65  	return &mng, nil
    66  }
    67  
    68  // Create transfer errors directory inside the JFrog CLI home directory.
    69  // Inside the errors' directory creates directory for retryable errors and skipped errors.
    70  // Return the root errors' directory path.
    71  func initTransferErrorsDir(repoKey string) error {
    72  	// Create errors directory
    73  	errorsDirPath, err := getJfrogTransferRepoErrorsDir(repoKey)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	err = makeDirIfDoesNotExists(errorsDirPath)
    78  	if err != nil {
    79  		return err
    80  	}
    81  	// Create retryable directory inside errors directory
    82  	retryable, err := getJfrogTransferRepoRetryableDir(repoKey)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	err = makeDirIfDoesNotExists(retryable)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	// Create skipped directory inside errors directory
    91  	skipped, err := getJfrogTransferRepoSkippedDir(repoKey)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	return makeDirIfDoesNotExists(skipped)
    96  }
    97  
    98  func makeDirIfDoesNotExists(path string) error {
    99  	exists, err := fileutils.IsDirExists(path, false)
   100  	if err != nil {
   101  		return err
   102  	}
   103  	if !exists {
   104  		err = os.Mkdir(path, 0777)
   105  	}
   106  	return err
   107  }
   108  
   109  func (mng *TransferErrorsMng) start() (err error) {
   110  	// Init content writers manager
   111  	writerMng := errorWriterMng{}
   112  	// Init the content writer which is responsible for writing 'retryable errors' into files.
   113  	// In the next run we would like to retry and upload those files again.
   114  	retryablePath, err := getJfrogTransferRepoRetryableDir(mng.repoKey)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	writerRetry, retryFilePath, err := mng.newUniqueContentWriter(retryablePath)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	defer func() {
   123  		err = errors.Join(err, mng.errorWriterMng.retryable.closeWriter())
   124  	}()
   125  	writerMng.retryable = errorWriter{writer: writerRetry, filePath: retryFilePath}
   126  	// Init the content writer which is responsible for writing 'skipped errors' into files.
   127  	// In the next run we won't retry and upload those files.
   128  	skippedPath, err := getJfrogTransferRepoSkippedDir(mng.repoKey)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	writerSkip, skipFilePath, err := mng.newUniqueContentWriter(skippedPath)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	defer func() {
   137  		err = errors.Join(err, mng.errorWriterMng.skipped.closeWriter())
   138  	}()
   139  	writerMng.skipped = errorWriter{writer: writerSkip, filePath: skipFilePath}
   140  	mng.errorWriterMng = writerMng
   141  
   142  	// Read errors from channel and write them to files.
   143  	for e := range mng.errorsChannelMng.channel {
   144  		err = mng.writeErrorContent(e)
   145  		if err != nil {
   146  			return
   147  		}
   148  	}
   149  	return
   150  }
   151  
   152  func (mng *TransferErrorsMng) newUniqueContentWriter(dirPath string) (*content.ContentWriter, string, error) {
   153  	writer, err := content.NewContentWriter("errors", true, false)
   154  	if err != nil {
   155  		return nil, "", err
   156  	}
   157  	errorsFilePath, err := getUniqueErrorOrDelayFilePath(dirPath, func() string {
   158  		return getErrorsFileNamePrefix(mng.repoKey, mng.phaseId, mng.phaseStartTime)
   159  	})
   160  	if err != nil {
   161  		return nil, "", err
   162  	}
   163  	return writer, errorsFilePath, nil
   164  }
   165  
   166  func getErrorsFileNamePrefix(repoKey string, phaseId int, phaseStartTime string) string {
   167  	return fmt.Sprintf("%s-%d-%s", repoKey, phaseId, phaseStartTime)
   168  }
   169  
   170  func (mng *TransferErrorsMng) writeErrorContent(e ExtendedFileUploadStatusResponse) error {
   171  	var err error
   172  	switch e.Status {
   173  	case api.SkippedLargeProps:
   174  		err = mng.writeSkippedErrorContent(e)
   175  	default:
   176  		err = mng.writeRetryableErrorContent(e)
   177  		if err == nil && mng.progressBar != nil {
   178  			// Increment the failures counter view by 1, following the addition
   179  			// of the file to errors file.
   180  			mng.progressBar.changeNumberOfFailuresBy(1)
   181  			err = mng.stateManager.ChangeTransferFailureCountBy(1, true)
   182  		}
   183  	}
   184  	return err
   185  }
   186  
   187  func (mng *TransferErrorsMng) writeSkippedErrorContent(e ExtendedFileUploadStatusResponse) error {
   188  	logWritingArtifact(e, mng.errorWriterMng.skipped.filePath)
   189  	mng.errorWriterMng.skipped.writer.Write(e)
   190  	mng.errorWriterMng.skipped.errorCount++
   191  	// If file contains maximum number of errors - create and write to a new errors file
   192  	if mng.errorWriterMng.skipped.errorCount == maxErrorsInFile {
   193  		err := mng.errorWriterMng.skipped.closeWriter()
   194  		if err != nil {
   195  			return err
   196  		}
   197  		// Initialize variables for new errors file
   198  		dirPath, err := getJfrogTransferRepoSkippedDir(mng.repoKey)
   199  		if err != nil {
   200  			return err
   201  		}
   202  		mng.errorWriterMng.skipped.writer, mng.errorWriterMng.skipped.filePath, err = mng.newUniqueContentWriter(dirPath)
   203  		if err != nil {
   204  			return err
   205  		}
   206  		mng.errorWriterMng.skipped.errorCount = 0
   207  	}
   208  	return nil
   209  }
   210  
   211  func (mng *TransferErrorsMng) writeRetryableErrorContent(e ExtendedFileUploadStatusResponse) error {
   212  	logWritingArtifact(e, mng.errorWriterMng.retryable.filePath)
   213  	mng.errorWriterMng.retryable.writer.Write(e)
   214  	mng.errorWriterMng.retryable.errorCount++
   215  	// If file contains maximum number of errors - create and write to a new errors file
   216  	if mng.errorWriterMng.retryable.errorCount == maxErrorsInFile {
   217  		err := mng.errorWriterMng.retryable.closeWriter()
   218  		if err != nil {
   219  			return err
   220  		}
   221  		// Initialize variables for new errors file
   222  		dirPath, err := getJfrogTransferRepoRetryableDir(mng.repoKey)
   223  		if err != nil {
   224  			return err
   225  		}
   226  		mng.errorWriterMng.retryable.writer, mng.errorWriterMng.retryable.filePath, err = mng.newUniqueContentWriter(dirPath)
   227  		if err != nil {
   228  			return err
   229  		}
   230  		mng.errorWriterMng.retryable.errorCount = 0
   231  	}
   232  	return nil
   233  }
   234  
   235  func logWritingArtifact(e ExtendedFileUploadStatusResponse, errorsFilePath string) {
   236  	if log.GetLogger().GetLogLevel() != log.DEBUG {
   237  		return
   238  	}
   239  	msg := fmt.Sprintf("Writing artifact '%s' to errors file '%s'.", path.Join(e.Repo, e.Path, e.Name), errorsFilePath)
   240  	if e.Reason != "" {
   241  		msg += fmt.Sprintf(" Reason: '%s'.", e.Reason)
   242  	}
   243  	log.Debug(msg)
   244  }
   245  
   246  func (writerMng *errorWriter) closeWriter() error {
   247  	// Close content writer and move output file to our working directory
   248  	if writerMng.writer == nil {
   249  		return nil
   250  	}
   251  	err := writerMng.writer.Close()
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	if writerMng.writer.GetFilePath() != "" {
   257  		log.Debug(fmt.Sprintf("Saving errors outpt to: %s.", writerMng.filePath))
   258  		err = fileutils.MoveFile(writerMng.writer.GetFilePath(), writerMng.filePath)
   259  		if err != nil {
   260  			err = fmt.Errorf("saving error file failed! failed moving %s to %s: %w", writerMng.writer.GetFilePath(), writerMng.filePath, err)
   261  		}
   262  	}
   263  	return err
   264  }
   265  
   266  // Creates the csv errors files - contains the retryable and skipped errors.
   267  // In case no errors were written returns empty string
   268  func createErrorsCsvSummary(sourceRepos []string, timeStarted time.Time) (string, error) {
   269  	errorsFiles, err := getErrorsFiles(sourceRepos, true)
   270  	if err != nil {
   271  		return "", err
   272  	}
   273  
   274  	skippedErrorsFiles, err := getErrorsFiles(sourceRepos, false)
   275  	if err != nil {
   276  		return "", err
   277  	}
   278  
   279  	errorsFiles = append(errorsFiles, skippedErrorsFiles...)
   280  	if len(errorsFiles) == 0 {
   281  		return "", nil
   282  	}
   283  	// Collect all errors from the given log files
   284  	allErrors, err := parseErrorsFromLogFiles(errorsFiles)
   285  	if err != nil {
   286  		return "", err
   287  	}
   288  	return cmdutils.CreateCSVFile("transfer-files-logs", allErrors.Errors, timeStarted)
   289  }
   290  
   291  // Gets a list of all errors files from the CLI's cache.
   292  // Errors-files contain files that were failed to upload or actions that were skipped because of known limitations.
   293  func getErrorsFiles(repoKeys []string, isRetry bool) (filesPaths []string, err error) {
   294  	if isRetry {
   295  		return getErrorOrDelayFiles(repoKeys, getJfrogTransferRepoRetryableDir)
   296  	}
   297  	return getErrorOrDelayFiles(repoKeys, getJfrogTransferRepoSkippedDir)
   298  }
   299  
   300  // Count the number of transfer failures of a given subset of repositories
   301  func getRetryErrorCount(repoKeys []string) (int, error) {
   302  	files, err := getErrorsFiles(repoKeys, true)
   303  	if err != nil {
   304  		return -1, err
   305  	}
   306  
   307  	count := 0
   308  	for _, file := range files {
   309  		failedFiles, err := readErrorFile(file)
   310  		if err != nil {
   311  			return -1, err
   312  		}
   313  		count += len(failedFiles.Errors)
   314  	}
   315  	return count, nil
   316  }
   317  
   318  // Reads an error file from a given path, parses and populate a given FilesErrors instance with the file information
   319  func readErrorFile(path string) (FilesErrors, error) {
   320  	// Stores the errors read from the errors file.
   321  	var failedFiles FilesErrors
   322  
   323  	fContent, err := os.ReadFile(path)
   324  	if err != nil {
   325  		return failedFiles, errorutils.CheckError(err)
   326  	}
   327  
   328  	err = json.Unmarshal(fContent, &failedFiles)
   329  	if err != nil {
   330  		return failedFiles, errorutils.CheckError(err)
   331  	}
   332  	return failedFiles, nil
   333  }
   334  
   335  // ErrorsChannelMng handles the uploading errors and adds them to a common channel.
   336  // Stops adding elements to the channel if an error occurs while handling the files.
   337  type ErrorsChannelMng struct {
   338  	channel chan ExtendedFileUploadStatusResponse
   339  	err     error
   340  }
   341  
   342  type FilesErrors struct {
   343  	Errors []ExtendedFileUploadStatusResponse `json:"errors,omitempty"`
   344  }
   345  
   346  type ExtendedFileUploadStatusResponse struct {
   347  	api.FileUploadStatusResponse
   348  	Time string `json:"time,omitempty"`
   349  }
   350  
   351  func (mng ErrorsChannelMng) add(element api.FileUploadStatusResponse) (stopped bool) {
   352  	if mng.shouldStop() {
   353  		return true
   354  	}
   355  	extendedElement := ExtendedFileUploadStatusResponse{FileUploadStatusResponse: element, Time: time.Now().Format(time.RFC3339)}
   356  	mng.channel <- extendedElement
   357  	return false
   358  }
   359  
   360  // Close channel
   361  func (mng ErrorsChannelMng) close() {
   362  	close(mng.channel)
   363  }
   364  
   365  func (mng ErrorsChannelMng) shouldStop() bool {
   366  	// Stop adding elements to the channel if a 'blocking' error occurred in a different go routine.
   367  	return mng.err != nil
   368  }
   369  
   370  func createErrorsChannelMng() ErrorsChannelMng {
   371  	errorChannel := make(chan ExtendedFileUploadStatusResponse, fileWritersChannelSize)
   372  	return ErrorsChannelMng{channel: errorChannel}
   373  }