github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/vfs/couchdb_indexer.go (about) 1 package vfs 2 3 import ( 4 "encoding/json" 5 "errors" 6 "os" 7 "path" 8 "strings" 9 "time" 10 11 "github.com/cozy/cozy-stack/pkg/consts" 12 "github.com/cozy/cozy-stack/pkg/couchdb" 13 "github.com/cozy/cozy-stack/pkg/couchdb/mango" 14 "github.com/cozy/cozy-stack/pkg/logger" 15 "github.com/cozy/cozy-stack/pkg/prefixer" 16 ) 17 18 type couchdbIndexer struct { 19 db prefixer.Prefixer 20 } 21 22 // NewCouchdbIndexer creates an Indexer instance based on couchdb to store 23 // files and directories metadata and index them. 24 func NewCouchdbIndexer(db prefixer.Prefixer) Indexer { 25 return &couchdbIndexer{ 26 db: db, 27 } 28 } 29 30 func (c *couchdbIndexer) InitIndex() error { 31 createDate := time.Now() 32 err := couchdb.CreateNamedDocWithDB(c.db, &DirDoc{ 33 DocName: "", 34 Type: consts.DirType, 35 DocID: consts.RootDirID, 36 Fullpath: "/", 37 DirID: "", 38 CreatedAt: createDate, 39 UpdatedAt: createDate, 40 }) 41 if err != nil { 42 return err 43 } 44 45 err = couchdb.CreateNamedDocWithDB(c.db, &DirDoc{ 46 DocName: path.Base(TrashDirName), 47 Type: consts.DirType, 48 DocID: consts.TrashDirID, 49 Fullpath: TrashDirName, 50 DirID: consts.RootDirID, 51 CreatedAt: createDate, 52 UpdatedAt: createDate, 53 }) 54 if err != nil && !couchdb.IsConflictError(err) { 55 return err 56 } 57 return nil 58 } 59 60 // DiskUsage returns the total size of the files (current + old versions). 61 func (c *couchdbIndexer) DiskUsage() (int64, error) { 62 used, err := c.FilesUsage() 63 if err != nil { 64 return 0, err 65 } 66 67 if versions, err := c.VersionsUsage(); err == nil { 68 used += versions 69 } 70 71 return used, nil 72 } 73 74 // FilesUsage returns the files total size (without versions) 75 func (c *couchdbIndexer) FilesUsage() (int64, error) { 76 var doc couchdb.ViewResponse 77 err := couchdb.ExecView(c.db, couchdb.DiskUsageView, &couchdb.ViewRequest{ 78 Reduce: true, 79 }, &doc) 80 if err != nil { 81 return 0, err 82 } 83 if len(doc.Rows) == 0 { 84 return 0, nil 85 } 86 // Reduce of _sum should give us a number value 87 used, ok := doc.Rows[0].Value.(float64) 88 if !ok { 89 return 0, ErrWrongCouchdbState 90 } 91 92 return int64(used), nil 93 } 94 95 // VersionsUsage returns the total size for old file versions (not including 96 // current version). 97 func (c *couchdbIndexer) VersionsUsage() (int64, error) { 98 var doc couchdb.ViewResponse 99 err := couchdb.ExecView(c.db, couchdb.OldVersionsDiskUsageView, &couchdb.ViewRequest{ 100 Reduce: true, 101 }, &doc) 102 if err != nil { 103 return 0, err 104 } 105 if len(doc.Rows) == 0 { 106 return 0, nil 107 } 108 // Reduce of _sum should give us a number value 109 used, ok := doc.Rows[0].Value.(float64) 110 if !ok { 111 return 0, ErrWrongCouchdbState 112 } 113 114 return int64(used), nil 115 } 116 117 // TrashUsage returns the space taken by the files in the trash. 118 func (c *couchdbIndexer) TrashUsage() (int64, error) { 119 var size int64 120 trash, err := c.DirByID(consts.TrashDirID) 121 if err != nil { 122 return 0, err 123 } 124 err = walk(c, trash.Name(), trash, nil, func(_ string, _ *DirDoc, file *FileDoc, err error) error { 125 if file != nil { 126 size += file.ByteSize 127 } 128 return err 129 }, 0) 130 return size, err 131 } 132 133 func (c *couchdbIndexer) prepareFileDoc(doc *FileDoc) error { 134 // Ensure that fullpath is filled because it's used in realtime/@events 135 if _, err := doc.Path(c); err != nil { 136 return err 137 } 138 // If a valid datetime is extracted from the EXIF metadata, use it as the 139 // created_at of the file. By valid, we mean that we filter out photos 140 // taken on camera were the clock was never configured (e.g. 1970-01-01). 141 if date, ok := doc.Metadata["datetime"].(time.Time); ok && date.Year() > 1990 { 142 doc.CreatedAt = date 143 if doc.UpdatedAt.Before(date) { 144 doc.UpdatedAt = date 145 } 146 } 147 return nil 148 } 149 150 func (c *couchdbIndexer) DirSize(doc *DirDoc) (int64, error) { 151 start := doc.Fullpath + "/" 152 stop := doc.Fullpath + "/\ufff0" 153 if doc.DocID == consts.RootDirID { 154 start = "/" 155 stop = "/\ufff0" 156 } 157 158 // Find the subdirectories 159 sel := mango.And( 160 mango.Gt("path", start), 161 mango.Lt("path", stop), 162 mango.Equal("type", consts.DirType), 163 ) 164 req := &couchdb.FindRequest{ 165 UseIndex: "dir-by-path", 166 Selector: sel, 167 Fields: []string{"_id"}, 168 Limit: 10000, 169 } 170 var children []couchdb.JSONDoc 171 err := couchdb.FindDocs(c.db, consts.Files, req, &children) 172 if err != nil { 173 return 0, err 174 } 175 keys := make([]interface{}, len(children)+1) 176 keys[0] = doc.DocID 177 for i, child := range children { 178 keys[i+1] = child.ID() 179 } 180 181 // Get the size for the directory and each of its sub-directory, and sum them 182 var resp couchdb.ViewResponse 183 err = couchdb.ExecView(c.db, couchdb.DiskUsageView, &couchdb.ViewRequest{ 184 Keys: keys, 185 Group: true, 186 Reduce: true, 187 }, &resp) 188 if err != nil { 189 return 0, err 190 } 191 if len(resp.Rows) == 0 { 192 return 0, nil 193 } 194 195 // Reduce of _sum should give us a number value 196 var size int64 197 for _, row := range resp.Rows { 198 value, ok := row.Value.(float64) 199 if !ok { 200 return 0, ErrWrongCouchdbState 201 } 202 size += int64(value) 203 } 204 return size, nil 205 } 206 207 func (c *couchdbIndexer) CreateFileDoc(doc *FileDoc) error { 208 if err := c.prepareFileDoc(doc); err != nil { 209 return err 210 } 211 return couchdb.CreateDoc(c.db, doc) 212 } 213 214 func (c *couchdbIndexer) CreateNamedFileDoc(doc *FileDoc) error { 215 if err := c.prepareFileDoc(doc); err != nil { 216 return err 217 } 218 return couchdb.CreateNamedDoc(c.db, doc) 219 } 220 221 func (c *couchdbIndexer) UpdateFileDoc(olddoc, newdoc *FileDoc) error { 222 if err := c.prepareFileDoc(newdoc); err != nil { 223 return err 224 } 225 226 fullpath, err := olddoc.Path(c) 227 if err != nil { 228 return err 229 } 230 if strings.HasPrefix(fullpath, TrashDirName) { 231 c.checkTrashedFileIsShared(newdoc) 232 } 233 234 newdoc.SetID(olddoc.ID()) 235 newdoc.SetRev(olddoc.Rev()) 236 return couchdb.UpdateDocWithOld(c.db, newdoc, olddoc) 237 } 238 239 var DeleteNote = func(db prefixer.Prefixer, noteID string) {} 240 241 func (c *couchdbIndexer) DeleteFileDoc(doc *FileDoc) error { 242 // Ensure that fullpath is filled because it's used in realtime/@events 243 if _, err := doc.Path(c); err != nil { 244 return err 245 } 246 if doc.Mime == consts.NoteMimeType { 247 DeleteNote(c.db, doc.DocID) 248 } 249 return couchdb.DeleteDoc(c.db, doc) 250 } 251 252 func (c *couchdbIndexer) CreateDirDoc(doc *DirDoc) error { 253 return couchdb.CreateDoc(c.db, doc) 254 } 255 256 func (c *couchdbIndexer) CreateNamedDirDoc(doc *DirDoc) error { 257 return couchdb.CreateNamedDoc(c.db, doc) 258 } 259 260 func (c *couchdbIndexer) UpdateDirDoc(olddoc, newdoc *DirDoc) error { 261 newdoc.SetID(olddoc.ID()) 262 newdoc.SetRev(olddoc.Rev()) 263 264 oldTrashed := strings.HasPrefix(olddoc.Fullpath, TrashDirName) 265 newTrashed := strings.HasPrefix(newdoc.Fullpath, TrashDirName) 266 267 isRestored := oldTrashed && !newTrashed 268 isTrashed := !oldTrashed && newTrashed 269 270 if isTrashed { 271 c.checkTrashedDirIsShared(newdoc) 272 if err := c.setTrashedForFilesInsideDir(olddoc, true); err != nil { 273 return err 274 } 275 } 276 277 if newdoc.Fullpath != olddoc.Fullpath { 278 if err := c.MoveDir(olddoc.Fullpath, newdoc.Fullpath); err != nil { 279 return err 280 } 281 } 282 283 if err := couchdb.UpdateDocWithOld(c.db, newdoc, olddoc); err != nil { 284 return err 285 } 286 287 if isRestored { 288 if err := c.setTrashedForFilesInsideDir(newdoc, false); err != nil { 289 return err 290 } 291 } 292 293 return nil 294 } 295 296 func (c *couchdbIndexer) DeleteDirDoc(doc *DirDoc) error { 297 return couchdb.DeleteDoc(c.db, doc) 298 } 299 300 func (c *couchdbIndexer) DeleteDirDocAndContent(doc *DirDoc, onlyContent bool) (files []*FileDoc, n int64, err error) { 301 var docs []couchdb.Doc 302 if !onlyContent { 303 docs = append(docs, doc) 304 } 305 err = walk(c, doc.Name(), doc, nil, func(name string, dir *DirDoc, file *FileDoc, err error) error { 306 if err != nil { 307 return err 308 } 309 if dir != nil { 310 if dir.ID() == doc.ID() { 311 return nil 312 } 313 docs = append(docs, dir.Clone()) 314 } else { 315 cloned := file.Clone() 316 docs = append(docs, cloned) 317 files = append(files, cloned.(*FileDoc)) 318 n += file.ByteSize 319 if file.Mime == consts.NoteMimeType { 320 DeleteNote(c.db, file.DocID) 321 } 322 } 323 return err 324 }, 0) 325 if err == nil { 326 err = c.BatchDelete(docs) 327 } 328 return 329 } 330 331 func (c *couchdbIndexer) BatchDelete(docs []couchdb.Doc) error { 332 remaining := docs 333 for len(remaining) > 0 { 334 n := 1000 335 if len(remaining) < n { 336 n = len(remaining) 337 } 338 toDelete := remaining[:n] 339 remaining = remaining[n:] 340 if err := couchdb.BulkDeleteDocs(c.db, consts.Files, toDelete); err != nil { 341 // If it fails once, try again 342 time.Sleep(1 * time.Second) 343 if err := couchdb.BulkDeleteDocs(c.db, consts.Files, toDelete); err != nil { 344 return err 345 } 346 } 347 } 348 return nil 349 } 350 351 func (c *couchdbIndexer) MoveDir(oldpath, newpath string) error { 352 if oldpath+"/" == newpath { 353 return nil 354 } 355 356 limit := 256 357 var children []*DirDoc 358 docs := make([]interface{}, 0, limit) 359 olddocs := make([]interface{}, 0, limit) 360 isTrashed := strings.HasPrefix(newpath, TrashDirName) 361 362 // We limit the stack to 128 bulk updates to avoid infinite loops, as we 363 // had a case in the past. 364 start := oldpath + "/" 365 stop := oldpath + "/\ufff0" 366 for i := 0; i < 128; i++ { 367 // The simple selector mango.StartWith can have some issues when 368 // renaming a folder to the same name, but with a different unicode 369 // normalization (like NFC to NFD). In that case, CouchDB would always 370 // return the same documents with this selector, as it does the 371 // comparison on a normalized string. 372 sel := mango.And( 373 mango.Gt("path", start), 374 mango.Lt("path", stop), 375 ) 376 req := &couchdb.FindRequest{ 377 UseIndex: "dir-by-path", 378 Selector: sel, 379 Skip: 0, 380 Limit: limit, 381 } 382 err := couchdb.FindDocs(c.db, consts.Files, req, &children) 383 if err != nil { 384 return err 385 } 386 if len(children) == 0 { 387 break 388 } 389 start = children[len(children)-1].Fullpath 390 for _, child := range children { 391 // XXX In theory, only the directories have a path, but in 392 // practice, it's safer to check it as we had already some bugs in 393 // the VFS. 394 if child.Type != consts.DirType { 395 continue 396 } 397 // XXX We can have documents that are not a child of the moved dir 398 // because of the comparison of strings used by CouchDB: 399 // /Photos/ < /PHOTOS/AAA < /Photos/bbb < /Photos0 400 // So, we need to skip the documents that are not really the children. 401 // Cf http://docs.couchdb.org/en/stable/ddocs/views/collation.html#collation-specification 402 if !strings.HasPrefix(child.Fullpath, oldpath) { 403 continue 404 } 405 cloned := child.Clone() 406 if isTrashed { 407 c.checkTrashedDirIsShared(child) 408 } 409 olddocs = append(olddocs, cloned) 410 child.Fullpath = path.Join(newpath, child.Fullpath[len(oldpath)+1:]) 411 docs = append(docs, child) 412 } 413 if err = couchdb.BulkUpdateDocs(c.db, consts.Files, docs, olddocs); err != nil { 414 return err 415 } 416 if len(children) < limit { 417 break 418 } 419 children = children[:0] 420 docs = docs[:0] 421 olddocs = olddocs[:0] 422 } 423 424 return nil 425 } 426 427 func (c *couchdbIndexer) DirByID(fileID string) (*DirDoc, error) { 428 doc := &DirDoc{} 429 err := couchdb.GetDoc(c.db, consts.Files, fileID, doc) 430 if couchdb.IsNotFoundError(err) { 431 return nil, os.ErrNotExist 432 } 433 if err != nil { 434 return nil, err 435 } 436 if doc.Type != consts.DirType { 437 return nil, os.ErrNotExist 438 } 439 return doc, nil 440 } 441 442 func (c *couchdbIndexer) DirByPath(name string) (*DirDoc, error) { 443 if !path.IsAbs(name) { 444 return nil, ErrNonAbsolutePath 445 } 446 var docs []*DirDoc 447 sel := mango.Equal("path", path.Clean(name)) 448 req := &couchdb.FindRequest{ 449 UseIndex: "dir-by-path", 450 Selector: sel, 451 Limit: 1, 452 } 453 err := couchdb.FindDocs(c.db, consts.Files, req, &docs) 454 if err != nil { 455 return nil, err 456 } 457 if len(docs) == 0 { 458 return nil, os.ErrNotExist 459 } 460 if docs[0].Type != consts.DirType { 461 return nil, os.ErrNotExist 462 } 463 return docs[0], nil 464 } 465 466 func (c *couchdbIndexer) FileByID(fileID string) (*FileDoc, error) { 467 doc := &FileDoc{} 468 err := couchdb.GetDoc(c.db, consts.Files, fileID, doc) 469 if couchdb.IsNotFoundError(err) { 470 return nil, os.ErrNotExist 471 } 472 if err != nil { 473 return nil, err 474 } 475 if doc.Type != consts.FileType { 476 return nil, os.ErrNotExist 477 } 478 return doc, nil 479 } 480 481 func (c *couchdbIndexer) FileByPath(name string) (*FileDoc, error) { 482 if !path.IsAbs(name) { 483 return nil, ErrNonAbsolutePath 484 } 485 parent, err := c.DirByPath(path.Dir(name)) 486 if err != nil { 487 return nil, err 488 } 489 490 // consts.FilesByParentView keys are [parentID, type, name] 491 var res couchdb.ViewResponse 492 err = couchdb.ExecView(c.db, couchdb.FilesByParentView, &couchdb.ViewRequest{ 493 Key: []string{parent.DocID, consts.FileType, path.Base(name)}, 494 IncludeDocs: true, 495 }, &res) 496 if err != nil { 497 return nil, err 498 } 499 500 // XXX CouchDB can return documents with the same name but in a different 501 // encoding (NFC vs NFD), so we need to check that the name is really the 502 // expected one. 503 for _, row := range res.Rows { 504 var doc FileDoc 505 if err = json.Unmarshal(row.Doc, &doc); err != nil { 506 return nil, err 507 } 508 if doc.DocName == path.Base(name) { 509 return &doc, nil 510 } 511 } 512 return nil, os.ErrNotExist 513 } 514 515 func (c *couchdbIndexer) FilePath(doc *FileDoc) (string, error) { 516 var parentPath string 517 if doc.DirID == consts.RootDirID { 518 parentPath = "/" 519 } else if doc.DirID == consts.TrashDirID { 520 parentPath = TrashDirName 521 } else { 522 parent, err := c.DirByID(doc.DirID) 523 if err != nil { 524 return "", ErrParentDoesNotExist 525 } 526 parentPath = parent.Fullpath 527 } 528 return path.Join(parentPath, doc.DocName), nil 529 } 530 531 func (c *couchdbIndexer) DirOrFileByID(fileID string) (*DirDoc, *FileDoc, error) { 532 dirOrFile := &DirOrFileDoc{} 533 err := couchdb.GetDoc(c.db, consts.Files, fileID, dirOrFile) 534 if err != nil { 535 if couchdb.IsNotFoundError(err) { 536 err = os.ErrNotExist 537 } 538 return nil, nil, err 539 } 540 dirDoc, fileDoc := dirOrFile.Refine() 541 return dirDoc, fileDoc, nil 542 } 543 544 func (c *couchdbIndexer) DirOrFileByPath(name string) (*DirDoc, *FileDoc, error) { 545 dirDoc, err := c.DirByPath(name) 546 if err == nil { 547 return dirDoc, nil, nil 548 } 549 if !os.IsNotExist(err) { 550 return nil, nil, err 551 } 552 fileDoc, err := c.FileByPath(name) 553 if err == nil { 554 return nil, fileDoc, nil 555 } 556 return nil, nil, err 557 } 558 559 func (c *couchdbIndexer) DirIterator(doc *DirDoc, opts *IteratorOptions) DirIterator { 560 return NewIterator(c.db, doc, opts) 561 } 562 563 func (c *couchdbIndexer) DirBatch(doc *DirDoc, cursor couchdb.Cursor) ([]DirOrFileDoc, error) { 564 // consts.FilesByParentView keys are [parentID, type, name] 565 req := couchdb.ViewRequest{ 566 StartKey: []string{doc.DocID, ""}, 567 EndKey: []string{doc.DocID, couchdb.MaxString}, 568 IncludeDocs: true, 569 } 570 var res couchdb.ViewResponse 571 cursor.ApplyTo(&req) 572 err := couchdb.ExecView(c.db, couchdb.FilesByParentView, &req, &res) 573 if err != nil { 574 return nil, err 575 } 576 cursor.UpdateFrom(&res) 577 578 docs := make([]DirOrFileDoc, len(res.Rows)) 579 for i, row := range res.Rows { 580 var doc DirOrFileDoc 581 err := json.Unmarshal(row.Doc, &doc) 582 if err != nil { 583 return nil, err 584 } 585 docs[i] = doc 586 } 587 588 return docs, nil 589 } 590 591 func (c *couchdbIndexer) DirLength(doc *DirDoc) (int, error) { 592 req := couchdb.ViewRequest{ 593 StartKey: []string{doc.DocID, ""}, 594 EndKey: []string{doc.DocID, couchdb.MaxString}, 595 Reduce: true, 596 GroupLevel: 1, 597 } 598 var res couchdb.ViewResponse 599 err := couchdb.ExecView(c.db, couchdb.FilesByParentView, &req, &res) 600 if err != nil { 601 return 0, err 602 } 603 604 if len(res.Rows) == 0 { 605 return 0, nil 606 } 607 608 // Reduce of _count should give us a number value 609 f64, ok := res.Rows[0].Value.(float64) 610 if !ok { 611 return 0, ErrWrongCouchdbState 612 } 613 return int(f64), nil 614 } 615 616 func (c *couchdbIndexer) DirChildExists(dirID, name string) (bool, error) { 617 var res couchdb.ViewResponse 618 619 // consts.FilesByParentView keys are [parentID, type, name] 620 err := couchdb.ExecView(c.db, couchdb.FilesByParentView, &couchdb.ViewRequest{ 621 Keys: []interface{}{ 622 []string{dirID, consts.FileType, name}, 623 []string{dirID, consts.DirType, name}, 624 }, 625 IncludeDocs: true, 626 }, &res) 627 if err != nil { 628 return false, err 629 } 630 631 // XXX CouchDB can return documents with the same name but in a different 632 // encoding (NFC vs NFD), so we need to check that the name is really the 633 // expected one. 634 for _, row := range res.Rows { 635 var doc DirOrFileDoc 636 if err = json.Unmarshal(row.Doc, &doc); err != nil { 637 return false, err 638 } 639 if doc.DirDoc == nil { 640 return false, ErrWrongCouchdbState 641 } 642 if doc.DocName == name { 643 return true, nil 644 } 645 } 646 return false, nil 647 } 648 649 func (c *couchdbIndexer) setTrashedForFilesInsideDir(doc *DirDoc, trashed bool) error { 650 var files, olddocs []interface{} 651 dirs := map[string]string{ 652 doc.DocID: doc.Fullpath, 653 } 654 err := walk(c, doc.Fullpath, doc, nil, func(name string, dir *DirDoc, file *FileDoc, err error) error { 655 if dir != nil { 656 dirs[dir.DocID] = dir.Fullpath 657 } 658 if file != nil && file.Trashed != trashed { 659 // Fullpath is used by event triggers and should be pre-filled here 660 cloned := file.Clone().(*FileDoc) 661 parentPath, ok := dirs[file.DirID] 662 if !ok { 663 logger.WithDomain(c.db.DomainName()).WithNamespace("vfs"). 664 Infof("setTrashedForFilesInsideDir: parent not found for %s", file.DocID) 665 return nil 666 } 667 fullpath := path.Join(parentPath, file.DocName) 668 fullpath = strings.TrimPrefix(fullpath, TrashDirName) 669 trashpath := strings.Replace(fullpath, doc.Fullpath, TrashDirName, 1) 670 if trashed { 671 c.checkTrashedFileIsShared(cloned) 672 cloned.fullpath = fullpath 673 file.fullpath = trashpath 674 } else { 675 cloned.fullpath = trashpath 676 file.fullpath = fullpath 677 } 678 file.Trashed = trashed 679 files = append(files, file) 680 olddocs = append(olddocs, cloned) 681 } 682 return err 683 }, 0) 684 if err != nil { 685 return err 686 } 687 return c.BatchUpdate(files, olddocs) 688 } 689 690 func (c *couchdbIndexer) BatchUpdate(docs, oldDocs []interface{}) error { 691 return couchdb.BulkUpdateDocs(c.db, consts.Files, docs, oldDocs) 692 } 693 694 func (c *couchdbIndexer) CreateVersion(v *Version) error { 695 return couchdb.CreateNamedDocWithDB(c.db, v) 696 } 697 698 func (c *couchdbIndexer) DeleteVersion(v *Version) error { 699 return couchdb.DeleteDoc(c.db, v) 700 } 701 702 func (c *couchdbIndexer) AllVersions() ([]*Version, error) { 703 var versions []*Version 704 err := couchdb.ForeachDocs(c.db, consts.FilesVersions, func(_id string, doc json.RawMessage) error { 705 var v Version 706 if err := json.Unmarshal(doc, &v); err != nil { 707 return err 708 } 709 versions = append(versions, &v) 710 return nil 711 }) 712 return versions, err 713 } 714 715 func (c *couchdbIndexer) BatchDeleteVersions(versions []*Version) error { 716 remaining := make([]couchdb.Doc, len(versions)) 717 for i, v := range versions { 718 remaining[i] = v 719 } 720 for len(remaining) > 0 { 721 n := 1000 722 if len(remaining) < n { 723 n = len(remaining) 724 } 725 toDelete := remaining[:n] 726 remaining = remaining[n:] 727 if err := couchdb.BulkDeleteDocs(c.db, consts.FilesVersions, toDelete); err != nil { 728 // If it fails once, try again 729 if err := couchdb.BulkDeleteDocs(c.db, consts.FilesVersions, toDelete); err != nil { 730 return err 731 } 732 } 733 } 734 return nil 735 } 736 737 func (c *couchdbIndexer) ListNotSynchronizedOn(clientID string) ([]DirDoc, error) { 738 req := couchdb.ViewRequest{ 739 StartKey: []string{consts.OAuthClients, clientID}, 740 EndKey: []string{consts.OAuthClients, clientID}, 741 IncludeDocs: true, 742 } 743 var docs []DirDoc 744 745 cursor := couchdb.NewKeyCursor(100, nil, "") 746 for cursor.HasMore() { 747 cursor.ApplyTo(&req) 748 var res couchdb.ViewResponse 749 err := couchdb.ExecView(c.db, couchdb.DirNotSynchronizedOnView, &req, &res) 750 if err != nil { 751 return nil, err 752 } 753 cursor.UpdateFrom(&res) 754 755 for _, row := range res.Rows { 756 var doc DirDoc 757 err := json.Unmarshal(row.Doc, &doc) 758 if err != nil { 759 return nil, err 760 } 761 docs = append(docs, doc) 762 } 763 } 764 765 return docs, nil 766 } 767 768 // checkTrashedDirIsShared will look for a dir going to the trash if it was the 769 // main dir of a sharing. If it is the case, the sharing is revoked and the 770 // reference to the sharing is removed. 771 func (c *couchdbIndexer) checkTrashedDirIsShared(doc *DirDoc) { 772 refs := doc.ReferencedBy[:0] 773 for _, ref := range doc.ReferencedBy { 774 if ref.Type == consts.Sharings { 775 c.revokeSharing(ref.ID) 776 } else { 777 refs = append(refs, ref) 778 } 779 } 780 doc.ReferencedBy = refs 781 } 782 783 // checkTrashedFileIsShared will look for a file going to the trash if it was 784 // the main file of a sharing. If it is the case, the sharing is revoked and 785 // the reference to the sharing is removed. 786 func (c *couchdbIndexer) checkTrashedFileIsShared(doc *FileDoc) { 787 // A shortcut is created for a sharing not yet accepted if the owner of the 788 // sharing knows the Cozy URL of a recipient. Normally, this shortcut is 789 // removed when the sharing is accepted, but it is safer to avoid revoking 790 // the sharing is such a shortcut is deleted later. 791 if doc.Class == "shortcut" { 792 return 793 } 794 795 refs := doc.ReferencedBy[:0] 796 for _, ref := range doc.ReferencedBy { 797 if ref.Type == consts.Sharings { 798 c.revokeSharing(ref.ID) 799 } else { 800 refs = append(refs, ref) 801 } 802 } 803 doc.ReferencedBy = refs 804 } 805 806 // RevokeSharingFunc does nothing. It will will be overridden from the sharing 807 // package. 808 var RevokeSharingFunc = func(db prefixer.Prefixer, sharingID string) { 809 logger.WithNamespace("vfs").WithField("critical", "true"). 810 Errorf("RevokeSharingFunc called without having been overridden!") 811 } 812 813 // revokeSharing is called when the main file/dir of a sharing is trashed, in 814 // order to revoke the sharing. 815 // 816 // Note: we don't want to rely on triggers/jobs, as they are asynchronous and 817 // if they are executed a bit late, the trashing of the files inside the shared 818 // directory can be replicated to the other members before the sharing is 819 // revoked. 820 // 821 // Technically, it isn't straightforward, as we are in the VFS and we can't 822 // call functions in the sharing module (it would cause cyclic import error). 823 // And, we don't have an instance, just a CouchDB indexer. So, it is a bit 824 // hackish. 825 func (c *couchdbIndexer) revokeSharing(sharingID string) { 826 RevokeSharingFunc(c.db, sharingID) 827 } 828 829 func (c *couchdbIndexer) CheckIndexIntegrity(accumulate func(*FsckLog), failFast bool) error { 830 tree, err := c.BuildTree() 831 if err != nil { 832 return err 833 } 834 if err := c.CheckTreeIntegrity(tree, accumulate, failFast); err != nil && !errors.Is(err, ErrFsckFailFast) { 835 return err 836 } 837 return nil 838 } 839 840 func (c *couchdbIndexer) CheckTreeIntegrity(tree *Tree, accumulate func(*FsckLog), failFast bool) error { 841 if err := c.checkNoConflicts(accumulate, failFast); err != nil { 842 if errors.Is(err, ErrFsckFailFast) { 843 return nil 844 } 845 return err 846 } 847 848 if tree.Root == nil { 849 accumulate(&FsckLog{Type: IndexMissingRoot}) 850 return nil 851 } 852 853 if _, ok := tree.DirsMap[consts.TrashDirID]; !ok { 854 accumulate(&FsckLog{Type: IndexMissingTrash}) 855 if failFast { 856 return nil 857 } 858 } 859 860 // cleanDirsMap browse the given root tree recursively into its children 861 // directories, removing them from the dirsmap table along the way. In the 862 // end, only trees with cycles should stay in the dirsmap. 863 if ok := cleanDirsMap(tree.Root, tree.DirsMap, accumulate, failFast); !ok { 864 return nil 865 } 866 for _, entries := range tree.Orphans { 867 for _, f := range entries { 868 if f.IsDir { 869 if ok := cleanDirsMap(f, tree.DirsMap, accumulate, failFast); !ok { 870 return nil 871 } 872 } 873 } 874 } 875 876 for _, orphanCycle := range tree.DirsMap { 877 orphanCycle.HasCycle = true 878 tree.Orphans[orphanCycle.DirID] = append(tree.Orphans[orphanCycle.DirID], orphanCycle) 879 } 880 881 for _, orphansTree := range tree.Orphans { 882 for _, orphan := range orphansTree { 883 if !orphan.IsDir { 884 accumulate(&FsckLog{ 885 Type: IndexOrphanTree, 886 IsFile: true, 887 FileDoc: orphan, 888 }) 889 } else { 890 accumulate(&FsckLog{ 891 Type: IndexOrphanTree, 892 IsFile: false, 893 DirDoc: orphan, 894 }) 895 } 896 if failFast { 897 return nil 898 } 899 } 900 } 901 902 return couchdb.ForeachDocs(c.db, consts.FilesVersions, func(_ string, data json.RawMessage) error { 903 v := &Version{} 904 if erru := json.Unmarshal(data, v); erru != nil { 905 return erru 906 } 907 fileID := strings.SplitN(v.DocID, "/", 2)[0] 908 if _, ok := tree.Files[fileID]; !ok { 909 accumulate(&FsckLog{ 910 Type: FileMissing, 911 IsVersion: true, 912 VersionDoc: v, 913 }) 914 } 915 return nil 916 }) 917 } 918 919 func (c *couchdbIndexer) BuildTree(eaches ...func(*TreeFile)) (t *Tree, err error) { 920 t = &Tree{ 921 Root: nil, 922 Orphans: make(map[string][]*TreeFile, 32), // DirID -> *FileDoc 923 DirsMap: make(map[string]*TreeFile, 256), // DocID -> *FileDoc 924 Files: make(map[string]struct{}, 1024), // DocID -> ∅ 925 } 926 927 // NOTE: the each method is called with objects in no particular order. The 928 // only enforcement is that either the Fullpath of the objet is informed or 929 // the IsOrphan flag is precised. It may be useful to gather along the way 930 // the files without having to browse the whole tree structure. 931 var each func(*TreeFile) 932 if len(eaches) > 0 { 933 each = eaches[0] 934 } else { 935 each = func(*TreeFile) {} 936 } 937 938 err = couchdb.ForeachDocs(c.db, consts.Files, func(_ string, data json.RawMessage) error { 939 var f *TreeFile 940 if erru := json.Unmarshal(data, &f); erru != nil { 941 return erru 942 } 943 f.IsDir = f.Type == consts.DirType 944 if f.DocID == consts.RootDirID { 945 t.Root = f 946 each(f) 947 } else if parent, ok := t.DirsMap[f.DirID]; ok { 948 if f.IsDir { 949 parent.DirsChildren = append(parent.DirsChildren, f) 950 } else { 951 parent.FilesChildren = append(parent.FilesChildren, f) 952 parent.FilesChildrenSize += f.ByteSize 953 if f.Fullpath == "" { 954 f.Fullpath = path.Join(parent.Fullpath, f.DocName) 955 } else { 956 f.HasPath = true 957 } 958 } 959 each(f) 960 } else { 961 t.Orphans[f.DirID] = append(t.Orphans[f.DirID], f) 962 } 963 if f.IsDir { 964 if bucket, ok := t.Orphans[f.DocID]; ok { 965 for _, child := range bucket { 966 if child.IsDir { 967 f.DirsChildren = append(f.DirsChildren, child) 968 } else { 969 f.FilesChildren = append(f.FilesChildren, child) 970 f.FilesChildrenSize += child.ByteSize 971 if child.Fullpath == "" { 972 child.Fullpath = path.Join(f.Fullpath, child.DocName) 973 } else { 974 child.HasPath = true 975 } 976 } 977 each(child) 978 } 979 delete(t.Orphans, f.DocID) 980 } 981 t.DirsMap[f.DocID] = f 982 } else { 983 t.Files[f.DocID] = struct{}{} 984 } 985 return nil 986 }) 987 for _, bucket := range t.Orphans { 988 for _, child := range bucket { 989 child.IsOrphan = true 990 each(child) 991 } 992 } 993 return 994 } 995 996 func cleanDirsMap( 997 parent *TreeFile, 998 dirsmap map[string]*TreeFile, 999 accumulate func(*FsckLog), 1000 failFast bool, 1001 ) bool { 1002 delete(dirsmap, parent.DocID) 1003 names := make(map[string]struct{}) 1004 inTrash := strings.HasPrefix(parent.Fullpath, TrashDirName) 1005 1006 for _, file := range parent.FilesChildren { 1007 if inTrash != file.Trashed { 1008 if file.Trashed { 1009 accumulate(&FsckLog{ 1010 Type: TrashedNotInTrash, 1011 FileDoc: file, 1012 }) 1013 } else { 1014 accumulate(&FsckLog{ 1015 Type: NotTrashedInTrash, 1016 FileDoc: file, 1017 }) 1018 } 1019 if failFast { 1020 return false 1021 } 1022 } 1023 1024 if file.HasPath { 1025 accumulate(&FsckLog{ 1026 Type: IndexFileWithPath, 1027 FileDoc: file, 1028 }) 1029 if failFast { 1030 return false 1031 } 1032 } 1033 if _, ok := names[file.DocName]; ok { 1034 accumulate(&FsckLog{ 1035 Type: IndexDuplicateName, 1036 FileDoc: file, 1037 }) 1038 if failFast { 1039 return false 1040 } 1041 } 1042 names[file.DocName] = struct{}{} 1043 } 1044 1045 for _, child := range parent.DirsChildren { 1046 if _, ok := names[child.DocName]; ok { 1047 accumulate(&FsckLog{ 1048 Type: IndexDuplicateName, 1049 DirDoc: child, 1050 }) 1051 if failFast { 1052 return false 1053 } 1054 } 1055 names[child.DocName] = struct{}{} 1056 expected := path.Join(parent.Fullpath, child.DocName) 1057 if expected != child.Fullpath { 1058 accumulate(&FsckLog{ 1059 Type: IndexBadFullpath, 1060 DirDoc: child, 1061 ExpectedFullpath: expected, 1062 }) 1063 if failFast { 1064 return false 1065 } 1066 } 1067 if ok := cleanDirsMap(child, dirsmap, accumulate, failFast); !ok { 1068 return false 1069 } 1070 } 1071 return true 1072 } 1073 1074 func (c *couchdbIndexer) checkNoConflicts(accumulate func(*FsckLog), failFast bool) error { 1075 var docs []DirOrFileDoc 1076 req := &couchdb.FindRequest{ 1077 UseIndex: "with-conflicts", 1078 Selector: mango.Exists("_conflicts"), 1079 Limit: 1000, 1080 Conflicts: true, 1081 } 1082 _, err := couchdb.FindDocsRaw(c.db, consts.Files, req, &docs) 1083 if err != nil { 1084 return err 1085 } 1086 for _, doc := range docs { 1087 if doc.Type == consts.DirType { 1088 accumulate(&FsckLog{ 1089 Type: ConflictInIndex, 1090 DirDoc: &TreeFile{DirOrFileDoc: doc}, 1091 }) 1092 } else { 1093 accumulate(&FsckLog{ 1094 Type: ConflictInIndex, 1095 FileDoc: &TreeFile{DirOrFileDoc: doc}, 1096 IsFile: true, 1097 }) 1098 } 1099 if failFast { 1100 return ErrFsckFailFast 1101 } 1102 } 1103 return nil 1104 }