github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferfiles/fulltransfer.go (about) 1 package transferfiles 2 3 import ( 4 "fmt" 5 "path" 6 "time" 7 8 "github.com/jfrog/gofrog/parallel" 9 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api" 10 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/state" 11 "github.com/jfrog/jfrog-cli-core/v2/utils/reposnapshot" 12 servicesUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 13 clientUtils "github.com/jfrog/jfrog-client-go/utils" 14 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 15 "github.com/jfrog/jfrog-client-go/utils/log" 16 ) 17 18 // Manages the phase of performing a full transfer of the repository. 19 // This phase is only executed once per repository if its completed. 20 // Transfer is performed by treating every folder as a task, and searching for it's content in a flat AQL. 21 // New folders found are handled as a separate task, and files are uploaded in chunks and polled on for status. 22 type fullTransferPhase struct { 23 phaseBase 24 transferManager *transferManager 25 } 26 27 func (m *fullTransferPhase) initProgressBar() error { 28 if m.progressBar == nil { 29 return nil 30 } 31 skip, err := m.shouldSkipPhase() 32 if err != nil { 33 return err 34 } 35 m.progressBar.AddPhase1(skip) 36 return nil 37 } 38 39 func (m *fullTransferPhase) getPhaseName() string { 40 return "Full Transfer Phase" 41 } 42 43 func (m *fullTransferPhase) phaseStarted() error { 44 m.startTime = time.Now() 45 return m.stateManager.SetRepoFullTransferStarted(m.startTime) 46 } 47 48 func (m *fullTransferPhase) phaseDone() error { 49 // If the phase stopped gracefully, don't mark the phase as completed or delete snapshots. 50 if !m.ShouldStop() { 51 if err := m.handleSuccessfulTransfer(); err != nil { 52 return err 53 } 54 } 55 56 if m.progressBar != nil { 57 return m.progressBar.DonePhase(m.phaseId) 58 } 59 return nil 60 } 61 62 // Marks the phase as completed and deletes snapshots. 63 // Should ONLY be called if phase ended SUCCESSFULLY (not interrupted / stopped). 64 func (m *fullTransferPhase) handleSuccessfulTransfer() error { 65 if err := m.stateManager.SetRepoFullTransferCompleted(); err != nil { 66 return err 67 } 68 // Disable repo transfer snapshot since it is not tracked by the following phases we are not handling a full transfer. 69 m.stateManager.DisableRepoTransferSnapshot() 70 snapshotDir, err := state.GetJfrogTransferRepoSnapshotDir(m.repoKey) 71 if err != nil { 72 return err 73 } 74 return fileutils.RemoveDirContents(snapshotDir) 75 } 76 77 func (m *fullTransferPhase) shouldSkipPhase() (bool, error) { 78 repoTransferred, err := m.stateManager.IsRepoTransferred() 79 if err != nil || !repoTransferred { 80 return false, err 81 } 82 m.skipPhase() 83 return true, nil 84 } 85 86 func (m *fullTransferPhase) skipPhase() { 87 // Init progress bar as "done" with 0 tasks. 88 if m.progressBar != nil { 89 m.progressBar.AddPhase1(true) 90 } 91 } 92 93 func (m *fullTransferPhase) run() error { 94 m.transferManager = newTransferManager(m.phaseBase, getDelayUploadComparisonFunctions(m.repoSummary.PackageType)) 95 action := func(pcWrapper *producerConsumerWrapper, uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng) error { 96 if ShouldStop(&m.phaseBase, &delayHelper, errorsChannelMng) { 97 return nil 98 } 99 100 // Get the directory's node from the snapshot manager, and use information from previous transfer attempts if such exists. 101 node, done, err := m.getAndHandleDirectoryNode(".") 102 if err != nil || done { 103 return err 104 } 105 106 folderHandler := m.createFolderFullTransferHandlerFunc(node, pcWrapper, uploadChunkChan, delayHelper, errorsChannelMng) 107 _, err = pcWrapper.chunkBuilderProducerConsumer.AddTaskWithError(folderHandler(folderParams{relativePath: "."}), pcWrapper.errorsQueue.AddError) 108 return err 109 } 110 delayAction := func(phase phaseBase, addedDelayFiles []string) error { 111 // Disable repo transfer snapshot as it is not used for delayed files. 112 if err := m.stateManager.SaveStateAndSnapshots(); err != nil { 113 return err 114 } 115 m.stateManager.DisableRepoTransferSnapshot() 116 return consumeDelayFilesIfNoErrors(phase, addedDelayFiles) 117 } 118 return m.transferManager.doTransferWithProducerConsumer(action, delayAction) 119 } 120 121 type folderFullTransferHandlerFunc func(params folderParams) parallel.TaskFunc 122 123 type folderParams struct { 124 relativePath string 125 } 126 127 func (m *fullTransferPhase) createFolderFullTransferHandlerFunc(node *reposnapshot.Node, pcWrapper *producerConsumerWrapper, uploadChunkChan chan UploadedChunk, 128 delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng) folderFullTransferHandlerFunc { 129 return func(params folderParams) parallel.TaskFunc { 130 return func(threadId int) error { 131 logMsgPrefix := clientUtils.GetLogMsgPrefix(threadId, false) 132 return m.transferFolder(node, params, logMsgPrefix, pcWrapper, uploadChunkChan, delayHelper, errorsChannelMng) 133 } 134 } 135 } 136 137 func (m *fullTransferPhase) transferFolder(node *reposnapshot.Node, params folderParams, logMsgPrefix string, pcWrapper *producerConsumerWrapper, 138 uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng) (err error) { 139 log.Debug(logMsgPrefix+"Handling folder:", path.Join(m.repoKey, params.relativePath)) 140 141 // Increment progress number of folders 142 if m.progressBar != nil { 143 m.progressBar.incNumberOfVisitedFolders() 144 } 145 if err = m.stateManager.IncVisitedFolders(); err != nil { 146 return 147 } 148 149 curUploadChunk, err := m.searchAndHandleFolderContents(params, pcWrapper, 150 uploadChunkChan, delayHelper, errorsChannelMng, node) 151 if err != nil { 152 return 153 } 154 155 // Mark that no more results are expected for the current folder. 156 if err = node.MarkDoneExploring(); err != nil { 157 return err 158 } 159 160 // Chunk didn't reach full size. Upload the remaining files. 161 if len(curUploadChunk.UploadCandidates) > 0 { 162 if _, err = pcWrapper.chunkUploaderProducerConsumer.AddTaskWithError(uploadChunkWhenPossibleHandler(pcWrapper, &m.phaseBase, curUploadChunk, uploadChunkChan, errorsChannelMng), pcWrapper.errorsQueue.AddError); err != nil { 163 return 164 } 165 } 166 log.Debug(logMsgPrefix+"Done transferring folder:", path.Join(m.repoKey, params.relativePath)) 167 return 168 } 169 170 func (m *fullTransferPhase) searchAndHandleFolderContents(params folderParams, pcWrapper *producerConsumerWrapper, 171 uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng, 172 node *reposnapshot.Node) (curUploadChunk api.UploadChunk, err error) { 173 curUploadChunk = api.UploadChunk{ 174 TargetAuth: createTargetAuth(m.targetRtDetails, m.proxyKey), 175 CheckExistenceInFilestore: m.checkExistenceInFilestore, 176 // Skip file filtering in the Data Transfer plugin if it is already enabled in the JFrog CLI. 177 // The local generated filter is enabled in the JFrog CLI for target Artifactory servers >= 7.55. 178 SkipFileFiltering: m.locallyGeneratedFilter.IsEnabled(), 179 MinCheckSumDeploySize: m.minCheckSumDeploySize, 180 } 181 182 var result []servicesUtils.ResultItem 183 var lastPage bool 184 paginationI := 0 185 for !lastPage && !ShouldStop(&m.phaseBase, &delayHelper, errorsChannelMng) { 186 result, lastPage, err = m.getDirectoryContentAql(params.relativePath, paginationI) 187 if err != nil { 188 return 189 } 190 191 // Add the folder as a candidate to transfer. The reason is that we'd like to transfer only folders with properties or empty folders. 192 if params.relativePath != "." { 193 curUploadChunk.AppendUploadCandidateIfNeeded(api.FileRepresentation{Repo: m.repoKey, Path: params.relativePath, NonEmptyDir: len(result) > 0}, m.buildInfoRepo) 194 } 195 196 // Empty folder 197 if paginationI == 0 && len(result) == 0 { 198 return 199 } 200 201 for _, item := range result { 202 if ShouldStop(&m.phaseBase, &delayHelper, errorsChannelMng) { 203 return 204 } 205 if item.Name == "." { 206 continue 207 } 208 switch item.Type { 209 case "folder": 210 err = m.handleFoundChildFolder(params, pcWrapper, 211 uploadChunkChan, delayHelper, errorsChannelMng, item) 212 case "file": 213 err = m.handleFoundFile(pcWrapper, 214 uploadChunkChan, delayHelper, errorsChannelMng, 215 node, item, &curUploadChunk) 216 } 217 if err != nil { 218 return 219 } 220 } 221 222 paginationI++ 223 } 224 return 225 } 226 227 func (m *fullTransferPhase) handleFoundChildFolder(params folderParams, pcWrapper *producerConsumerWrapper, 228 uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng, 229 item servicesUtils.ResultItem) (err error) { 230 newRelativePath := getFolderRelativePath(item.Name, params.relativePath) 231 232 // Get the directory's node from the snapshot manager, and use information from previous transfer attempts if such exists. 233 node, done, err := m.getAndHandleDirectoryNode(newRelativePath) 234 if err != nil || done { 235 return err 236 } 237 238 folderHandler := m.createFolderFullTransferHandlerFunc(node, pcWrapper, uploadChunkChan, delayHelper, errorsChannelMng) 239 _, err = pcWrapper.chunkBuilderProducerConsumer.AddTaskWithError(folderHandler(folderParams{relativePath: newRelativePath}), pcWrapper.errorsQueue.AddError) 240 return 241 } 242 243 func (m *fullTransferPhase) handleFoundFile(pcWrapper *producerConsumerWrapper, 244 uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng, 245 node *reposnapshot.Node, item servicesUtils.ResultItem, curUploadChunk *api.UploadChunk) (err error) { 246 file := api.FileRepresentation{Repo: item.Repo, Path: item.Path, Name: item.Name, Size: item.Size} 247 delayed, stopped := delayHelper.delayUploadIfNecessary(m.phaseBase, file) 248 if delayed || stopped { 249 // If delayed, do not increment files count to allow tree collapsing during this phase. 250 return 251 } 252 // Increment the files count in the directory's node in the snapshot manager, to track its progress. 253 err = node.IncrementFilesCount(uint64(file.Size)) 254 if err != nil { 255 return 256 } 257 curUploadChunk.AppendUploadCandidateIfNeeded(file, m.buildInfoRepo) 258 if curUploadChunk.IsChunkFull() { 259 _, err = pcWrapper.chunkUploaderProducerConsumer.AddTaskWithError(uploadChunkWhenPossibleHandler(pcWrapper, &m.phaseBase, *curUploadChunk, uploadChunkChan, errorsChannelMng), pcWrapper.errorsQueue.AddError) 260 if err != nil { 261 return 262 } 263 // Empty the uploaded chunk. 264 curUploadChunk.UploadCandidates = []api.FileRepresentation{} 265 } 266 return 267 } 268 269 func getFolderRelativePath(folderName, relativeLocation string) string { 270 if relativeLocation == "." { 271 return folderName 272 } 273 return path.Join(relativeLocation, folderName) 274 } 275 276 func (m *fullTransferPhase) getDirectoryContentAql(relativePath string, paginationOffset int) (result []servicesUtils.ResultItem, lastPage bool, err error) { 277 query := generateFolderContentAqlQuery(m.repoKey, relativePath, paginationOffset, m.disabledDistinctiveAql) 278 aqlResults, err := runAql(m.context, m.srcRtDetails, query) 279 if err != nil { 280 return []servicesUtils.ResultItem{}, false, err 281 } 282 283 lastPage = len(aqlResults.Results) < AqlPaginationLimit 284 result, err = m.locallyGeneratedFilter.FilterLocallyGenerated(aqlResults.Results) 285 return 286 } 287 288 func generateFolderContentAqlQuery(repoKey, relativePath string, paginationOffset int, disabledDistinctiveAql bool) string { 289 query := fmt.Sprintf(`items.find({"type":"any","$or":[{"$and":[{"repo":"%s","path":{"$match":"%s"},"name":{"$match":"*"}}]}]})`, repoKey, relativePath) 290 query += `.include("repo","path","name","type","size")` 291 query += fmt.Sprintf(`.sort({"$asc":["name"]}).offset(%d).limit(%d)`, paginationOffset*AqlPaginationLimit, AqlPaginationLimit) 292 query += appendDistinctIfNeeded(disabledDistinctiveAql) 293 return query 294 } 295 296 // Decide how to continue handling the directory by the node's state in the repository snapshot (completed or not) 297 // Outputs: 298 // node - A node in the repository snapshot tree, which represents the current directory. 299 // completed - Whether handling the node directory was completed. If it wasn't fully transferred, we start exploring and transferring it from scratch. 300 // previousChildren - If the directory requires exploring, previously known children will be added from this map in order to preserve their states and references. 301 func (m *fullTransferPhase) getAndHandleDirectoryNode(relativePath string) (node *reposnapshot.Node, completed bool, err error) { 302 node, err = m.stateManager.LookUpNode(relativePath) 303 if err != nil { 304 return 305 } 306 307 // If data was not loaded from snapshot, we know that the node is visited for the first time and was not explored. 308 if !m.stateManager.WasSnapshotLoaded() { 309 return 310 } 311 312 completed, err = node.IsCompleted() 313 if err != nil { 314 return 315 } 316 if completed { 317 log.Debug("Skipping completed folder:", path.Join(m.repoKey, relativePath)) 318 return 319 } 320 // If the node was not completed, we will start exploring it from the beginning. 321 // Remove all files names because we will begin exploring from the beginning. 322 err = node.RestartExploring() 323 return 324 }