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 }