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 }