github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/revisions.go (about) 1 package sharing 2 3 import ( 4 "fmt" 5 "path/filepath" 6 "strconv" 7 "strings" 8 9 "github.com/cozy/cozy-stack/model/vfs" 10 "github.com/cozy/cozy-stack/pkg/couchdb" 11 "github.com/cozy/cozy-stack/pkg/couchdb/revision" 12 "github.com/cozy/cozy-stack/pkg/prefixer" 13 ) 14 15 type conflictStatus int 16 17 const ( 18 // NoConflict is the status when the rev is in the revisions chain (OK) 19 NoConflict conflictStatus = iota 20 // LostConflict is the status when rev is greater than the last revision of 21 // the chain (the resolution is often to abort the update) 22 LostConflict 23 // WonConflict is the status when rev is not in the chain, 24 // but the last revision of the chain is still (the resolution can be to 25 // make the update but including rev in the revisions chain) 26 WonConflict 27 ) 28 29 // MaxDepth is the maximum number of revisions in a chain that we keep for a 30 // document. 31 const MaxDepth = 100 32 33 // RevsStruct is a struct for revisions in bulk methods of CouchDB 34 type RevsStruct struct { 35 Start int `json:"start"` 36 IDs []string `json:"ids"` 37 } 38 39 // RevsTree is a tree of revisions, like CouchDB has. 40 // The revisions are sorted by growing generation (the number before the hyphen). 41 // http://docs.couchdb.org/en/stable/replication/conflicts.html#revision-tree 42 type RevsTree struct { 43 // Rev is a revision, with the generation and the id 44 // e.g. 1-1bad9a88f0a608ea78c12ab49882ac41 45 Rev string `json:"rev"` 46 47 // Branches is the list of revisions that have this revision for parent. 48 // The general case is to have only one branch, but we can have more with 49 // conflicts. 50 Branches []RevsTree `json:"branches,omitempty"` 51 } 52 53 // Clone duplicates the RevsTree 54 func (rt *RevsTree) Clone() RevsTree { 55 cloned := RevsTree{Rev: rt.Rev} 56 cloned.Branches = make([]RevsTree, len(rt.Branches)) 57 for i, b := range rt.Branches { 58 cloned.Branches[i] = b.Clone() 59 } 60 return cloned 61 } 62 63 // Generation returns the maximal generation of a revision in this tree 64 func (rt *RevsTree) Generation() int { 65 if len(rt.Branches) == 0 { 66 return revision.Generation(rt.Rev) 67 } 68 max := 0 69 for _, b := range rt.Branches { 70 if g := b.Generation(); g > max { 71 max = g 72 } 73 } 74 return max 75 } 76 77 // Find returns the sub-tree for the given revision, or nil if not found. It 78 // also gives the depth of the sub-tree (how many nodes are traversed from the 79 // root of RevsTree to reach this sub-tree). 80 func (rt *RevsTree) Find(rev string) (*RevsTree, int) { 81 if rt.Rev == rev { 82 return rt, 1 83 } 84 for i := range rt.Branches { 85 if sub, depth := rt.Branches[i].Find(rev); sub != nil { 86 return sub, depth + 1 87 } 88 } 89 return nil, 0 90 } 91 92 // ensureMaxDepth will remove the first nodes of the branch that contains parent. 93 func (rt *RevsTree) ensureMaxDepth(parent string, depth int) { 94 current := rt 95 for depth > MaxDepth { 96 if len(current.Branches) == 0 { 97 break 98 } else if len(current.Branches) == 1 { 99 next := current.Branches[0] 100 current.Rev = next.Rev 101 current.Branches = next.Branches 102 depth-- 103 } else { 104 for i := range current.Branches { 105 b := ¤t.Branches[i] 106 if sub, _ := b.Find(parent); sub != nil { 107 current = b 108 break 109 } 110 } 111 } 112 } 113 } 114 115 // Add inserts the given revision in the main branch 116 func (rt *RevsTree) Add(rev string) *RevsTree { 117 if rev == rt.Rev { 118 return rt 119 } 120 121 if revision.Generation(rev) < revision.Generation(rt.Rev) { 122 rt.Branches = []RevsTree{ 123 {Rev: rt.Rev, Branches: rt.Branches}, 124 } 125 rt.Rev = rev 126 return rt 127 } 128 129 if len(rt.Branches) > 0 { 130 // XXX This condition shouldn't be true, but it can help to limit 131 // damage in case bugs happen. 132 if rt.Branches[0].Rev == rev { 133 return &rt.Branches[0] 134 } 135 return rt.Branches[0].Add(rev) 136 } 137 138 rt.Branches = []RevsTree{ 139 {Rev: rev}, 140 } 141 return &rt.Branches[0] 142 } 143 144 // InsertAfter inserts the given revision in the tree as a child of the second 145 // revision. 146 func (rt *RevsTree) InsertAfter(rev, parent string) { 147 subtree, depth := rt.Find(parent) 148 if subtree == nil { 149 // XXX This condition shouldn't be true, but it can help to limit 150 // damage in case bugs happen. 151 if sub, _ := rt.Find(rev); sub != nil { 152 return 153 } 154 subtree = rt.Add(parent) 155 } 156 157 rt.ensureMaxDepth(parent, depth+1) 158 159 for _, b := range subtree.Branches { 160 if b.Rev == rev { 161 return 162 } 163 } 164 subtree.Branches = append(subtree.Branches, RevsTree{Rev: rev}) 165 } 166 167 // InsertChain inserts a chain of revisions, ie the first revision is the 168 // parent of the second revision, which is itself the parent of the third 169 // revision, etc. The first revisions of the chain are very probably already in 170 // the tree, the last one is certainly not. 171 // TODO ensure the MaxDepth limit is respected 172 func (rt *RevsTree) InsertChain(chain []string) { 173 if len(chain) == 0 { 174 return 175 } 176 common := 0 177 var subtree *RevsTree 178 var depth int 179 for i, rev := range chain { 180 subtree, depth = rt.Find(rev) 181 if subtree != nil { 182 depth += len(chain) - i 183 common = i 184 break 185 } 186 } 187 if subtree == nil { 188 subtree = rt.Add(chain[0]) 189 } 190 191 rt.ensureMaxDepth(subtree.Rev, depth) 192 193 for _, rev := range chain[common+1:] { 194 if len(subtree.Branches) > 0 { 195 found := false 196 for i := range subtree.Branches { 197 if subtree.Branches[i].Rev == rev { 198 found = true 199 subtree = &subtree.Branches[i] 200 break 201 } 202 } 203 if found { 204 continue 205 } 206 } 207 subtree.Branches = append(subtree.Branches, RevsTree{Rev: rev}) 208 subtree = &subtree.Branches[0] 209 } 210 } 211 212 // revsMapToStruct builds a RevsStruct from a json unmarshaled to a map 213 func revsMapToStruct(revs interface{}) *RevsStruct { 214 revisions, ok := revs.(map[string]interface{}) 215 if !ok { 216 return nil 217 } 218 start, ok := revisions["start"].(float64) 219 if !ok { 220 return nil 221 } 222 slice, ok := revisions["ids"].([]interface{}) 223 if !ok { 224 return nil 225 } 226 ids := make([]string, len(slice)) 227 for i, id := range slice { 228 ids[i], _ = id.(string) 229 } 230 return &RevsStruct{ 231 Start: int(start), 232 IDs: ids, 233 } 234 } 235 236 // revsChainToStruct transforms revisions from on format to another: 237 // ["2-aa", "3-bb", "4-cc"] -> { start: 4, ids: ["cc", "bb", "aa"] } 238 func revsChainToStruct(revs []string) RevsStruct { 239 s := RevsStruct{ 240 IDs: make([]string, len(revs)), 241 } 242 var last string 243 for i, rev := range revs { 244 parts := strings.SplitN(rev, "-", 2) 245 last = parts[0] 246 s.IDs[len(s.IDs)-i-1] = parts[1] 247 } 248 s.Start, _ = strconv.Atoi(last) 249 return s 250 } 251 252 // revsStructToChain is the reverse of revsChainToStruct: 253 // { start: 4, ids: ["cc", "bb", "aa"] } -> ["2-aa", "3-bb", "4-cc"] 254 func revsStructToChain(revs RevsStruct) []string { 255 start := revs.Start 256 ids := revs.IDs 257 chain := make([]string, len(ids)) 258 for i, id := range ids { 259 rev := fmt.Sprintf("%d-%s", start, id) 260 chain[len(ids)-i-1] = rev 261 start-- 262 } 263 return chain 264 } 265 266 // detectConflict says if there is a conflict (ie rev is not in the revisions 267 // chain), and if it is the case, if the update should be made (WonConflict) or 268 // aborted (LostConflict) 269 func detectConflict(rev string, chain []string) conflictStatus { 270 if len(chain) == 0 { 271 return LostConflict 272 } 273 for _, r := range chain { 274 if r == rev { 275 return NoConflict 276 } 277 } 278 279 last := chain[len(chain)-1] 280 genl := revision.Generation(last) 281 genr := revision.Generation(rev) 282 if genl > genr { 283 return WonConflict 284 } else if genl < genr { 285 return LostConflict 286 } else if last > rev { 287 return WonConflict 288 } 289 return LostConflict 290 } 291 292 // MixupChainToResolveConflict creates a new chain of revisions that can be 293 // used to resolve a conflict: the new chain will start the old rev and include 294 // other revisions from the chain with a greater generation. 295 func MixupChainToResolveConflict(rev string, chain []string) []string { 296 gen := revision.Generation(rev) 297 mixed := make([]string, 0) 298 found := false 299 for _, r := range chain { 300 if found { 301 mixed = append(mixed, r) 302 } else if gen == revision.Generation(r) { 303 mixed = append(mixed, rev) 304 found = true 305 } 306 } 307 return mixed 308 } 309 310 // addMissingRevsToChain includes the missing doc revisions to the chain, i.e. 311 // all the doc revisions between the highest revision saved in the 312 // revisions tree, and the lowest revision of the chain. 313 // This can occur when both local and remote updates are made to the same doc, 314 // with the remote ones saved before the local ones. 315 func addMissingRevsToChain(db prefixer.Prefixer, ref *SharedRef, chain []string) ([]string, error) { 316 refHighestGen := ref.Revisions.Generation() 317 chainLowestGen := revision.Generation(chain[0]) 318 if refHighestGen >= chainLowestGen-1 { 319 return chain, nil 320 } 321 docRef := extractDocReferenceFromID(ref.SID) 322 doc := &couchdb.JSONDoc{} 323 err := couchdb.GetDocWithRevs(db, docRef.Type, docRef.ID, doc) 324 if err != nil { 325 return chain, err 326 } 327 revisions := revsMapToStruct(doc.M["_revisions"]) 328 if len(revisions.IDs) < chainLowestGen-1 { 329 return nil, fmt.Errorf("Cannot add the missing revs to io.cozy.shared %s", docRef.ID) 330 } 331 var oldRevs []string 332 for i := refHighestGen + 1; i < chainLowestGen; i++ { 333 revID := revisions.IDs[len(revisions.IDs)-i] 334 revGen := strconv.Itoa(i) 335 rev := revGen + "-" + revID 336 oldRevs = append(oldRevs, rev) 337 } 338 chain = append(oldRevs, chain...) 339 return chain, nil 340 } 341 342 // conflictName generates a new name for a file/folder in conflict with another 343 // that has the same path. A conflicted file `foo` will be renamed foo (2), 344 // then foo (3), etc. 345 func conflictName(indexer vfs.Indexer, dirID, name string, isFile bool) string { 346 base, ext := name, "" 347 if isFile { 348 ext = filepath.Ext(name) 349 base = strings.TrimSuffix(base, ext) 350 } 351 i := 2 352 if strings.HasSuffix(base, ")") { 353 if idx := strings.LastIndex(base, " ("); idx > 0 { 354 num, err := strconv.Atoi(base[idx+2 : len(base)-1]) 355 if err == nil { 356 i = num + 1 357 base = base[0:idx] 358 } 359 } 360 } 361 for j := 0; j < 1000; j++ { 362 newname := fmt.Sprintf("%s (%d)%s", base, i, ext) 363 exists, err := indexer.DirChildExists(dirID, newname) 364 if err != nil || !exists { 365 return newname 366 } 367 i++ 368 } 369 return fmt.Sprintf("%s (%d)%s", base, i, ext) 370 } 371 372 // conflictID generates a new ID for a file/folder that has a conflict between 373 // two versions of its content. 374 func conflictID(id, rev string) string { 375 parts := strings.SplitN(rev, "-", 2) 376 key := []byte(parts[1]) 377 for i, c := range key { 378 switch { 379 case '0' <= c && c <= '9': 380 key[i] = c - '0' 381 case 'a' <= c && c <= 'f': 382 key[i] = c - 'a' + 10 383 case 'A' <= c && c <= 'F': 384 key[i] = c - 'A' + 10 385 } 386 } 387 return XorID(id, key) 388 } 389 390 // CheckSharedError is the type used when checking the io.cozy.shared, and one 391 // document has two revisions where a child don't its generation equal to the 392 // generation of the parent plus one. 393 type CheckSharedError struct { 394 Type string `json:"type"` 395 ID string `json:"_id"` 396 Parent string `json:"parent_rev"` 397 Child string `json:"child_rev"` 398 } 399 400 func (rt *RevsTree) check() *CheckSharedError { 401 if len(rt.Branches) == 0 { 402 return nil 403 } 404 405 gen := revision.Generation(rt.Rev) 406 for _, b := range rt.Branches { 407 if revision.Generation(b.Rev) != gen+1 { 408 return &CheckSharedError{ 409 Type: "invalid_revs_suite", 410 Parent: rt.Rev, 411 Child: b.Rev, 412 } 413 } 414 } 415 416 for _, b := range rt.Branches { 417 if check := b.check(); check != nil { 418 return check 419 } 420 } 421 return nil 422 }