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  }