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 }