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

     1  package state
     2  
     3  import (
     4  	"encoding/json"
     5  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
     6  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
     7  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
     8  	"github.com/jfrog/jfrog-client-go/utils/log"
     9  	"os"
    10  	"path/filepath"
    11  	"sync"
    12  	"time"
    13  )
    14  
    15  var saveStateMutex sync.Mutex
    16  
    17  type ActionOnStateFunc func(state *TransferState) error
    18  
    19  const (
    20  	transferStateFileVersion                = 0
    21  	transferStateFileInvalidVersionErrorMsg = "unexpected repository state file found"
    22  )
    23  
    24  // This struct holds the state of the current repository being transferred from the source Artifactory instance.
    25  // It is saved to a file located in a directory named with the sha of the repository, under the transfer directory.
    26  // The transfer-files command uses this state to determine which phases should be executed for the repository,
    27  // as well as other decisions related to the process execution.
    28  type TransferState struct {
    29  	lastSaveTimestamp time.Time
    30  	// The Version of the state.json file of a repository.
    31  	Version     int        `json:"state_version,omitempty"`
    32  	CurrentRepo Repository `json:"repository,omitempty"`
    33  }
    34  
    35  type Repository struct {
    36  	Phase1Info   ProgressState `json:"phase1_info,omitempty"`
    37  	Phase2Info   ProgressState `json:"phase2_info,omitempty"`
    38  	Phase3Info   ProgressState `json:"phase3_info,omitempty"`
    39  	Name         string        `json:"name,omitempty"`
    40  	FullTransfer PhaseDetails  `json:"full_transfer,omitempty"`
    41  	Diffs        []DiffDetails `json:"diffs,omitempty"`
    42  }
    43  
    44  type PhaseDetails struct {
    45  	Started string `json:"started,omitempty"`
    46  	Ended   string `json:"ended,omitempty"`
    47  }
    48  
    49  type DiffDetails struct {
    50  	// The start and end time of a complete transferring Files Diff phase
    51  	FilesDiffRunTime PhaseDetails `json:"files_diff,omitempty"`
    52  	// The start and end time of the last handled range
    53  	HandledRange PhaseDetails `json:"handled_range,omitempty"`
    54  	// If false, start the Diff phase from the start time of the full transfer
    55  	Completed bool `json:"completed,omitempty"`
    56  }
    57  
    58  func newRepositoryTransferState(repoKey string) TransferState {
    59  	return TransferState{
    60  		Version:     transferStateFileVersion,
    61  		CurrentRepo: Repository{Name: repoKey},
    62  	}
    63  }
    64  
    65  func (ts *TransferState) Action(action ActionOnStateFunc) error {
    66  	if err := action(ts); err != nil {
    67  		return err
    68  	}
    69  
    70  	now := time.Now()
    71  	if now.Sub(ts.lastSaveTimestamp).Seconds() < float64(stateAndStatusSaveIntervalSecs) {
    72  		return nil
    73  	}
    74  
    75  	if !saveStateMutex.TryLock() {
    76  		return nil
    77  	}
    78  	defer saveStateMutex.Unlock()
    79  
    80  	ts.lastSaveTimestamp = now
    81  	return ts.persistTransferState(false)
    82  }
    83  
    84  // Persist TransferState to file, at the repository dir. If snapshot requested, persist it to the repository's snapshot dir.
    85  func (ts *TransferState) persistTransferState(snapshot bool) (err error) {
    86  	repoStateFilePath, err := GetRepoStateFilepath(ts.CurrentRepo.Name, snapshot)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	content, err := json.Marshal(ts)
    92  	if err != nil {
    93  		return errorutils.CheckError(err)
    94  	}
    95  
    96  	err = os.WriteFile(repoStateFilePath, content, 0600)
    97  	if err != nil {
    98  		return errorutils.CheckError(err)
    99  	}
   100  	return nil
   101  }
   102  
   103  func (ts *TransferState) IsRepoTransferred() (isTransferred bool, err error) {
   104  	return isTransferred, ts.Action(func(state *TransferState) error {
   105  		isTransferred = state.CurrentRepo.FullTransfer.Ended != ""
   106  		return nil
   107  	})
   108  }
   109  
   110  func LoadTransferState(repoKey string, snapshot bool) (transferState TransferState, exists bool, err error) {
   111  	stateFilePath, err := GetRepoStateFilepath(repoKey, snapshot)
   112  	if err != nil {
   113  		return
   114  	}
   115  	exists, err = fileutils.IsFileExists(stateFilePath, false)
   116  	if err != nil || !exists {
   117  		return
   118  	}
   119  
   120  	content, err := fileutils.ReadFile(stateFilePath)
   121  	if err != nil {
   122  		return
   123  	}
   124  
   125  	if err = json.Unmarshal(content, &transferState); errorutils.CheckError(err) != nil {
   126  		return
   127  	}
   128  	if transferState.Version != transferStateFileVersion {
   129  		return TransferState{}, false, errorutils.CheckErrorf(transferStateFileInvalidVersionErrorMsg)
   130  	}
   131  	return
   132  }
   133  
   134  func GetRepoStateFilepath(repoKey string, snapshot bool) (string, error) {
   135  	var dirPath string
   136  	var err error
   137  	if snapshot {
   138  		dirPath, err = GetJfrogTransferRepoSnapshotDir(repoKey)
   139  	} else {
   140  		dirPath, err = GetRepositoryTransferDir(repoKey)
   141  	}
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	return filepath.Join(dirPath, coreutils.JfrogTransferRepoStateFileName), nil
   146  }
   147  
   148  // Returns a transfer state and repo transfer snapshot according to the state of the repository as found in the repository transfer directory.
   149  // A repo transfer snapshot is only returned if running phase 1 is required.
   150  // The state and snapshot will be loaded from snapshot dir if a previous run of phase 1 was interrupted, and reset was not required.
   151  func getTransferStateAndSnapshot(repoKey string, reset bool) (transferState TransferState, repoTransferSnapshot *RepoTransferSnapshot, err error) {
   152  	if reset {
   153  		return getCleanStateAndSnapshot(repoKey)
   154  	}
   155  
   156  	// Check if repo state exists. If not, start clean.
   157  	transferState, exists, err := LoadTransferState(repoKey, false)
   158  	if err != nil {
   159  		return
   160  	}
   161  	if !exists {
   162  		return getCleanStateAndSnapshot(repoKey)
   163  	}
   164  
   165  	// If it exists and repo already fully completed phase 1, load current state.
   166  	transferred, err := transferState.IsRepoTransferred()
   167  	if err != nil || transferred {
   168  		return transferState, nil, err
   169  	}
   170  
   171  	// Phase 1 was started and not completed. Try loading snapshots to continue from the same point.
   172  	return loadRepoSnapshots(repoKey)
   173  }
   174  
   175  // Loads the state and repo snapshots from the repository's snapshot directory.
   176  func loadRepoSnapshots(repoKey string) (transferState TransferState, repoTransferSnapshot *RepoTransferSnapshot, err error) {
   177  	transferState, stateExists, err := LoadTransferState(repoKey, true)
   178  	if err != nil {
   179  		return
   180  	}
   181  	snapshotPath, err := GetRepoSnapshotFilePath(repoKey)
   182  	if err != nil {
   183  		return
   184  	}
   185  	repoTransferSnapshot, snapshotExists, err := loadRepoTransferSnapshot(repoKey, snapshotPath)
   186  	if !stateExists || !snapshotExists {
   187  		log.Info("An attempt to transfer repository '" + repoKey + "' was previously stopped but no snapshot was found to continue from. " +
   188  			"Starting to transfer from scratch...")
   189  		return getCleanStateAndSnapshot(repoKey)
   190  	}
   191  	return
   192  }
   193  
   194  func getCleanStateAndSnapshot(repoKey string) (transferState TransferState, repoTransferSnapshot *RepoTransferSnapshot, err error) {
   195  	snapshotFilePath, err := GetRepoSnapshotFilePath(repoKey)
   196  	if err != nil {
   197  		return
   198  	}
   199  	return newRepositoryTransferState(repoKey), createRepoTransferSnapshot(repoKey, snapshotFilePath), nil
   200  }