github.com/jfrog/jfrog-cli-core/v2@v2.52.0/utils/reposnapshot/node.go (about)

     1  package reposnapshot
     2  
     3  import (
     4  	"encoding/json"
     5  	"os"
     6  	"path"
     7  	"sync"
     8  
     9  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    10  )
    11  
    12  // Represents a directory in the repo state snapshot.
    13  type Node struct {
    14  	parent   *Node
    15  	name     string
    16  	children []*Node
    17  	// Mutex is on the Node level to allow modifying non-conflicting content on multiple nodes simultaneously.
    18  	mutex sync.Mutex
    19  	// The files count is used to identify when handling a node is completed. It is only used during runtime, and is not persisted to disk for future runs.
    20  	filesCount      uint32
    21  	totalFilesCount uint32
    22  	totalFilesSize  uint64
    23  	NodeStatus
    24  }
    25  
    26  type NodeStatus uint8
    27  
    28  const (
    29  	Exploring NodeStatus = iota
    30  	DoneExploring
    31  	Completed
    32  )
    33  
    34  // Used to export/load the node tree to/from a file.
    35  // Wrapper is needed since fields on the original node are unexported (to avoid operations that aren't thread safe).
    36  // The wrapper only contains fields that are used in future runs, hence not all fields from Node are persisted.
    37  // In addition, it does not hold the parent pointer to avoid cyclic reference on export.
    38  type NodeExportWrapper struct {
    39  	Name            string               `json:"name,omitempty"`
    40  	Children        []*NodeExportWrapper `json:"children,omitempty"`
    41  	Completed       bool                 `json:"completed,omitempty"`
    42  	TotalFilesCount uint32               `json:"total_files_count,omitempty"`
    43  	TotalFilesSize  uint64               `json:"total_files_size,omitempty"`
    44  }
    45  
    46  type ActionOnNodeFunc func(node *Node) error
    47  
    48  // Perform an action on the node's content.
    49  // Warning: Calling an action inside another action will cause a deadlock!
    50  func (node *Node) action(action ActionOnNodeFunc) error {
    51  	node.mutex.Lock()
    52  	defer node.mutex.Unlock()
    53  
    54  	return action(node)
    55  }
    56  
    57  // Convert node to wrapper in order to save it to file.
    58  func (node *Node) convertToWrapper() (wrapper *NodeExportWrapper, err error) {
    59  	var children []*Node
    60  	err = node.action(func(node *Node) error {
    61  		wrapper = &NodeExportWrapper{
    62  			Name:            node.name,
    63  			Completed:       node.NodeStatus == Completed,
    64  			TotalFilesCount: node.totalFilesCount,
    65  			TotalFilesSize:  node.totalFilesSize,
    66  		}
    67  		children = node.children
    68  		return nil
    69  	})
    70  	if err != nil {
    71  		return
    72  	}
    73  
    74  	for i := range children {
    75  		converted, err := children[i].convertToWrapper()
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  		wrapper.Children = append(wrapper.Children, converted)
    80  	}
    81  	return
    82  }
    83  
    84  // Convert the loaded node export wrapper to node.
    85  func (wrapper *NodeExportWrapper) convertToNode() *Node {
    86  	node := &Node{
    87  		name:            wrapper.Name,
    88  		totalFilesCount: wrapper.TotalFilesCount,
    89  		totalFilesSize:  wrapper.TotalFilesSize,
    90  	}
    91  	// If node wasn't previously completed, we will start exploring it from scratch.
    92  	if wrapper.Completed {
    93  		node.NodeStatus = Completed
    94  	}
    95  	for i := range wrapper.Children {
    96  		converted := wrapper.Children[i].convertToNode()
    97  		converted.parent = node
    98  		node.children = append(node.children, converted)
    99  	}
   100  	return node
   101  }
   102  
   103  // Returns the node's relative path in the repository.
   104  func (node *Node) getActualPath() (actualPath string, err error) {
   105  	err = node.action(func(node *Node) error {
   106  		curPath := node.name
   107  		curNode := node
   108  		// Progress through parent references till reaching root.
   109  		for {
   110  			curNode = curNode.parent
   111  			if curNode == nil {
   112  				// Reached root.
   113  				actualPath = curPath
   114  				return nil
   115  			}
   116  			// Append parent node's dir name to beginning of path.
   117  			curPath = path.Join(curNode.name, curPath)
   118  		}
   119  	})
   120  	return
   121  }
   122  
   123  // Sets node as completed, clear its contents, notifies parent to check completion.
   124  func (node *Node) setCompleted() (err error) {
   125  	var parent *Node
   126  	err = node.action(func(node *Node) error {
   127  		node.NodeStatus = Completed
   128  		node.children = nil
   129  		parent = node.parent
   130  		node.parent = nil
   131  		return nil
   132  	})
   133  	if err == nil && parent != nil {
   134  		return parent.CheckCompleted()
   135  	}
   136  	return
   137  }
   138  
   139  // Sum up all subtree directories with status "completed"
   140  func (node *Node) CalculateTransferredFilesAndSize() (totalFilesCount uint32, totalFilesSize uint64, err error) {
   141  	var children []*Node
   142  	err = node.action(func(node *Node) error {
   143  		children = node.children
   144  		if node.NodeStatus == Completed {
   145  			totalFilesCount = node.totalFilesCount
   146  			totalFilesSize = node.totalFilesSize
   147  		}
   148  		return nil
   149  	})
   150  	if err != nil {
   151  		return
   152  	}
   153  	for _, child := range children {
   154  		childFilesCount, childTotalFilesSize, childErr := child.CalculateTransferredFilesAndSize()
   155  		if childErr != nil {
   156  			return 0, 0, childErr
   157  		}
   158  		totalFilesCount += childFilesCount
   159  		totalFilesSize += childTotalFilesSize
   160  	}
   161  	return
   162  }
   163  
   164  // Check if node completed - if done exploring, done handling files, children are completed.
   165  func (node *Node) CheckCompleted() error {
   166  	isCompleted := false
   167  	err := node.action(func(node *Node) error {
   168  		if node.NodeStatus == Exploring || node.filesCount > 0 {
   169  			return nil
   170  		}
   171  		var totalFilesCount uint32 = 0
   172  		var totalFilesSize uint64 = 0
   173  		for _, child := range node.children {
   174  			totalFilesCount += child.totalFilesCount
   175  			totalFilesSize += child.totalFilesSize
   176  			if child.NodeStatus < Completed {
   177  				return nil
   178  			}
   179  		}
   180  		node.totalFilesCount += totalFilesCount
   181  		node.totalFilesSize += totalFilesSize
   182  		isCompleted = true
   183  		return nil
   184  	})
   185  	if err != nil || !isCompleted {
   186  		return err
   187  	}
   188  	// All files and children completed. Mark this node as completed as well.
   189  	return node.setCompleted()
   190  }
   191  
   192  func (node *Node) IncrementFilesCount(fileSize uint64) error {
   193  	return node.action(func(node *Node) error {
   194  		node.filesCount++
   195  		node.totalFilesCount++
   196  		node.totalFilesSize += fileSize
   197  		return nil
   198  	})
   199  }
   200  
   201  func (node *Node) DecrementFilesCount() error {
   202  	return node.action(func(node *Node) error {
   203  		if node.filesCount == 0 {
   204  			return errorutils.CheckErrorf("attempting to decrease file count in node '%s', but the files count is already 0", node.name)
   205  		}
   206  		node.filesCount--
   207  		return nil
   208  	})
   209  }
   210  
   211  // Adds a new child node to children map.
   212  // childrenPool - [Optional] Children array to check existence of a dirName in before creating a new node.
   213  func (node *Node) AddChildNode(dirName string, childrenPool []*Node) error {
   214  	return node.action(func(node *Node) error {
   215  		for i := range childrenPool {
   216  			if childrenPool[i].name == dirName {
   217  				childrenPool[i].parent = node
   218  				node.children = append(node.children, childrenPool[i])
   219  				return nil
   220  			}
   221  		}
   222  		node.children = append(node.children, CreateNewNode(dirName, node))
   223  		return nil
   224  	})
   225  }
   226  
   227  func (node *Node) convertAndSaveToFile(stateFilePath string) error {
   228  	wrapper, err := node.convertToWrapper()
   229  	if err != nil {
   230  		return err
   231  	}
   232  	content, err := json.Marshal(wrapper)
   233  	if err != nil {
   234  		return errorutils.CheckError(err)
   235  	}
   236  	return errorutils.CheckError(os.WriteFile(stateFilePath, content, 0644))
   237  }
   238  
   239  // Marks that all contents of the node have been found and added.
   240  func (node *Node) MarkDoneExploring() error {
   241  	return node.action(func(node *Node) error {
   242  		node.NodeStatus = DoneExploring
   243  		return nil
   244  	})
   245  }
   246  
   247  func (node *Node) GetChildren() (children []*Node, err error) {
   248  	err = node.action(func(node *Node) error {
   249  		children = node.children
   250  		return nil
   251  	})
   252  	return
   253  }
   254  
   255  func (node *Node) IsCompleted() (completed bool, err error) {
   256  	err = node.action(func(node *Node) error {
   257  		completed = node.NodeStatus == Completed
   258  		return nil
   259  	})
   260  	return
   261  }
   262  
   263  func (node *Node) IsDoneExploring() (doneExploring bool, err error) {
   264  	err = node.action(func(node *Node) error {
   265  		doneExploring = node.NodeStatus >= DoneExploring
   266  		return nil
   267  	})
   268  	return
   269  }
   270  
   271  func (node *Node) RestartExploring() error {
   272  	return node.action(func(node *Node) error {
   273  		node.NodeStatus = Exploring
   274  		node.filesCount = 0
   275  		return nil
   276  	})
   277  }
   278  
   279  // Recursively find the node matching the path represented by the dirs array.
   280  // The search is done by comparing the children of each node path, till reaching the final node in the array.
   281  // If the node is not found, it is added and then returned.
   282  // For example:
   283  // For a structure such as repo->dir1->dir2->dir3
   284  // The initial call will be to the root, and for an input of ({"dir1","dir2"}), and the final output will be a pointer to dir2.
   285  func (node *Node) findMatchingNode(childrenDirs []string) (matchingNode *Node, err error) {
   286  	// The node was found in the cache. Let's return it.
   287  	if len(childrenDirs) == 0 {
   288  		matchingNode = node
   289  		return
   290  	}
   291  
   292  	// Check if any of the current node's children are parents of the current node.
   293  	var children []*Node
   294  	err = node.action(func(node *Node) error {
   295  		children = node.children
   296  		return nil
   297  	})
   298  	if err != nil {
   299  		return
   300  	}
   301  	for i := range children {
   302  		if children[i].name == childrenDirs[0] {
   303  			matchingNode, err = children[i].findMatchingNode(childrenDirs[1:])
   304  			return
   305  		}
   306  	}
   307  
   308  	// None of the current node's children are parents of the current node.
   309  	// This means we need to start creating the searched node parents.
   310  	newNode := CreateNewNode(childrenDirs[0], node)
   311  	err = node.action(func(node *Node) error {
   312  		node.children = append(node.children, newNode)
   313  		return nil
   314  	})
   315  	if err != nil {
   316  		return
   317  	}
   318  	return newNode.findMatchingNode(childrenDirs[1:])
   319  }
   320  
   321  func CreateNewNode(dirName string, parent *Node) *Node {
   322  	return &Node{
   323  		name:   dirName,
   324  		parent: parent,
   325  	}
   326  }