github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/files.go (about) 1 package sharing 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "os" 10 "path" 11 "path/filepath" 12 "sort" 13 "strings" 14 "time" 15 16 "github.com/cozy/cozy-stack/client/request" 17 "github.com/cozy/cozy-stack/model/instance" 18 "github.com/cozy/cozy-stack/model/note" 19 "github.com/cozy/cozy-stack/model/vfs" 20 "github.com/cozy/cozy-stack/pkg/consts" 21 "github.com/cozy/cozy-stack/pkg/couchdb" 22 "github.com/cozy/cozy-stack/pkg/crypto" 23 "github.com/cozy/cozy-stack/pkg/metadata" 24 multierror "github.com/hashicorp/go-multierror" 25 ) 26 27 // isTrashed returns true for a file or folder inside the trash 28 func isTrashed(doc couchdb.JSONDoc) bool { 29 if doc.Type != consts.Files { 30 return false 31 } 32 if doc.Get("type") == consts.FileType { 33 return doc.Get("trashed") == true 34 } 35 return strings.HasPrefix(doc.Get("path").(string), vfs.TrashDirName+"/") 36 } 37 38 // MakeXorKey generates a key for transforming the file identifiers 39 func MakeXorKey() []byte { 40 random := crypto.GenerateRandomBytes(8) 41 result := make([]byte, 2*len(random)) 42 for i, val := range random { 43 result[2*i] = val & 0xf 44 result[2*i+1] = val >> 4 45 } 46 return result 47 } 48 49 // XorID transforms the identifier of a file to a new identifier, in a 50 // reversible way: it makes a XOR on the hexadecimal characters 51 func XorID(id string, key []byte) string { 52 l := len(key) 53 buf := []byte(id) 54 for i, c := range buf { 55 switch { 56 case '0' <= c && c <= '9': 57 c = (c - '0') ^ key[i%l] 58 case 'a' <= c && c <= 'f': 59 c = (c - 'a' + 10) ^ key[i%l] 60 case 'A' <= c && c <= 'F': 61 c = (c - 'A' + 10) ^ key[i%l] 62 default: 63 continue 64 } 65 if c < 10 { 66 buf[i] = c + '0' 67 } else { 68 buf[i] = (c - 10) + 'a' 69 } 70 } 71 return string(buf) 72 } 73 74 // SortFilesToSent sorts the files slice that will be sent in bulk_docs: 75 // - directories must come before files (if a file is created in a new 76 // directory, we must create directory before the file) 77 // - directories are sorted by increasing depth (if a sub-folder is created 78 // in a new directory, we must create the parent before the child) 79 // - deleted elements must come at the end, to efficiently cope with moves. 80 // For example, if we have A->B->C hierarchy and C is moved elsewhere 81 // and B deleted, we must make the move before deleting B and its children. 82 func (s *Sharing) SortFilesToSent(files []map[string]interface{}) { 83 sort.SliceStable(files, func(i, j int) bool { 84 a, b := files[i], files[j] 85 if removed, ok := a["_deleted"].(bool); ok && removed { 86 return false 87 } 88 if removed, ok := b["_deleted"].(bool); ok && removed { 89 return true 90 } 91 if a["type"] == consts.FileType { 92 return false 93 } 94 if b["type"] == consts.FileType { 95 return true 96 } 97 q, ok := b["path"].(string) 98 if !ok { 99 return false 100 } 101 p, ok := a["path"].(string) 102 if !ok { 103 return true 104 } 105 return strings.Count(p, "/") < strings.Count(q, "/") 106 }) 107 } 108 109 // TransformFileToSent transforms an io.cozy.files document before sending it 110 // to another cozy instance: 111 // - its identifier is XORed 112 // - its dir_id is XORed or removed 113 // - the referenced_by are XORed or removed 114 // - the path is removed (directory only) 115 // 116 // ruleIndexes is a map of "doctype-docid" -> rule index 117 func (s *Sharing) TransformFileToSent(doc map[string]interface{}, xorKey []byte, ruleIndex int) { 118 if doc["type"] == consts.DirType { 119 delete(doc, "path") 120 delete(doc, "not_synchronized_on") 121 } 122 id := doc["_id"].(string) 123 doc["_id"] = XorID(id, xorKey) 124 dir, ok := doc["dir_id"].(string) 125 if !ok { 126 return 127 } 128 rule := s.Rules[ruleIndex] 129 var noDirID bool 130 if rule.Selector == couchdb.SelectorReferencedBy { 131 noDirID = true 132 if refs, ok := doc[couchdb.SelectorReferencedBy].([]interface{}); ok { 133 kept := make([]interface{}, 0) 134 for _, ref := range refs { 135 if r, ok := ref.(map[string]interface{}); ok { 136 v := r["type"].(string) + "/" + r["id"].(string) 137 for _, val := range rule.Values { 138 if val == v { 139 r["id"] = XorID(r["id"].(string), xorKey) 140 kept = append(kept, r) 141 break 142 } 143 } 144 } 145 } 146 doc[couchdb.SelectorReferencedBy] = kept 147 } 148 } else { 149 for _, v := range rule.Values { 150 if v == id { 151 noDirID = true 152 } 153 } 154 delete(doc, couchdb.SelectorReferencedBy) 155 } 156 if !noDirID { 157 for _, v := range rule.Values { 158 if v == dir { 159 noDirID = true 160 break 161 } 162 } 163 } 164 if noDirID { 165 delete(doc, "dir_id") 166 } else { 167 doc["dir_id"] = XorID(dir, xorKey) 168 } 169 } 170 171 // EnsureSharedWithMeDir returns the shared-with-me directory, and create it if 172 // it doesn't exist 173 func EnsureSharedWithMeDir(inst *instance.Instance) (*vfs.DirDoc, error) { 174 fs := inst.VFS() 175 dir, _, err := fs.DirOrFileByID(consts.SharedWithMeDirID) 176 if err != nil && !errors.Is(err, os.ErrNotExist) { 177 inst.Logger().WithNamespace("sharing"). 178 Warnf("EnsureSharedWithMeDir failed to find the dir: %s", err) 179 return nil, err 180 } 181 182 if dir == nil { 183 name := inst.Translate("Tree Shared with me") 184 dir, err = vfs.NewDirDocWithPath(name, consts.RootDirID, "/", nil) 185 if err != nil { 186 inst.Logger().WithNamespace("sharing"). 187 Warnf("EnsureSharedWithMeDir failed to make the dir: %s", err) 188 return nil, err 189 } 190 dir.DocID = consts.SharedWithMeDirID 191 dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil)) 192 err = fs.CreateDir(dir) 193 if errors.Is(err, os.ErrExist) { 194 dir, err = fs.DirByPath(dir.Fullpath) 195 } 196 if err != nil { 197 inst.Logger().WithNamespace("sharing"). 198 Warnf("EnsureSharedWithMeDir failed to create the dir: %s", err) 199 return nil, err 200 } 201 return dir, nil 202 } 203 204 if dir.RestorePath != "" { 205 now := time.Now() 206 instanceURL := inst.PageURL("/", nil) 207 if dir.CozyMetadata == nil { 208 dir.CozyMetadata = vfs.NewCozyMetadata(instanceURL) 209 } else { 210 dir.CozyMetadata.UpdatedAt = now 211 } 212 _, err = vfs.RestoreDir(fs, dir) 213 if err != nil { 214 inst.Logger().WithNamespace("sharing"). 215 Warnf("EnsureSharedWithMeDir failed to restore the dir: %s", err) 216 return nil, err 217 } 218 children, err := fs.DirBatch(dir, couchdb.NewSkipCursor(0, 0)) 219 if err != nil { 220 inst.Logger().WithNamespace("sharing"). 221 Warnf("EnsureSharedWithMeDir failed to find children: %s", err) 222 return nil, err 223 } 224 for _, child := range children { 225 d, f := child.Refine() 226 if d != nil { 227 if d.CozyMetadata == nil { 228 d.CozyMetadata = vfs.NewCozyMetadata(instanceURL) 229 } else { 230 d.CozyMetadata.UpdatedAt = now 231 } 232 _, err = vfs.TrashDir(fs, d) 233 } else { 234 if f.CozyMetadata == nil { 235 f.CozyMetadata = vfs.NewCozyMetadata(instanceURL) 236 } else { 237 f.CozyMetadata.UpdatedAt = now 238 } 239 _, err = vfs.TrashFile(fs, f) 240 } 241 if err != nil { 242 inst.Logger().WithNamespace("sharing"). 243 Warnf("EnsureSharedWithMeDir failed to trash children: %s", err) 244 return nil, err 245 } 246 } 247 } 248 249 return dir, nil 250 } 251 252 // CreateDirForSharing creates the directory where files for this sharing will 253 // be put. This directory will be initially inside the Shared with me folder. 254 func (s *Sharing) CreateDirForSharing(inst *instance.Instance, rule *Rule, parentID string) (*vfs.DirDoc, error) { 255 fs := inst.VFS() 256 var err error 257 var parent *vfs.DirDoc 258 if parentID == "" { 259 parent, err = EnsureSharedWithMeDir(inst) 260 } else { 261 parent, err = fs.DirByID(parentID) 262 } 263 if err != nil { 264 inst.Logger().WithNamespace("sharing"). 265 Warnf("CreateDirForSharing failed to find parent directory: %s", err) 266 return nil, err 267 } 268 dir, err := vfs.NewDirDocWithParent(rule.Title, parent, []string{"from-sharing-" + s.SID}) 269 if err != nil { 270 inst.Logger().WithNamespace("sharing"). 271 Warnf("CreateDirForSharing failed to make dir: %s", err) 272 return nil, err 273 } 274 parts := strings.Split(rule.Values[0], "/") 275 dir.DocID = parts[len(parts)-1] 276 dir.AddReferencedBy(couchdb.DocReference{ 277 ID: s.SID, 278 Type: consts.Sharings, 279 }) 280 dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil)) 281 basename := dir.DocName 282 for i := 2; i < 20; i++ { 283 if err = fs.CreateDir(dir); err == nil { 284 return dir, nil 285 } 286 if couchdb.IsConflictError(err) || errors.Is(err, os.ErrExist) { 287 doc, err := fs.DirByID(dir.DocID) 288 if err == nil { 289 doc.AddReferencedBy(couchdb.DocReference{ 290 ID: s.SID, 291 Type: consts.Sharings, 292 }) 293 _ = couchdb.UpdateDoc(inst, doc) 294 return doc, nil 295 } 296 } 297 dir.DocName = fmt.Sprintf("%s (%d)", basename, i) 298 dir.Fullpath = path.Join(parent.Fullpath, dir.DocName) 299 } 300 inst.Logger().WithNamespace("sharing"). 301 Errorf("Cannot create the sharing directory: %s", err) 302 return nil, err 303 } 304 305 // AddReferenceForSharingDir adds a reference to the sharing on the sharing directory 306 func (s *Sharing) AddReferenceForSharingDir(inst *instance.Instance, rule *Rule) error { 307 fs := inst.VFS() 308 parts := strings.Split(rule.Values[0], "/") 309 dir, _, err := fs.DirOrFileByID(parts[len(parts)-1]) 310 if err != nil { 311 inst.Logger().WithNamespace("sharing"). 312 Warnf("AddReferenceForSharingDir failed to find dir: %s", err) 313 return err 314 } 315 if dir == nil { 316 return nil 317 } 318 for _, ref := range dir.ReferencedBy { 319 if ref.Type == consts.Sharings && ref.ID == s.SID { 320 return nil 321 } 322 } 323 olddoc := dir.Clone().(*vfs.DirDoc) 324 dir.AddReferencedBy(couchdb.DocReference{ 325 ID: s.SID, 326 Type: consts.Sharings, 327 }) 328 if dir.CozyMetadata == nil { 329 dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil)) 330 } else { 331 dir.CozyMetadata.UpdatedAt = time.Now() 332 } 333 return fs.UpdateDirDoc(olddoc, dir) 334 } 335 336 // GetSharingDir returns the directory used by this sharing for putting files 337 // and folders that have no dir_id. 338 func (s *Sharing) GetSharingDir(inst *instance.Instance) (*vfs.DirDoc, error) { 339 // When we can, find the sharing dir by its ID 340 fs := inst.VFS() 341 rule := s.FirstFilesRule() 342 if rule != nil { 343 if rule.Mime != "" { 344 inst.Logger().WithNamespace("sharing"). 345 Warnf("GetSharingDir called for only one file: %s", s.SID) 346 return nil, ErrInternalServerError 347 } 348 dir, _ := fs.DirByID(rule.Values[0]) 349 if dir != nil { 350 return dir, nil 351 } 352 } 353 354 // Else, try to find it by a reference 355 key := []string{consts.Sharings, s.SID} 356 end := []string{key[0], key[1], couchdb.MaxString} 357 req := &couchdb.ViewRequest{ 358 StartKey: key, 359 EndKey: end, 360 IncludeDocs: true, 361 } 362 var res couchdb.ViewResponse 363 err := couchdb.ExecView(inst, couchdb.FilesReferencedByView, req, &res) 364 if err != nil { 365 inst.Logger().WithNamespace("sharing"). 366 Warnf("Sharing dir not found: %v (%s)", err, s.SID) 367 return nil, ErrInternalServerError 368 } 369 var parentID string 370 if len(res.Rows) > 0 { 371 dir, file, err := fs.DirOrFileByID(res.Rows[0].ID) 372 if err != nil { 373 inst.Logger().WithNamespace("sharing"). 374 Warnf("GetSharingDir failed to find dir: %s", err) 375 return dir, err 376 } 377 if dir != nil { 378 return dir, nil 379 } 380 // file is a shortcut 381 parentID = file.DirID 382 if err := fs.DestroyFile(file); err != nil { 383 inst.Logger().WithNamespace("sharing"). 384 Warnf("GetSharingDir failed to delete shortcut: %s", err) 385 return nil, err 386 } 387 s.ShortcutID = "" 388 _ = couchdb.UpdateDoc(inst, s) 389 } 390 if rule == nil { 391 inst.Logger().WithNamespace("sharing"). 392 Errorf("no first rule for: %#v", s) 393 return nil, ErrInternalServerError 394 } 395 // And, we may have to create it in last resort 396 return s.CreateDirForSharing(inst, rule, parentID) 397 } 398 399 // RemoveSharingDir removes the reference on the sharing directory, and adds a 400 // suffix to its name: the suffix will help make the user understand that the 401 // sharing has been revoked, and it will avoid conflicts if the user accepts a 402 // new sharing for the same folder. It should be called when a sharing is 403 // revoked, on the recipient Cozy. 404 func (s *Sharing) RemoveSharingDir(inst *instance.Instance) error { 405 dir, err := s.GetSharingDir(inst) 406 if couchdb.IsNotFoundError(err) { 407 return nil 408 } else if err != nil { 409 return err 410 } 411 olddoc := dir.Clone().(*vfs.DirDoc) 412 dir.RemoveReferencedBy(couchdb.DocReference{ 413 ID: s.SID, 414 Type: consts.Sharings, 415 }) 416 if dir.CozyMetadata == nil { 417 dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil)) 418 } else { 419 dir.CozyMetadata.UpdatedAt = time.Now() 420 } 421 suffix := inst.Translate("Tree Revoked sharing suffix") 422 parentPath := filepath.Dir(dir.Fullpath) 423 basename := fmt.Sprintf("%s (%s)", dir.DocName, suffix) 424 dir.DocName = basename 425 for i := 2; i < 100; i++ { 426 dir.Fullpath = path.Join(parentPath, dir.DocName) 427 if err = inst.VFS().UpdateDirDoc(olddoc, dir); err == nil { 428 return nil 429 } 430 dir.DocName = fmt.Sprintf("%s (%d)", basename, i) 431 } 432 return err 433 } 434 435 // GetNoLongerSharedDir returns the directory used for files and folders that 436 // are removed from a sharing, but are still used via a reference. It the 437 // directory does not exist, it is created. 438 func (s *Sharing) GetNoLongerSharedDir(inst *instance.Instance) (*vfs.DirDoc, error) { 439 fs := inst.VFS() 440 dir, _, err := fs.DirOrFileByID(consts.NoLongerSharedDirID) 441 if err != nil && !errors.Is(err, os.ErrNotExist) { 442 return nil, err 443 } 444 445 if dir == nil { 446 parent, errp := EnsureSharedWithMeDir(inst) 447 if errp != nil { 448 return nil, errp 449 } 450 if strings.HasPrefix(parent.Fullpath, vfs.TrashDirName) { 451 parent, errp = fs.DirByID(consts.RootDirID) 452 if errp != nil { 453 return nil, errp 454 } 455 } 456 name := inst.Translate("Tree No longer shared") 457 dir, err = vfs.NewDirDocWithParent(name, parent, nil) 458 if err != nil { 459 return nil, err 460 } 461 dir.DocID = consts.NoLongerSharedDirID 462 dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil)) 463 if err = fs.CreateDir(dir); err != nil { 464 return nil, err 465 } 466 return dir, nil 467 } 468 469 if dir.RestorePath != "" { 470 now := time.Now() 471 instanceURL := inst.PageURL("/", nil) 472 if dir.CozyMetadata == nil { 473 dir.CozyMetadata = vfs.NewCozyMetadata(instanceURL) 474 } else { 475 dir.CozyMetadata.UpdatedAt = now 476 } 477 dir.CozyMetadata.UpdatedAt = now 478 _, err = vfs.RestoreDir(fs, dir) 479 if err != nil { 480 return nil, err 481 } 482 children, err := fs.DirBatch(dir, couchdb.NewSkipCursor(0, 0)) 483 if err != nil { 484 return nil, err 485 } 486 for _, child := range children { 487 d, f := child.Refine() 488 if d != nil { 489 if d.CozyMetadata == nil { 490 d.CozyMetadata = vfs.NewCozyMetadata(instanceURL) 491 } else { 492 d.CozyMetadata.UpdatedAt = now 493 } 494 _, err = vfs.TrashDir(fs, d) 495 } else { 496 if f.CozyMetadata == nil { 497 f.CozyMetadata = vfs.NewCozyMetadata(instanceURL) 498 } else { 499 f.CozyMetadata.UpdatedAt = now 500 } 501 _, err = vfs.TrashFile(fs, f) 502 } 503 if err != nil { 504 return nil, err 505 } 506 } 507 } 508 509 return dir, nil 510 } 511 512 // GetFolder returns informations about a folder (with XORed IDs) 513 func (s *Sharing) GetFolder(inst *instance.Instance, m *Member, xoredID string) (map[string]interface{}, error) { 514 creds := s.FindCredentials(m) 515 if creds == nil { 516 return nil, ErrInvalidSharing 517 } 518 dirID := XorID(xoredID, creds.XorKey) 519 ref := &SharedRef{} 520 err := couchdb.GetDoc(inst, consts.Shared, consts.Files+"/"+dirID, ref) 521 if err != nil { 522 return nil, err 523 } 524 info, ok := ref.Infos[s.SID] 525 if !ok || info.Removed { 526 return nil, ErrFolderNotFound 527 } 528 dir, err := inst.VFS().DirByID(dirID) 529 if err != nil { 530 return nil, err 531 } 532 doc := dirToJSONDoc(dir, inst.PageURL("/", nil)).M 533 s.TransformFileToSent(doc, creds.XorKey, info.Rule) 534 return doc, nil 535 } 536 537 // ApplyBulkFiles takes a list of documents for the io.cozy.files doctype and 538 // will apply changes to the VFS according to those documents. 539 func (s *Sharing) ApplyBulkFiles(inst *instance.Instance, docs DocsList) error { 540 type retryOp struct { 541 target map[string]interface{} 542 dir *vfs.DirDoc 543 ref *SharedRef 544 } 545 546 var errm error 547 var retries []retryOp 548 fs := inst.VFS() 549 550 for _, target := range docs { 551 id, ok := target["_id"].(string) 552 if !ok { 553 errm = multierror.Append(errm, ErrMissingID) 554 continue 555 } 556 ref := &SharedRef{} 557 err := couchdb.GetDoc(inst, consts.Shared, consts.Files+"/"+id, ref) 558 if err != nil { 559 if !couchdb.IsNotFoundError(err) { 560 inst.Logger().WithNamespace("replicator"). 561 Debugf("Error on finding doc of bulk files: %s", err) 562 errm = multierror.Append(errm, err) 563 continue 564 } 565 ref = nil 566 } 567 var infos SharedInfo 568 if ref != nil { 569 infos, ok = ref.Infos[s.SID] 570 if !ok { 571 inst.Logger().WithNamespace("replicator"). 572 Infof("Operation aborted for %s on sharing %s", id, s.SID) 573 errm = multierror.Append(errm, ErrSafety) 574 continue 575 } 576 } 577 dir, file, err := fs.DirOrFileByID(id) 578 if err != nil && !errors.Is(err, os.ErrNotExist) { 579 inst.Logger().WithNamespace("replicator"). 580 Debugf("Error on finding ref of bulk files: %s", err) 581 errm = multierror.Append(errm, err) 582 continue 583 } 584 if _, ok := target["_deleted"]; ok { 585 if ref == nil || infos.Removed { 586 continue 587 } 588 if dir == nil && file == nil { 589 continue 590 } 591 if dir != nil { 592 err = s.TrashDir(inst, dir) 593 } else { 594 err = s.TrashFile(inst, file, &s.Rules[infos.Rule]) 595 } 596 } else if target["type"] != consts.DirType { 597 // Let the upload worker manages this file 598 continue 599 } else if ref != nil && infos.Removed && !infos.Dissociated { 600 continue 601 } else if dir == nil { 602 err = s.CreateDir(inst, target, delayResolution) 603 if errors.Is(err, os.ErrExist) { 604 retries = append(retries, retryOp{ 605 target: target, 606 }) 607 err = nil 608 } 609 } else if ref == nil || infos.Dissociated { 610 // If it is a file: let the upload worker manages this file 611 // If it is a dir: ignore this (safety rule) 612 continue 613 } else { 614 // XXX we have to clone the dir document as it is modified by the 615 // UpdateDir function and retrying the operation won't work with 616 // the modified doc 617 cloned := dir.Clone().(*vfs.DirDoc) 618 err = s.UpdateDir(inst, target, dir, ref, delayResolution) 619 if errors.Is(err, os.ErrExist) { 620 retries = append(retries, retryOp{ 621 target: target, 622 dir: cloned, 623 ref: ref, 624 }) 625 err = nil 626 } 627 } 628 if err != nil { 629 inst.Logger().WithNamespace("replicator"). 630 Debugf("Error on apply bulk file: %s (%#v - %#v)", err, target, ref) 631 errm = multierror.Append(errm, fmt.Errorf("%s - %w", id, err)) 632 } 633 } 634 635 for _, op := range retries { 636 var err error 637 if op.dir == nil { 638 err = s.CreateDir(inst, op.target, resolveResolution) 639 } else { 640 err = s.UpdateDir(inst, op.target, op.dir, op.ref, resolveResolution) 641 } 642 if err != nil { 643 inst.Logger().WithNamespace("replicator"). 644 Debugf("Error on apply bulk file: %s (%#v - %#v)", err, op.target, op.ref) 645 errm = multierror.Append(errm, err) 646 } 647 } 648 649 return errm 650 } 651 652 func (s *Sharing) GetNotes(inst *instance.Instance) ([]*vfs.FileDoc, error) { 653 rule := s.FirstFilesRule() 654 if rule != nil { 655 if rule.Mime != "" { 656 if rule.Mime == consts.NoteMimeType { 657 var notes []*vfs.FileDoc 658 req := &couchdb.AllDocsRequest{Keys: rule.Values} 659 if err := couchdb.GetAllDocs(inst, consts.Files, req, ¬es); err != nil { 660 return nil, fmt.Errorf("failed to fetch notes shared by themselves: %w", err) 661 } 662 663 return notes, nil 664 } else { 665 return nil, nil 666 } 667 } 668 669 sharingDir, err := s.GetSharingDir(inst) 670 if err != nil { 671 return nil, fmt.Errorf("failed to get notes sharing dir: %w", err) 672 } 673 674 var notes []*vfs.FileDoc 675 fs := inst.VFS() 676 iter := fs.DirIterator(sharingDir, nil) 677 for { 678 _, f, err := iter.Next() 679 if errors.Is(err, vfs.ErrIteratorDone) { 680 break 681 } 682 if err != nil { 683 return nil, fmt.Errorf("failed to get next shared note: %w", err) 684 } 685 if f != nil && f.Mime == consts.NoteMimeType { 686 notes = append(notes, f) 687 } 688 } 689 690 return notes, nil 691 } 692 693 return nil, nil 694 } 695 696 func (s *Sharing) FixRevokedNotes(inst *instance.Instance) error { 697 docs, err := s.GetNotes(inst) 698 if err != nil { 699 return fmt.Errorf("failed to get revoked sharing notes: %w", err) 700 } 701 702 var errm error 703 for _, doc := range docs { 704 // If the note came from another cozy via a sharing that is now revoked, we 705 // may need to recreate the trigger. 706 if err := note.SetupTrigger(inst, doc.ID()); err != nil { 707 errm = multierror.Append(errm, fmt.Errorf("failed to setup revoked note trigger: %w", err)) 708 } 709 710 if err := note.ImportImages(inst, doc); err != nil { 711 errm = multierror.Append(errm, fmt.Errorf("failed to import revoked note images: %w", err)) 712 } 713 } 714 return errm 715 } 716 717 func removeReferencesFromRule(file *vfs.FileDoc, rule *Rule) { 718 if rule.Selector != couchdb.SelectorReferencedBy { 719 return 720 } 721 refs := file.ReferencedBy[:0] 722 for _, ref := range file.ReferencedBy { 723 if !rule.hasReferencedBy(ref) { 724 refs = append(refs, ref) 725 } 726 } 727 file.ReferencedBy = refs 728 } 729 730 func buildReferencedBy(target, file *vfs.FileDoc, rule *Rule) []couchdb.DocReference { 731 refs := make([]couchdb.DocReference, 0) 732 if file != nil { 733 for _, ref := range file.ReferencedBy { 734 if !rule.hasReferencedBy(ref) { 735 refs = append(refs, ref) 736 } 737 } 738 } 739 for _, ref := range target.ReferencedBy { 740 if rule.hasReferencedBy(ref) { 741 refs = append(refs, ref) 742 } 743 } 744 return refs 745 } 746 747 func copySafeFieldsToFile(target, file *vfs.FileDoc) { 748 file.Tags = target.Tags 749 file.Metadata = target.Metadata.RemoveCertifiedMetadata() 750 file.CreatedAt = target.CreatedAt 751 file.UpdatedAt = target.UpdatedAt 752 file.Mime = target.Mime 753 file.Class = target.Class 754 file.Executable = target.Executable 755 file.CozyMetadata = target.CozyMetadata 756 } 757 758 func copySafeFieldsToDir(target map[string]interface{}, dir *vfs.DirDoc) { 759 if tags, ok := target["tags"].([]interface{}); ok { 760 dir.Tags = make([]string, 0, len(tags)) 761 for _, tag := range tags { 762 if t, ok := tag.(string); ok { 763 dir.Tags = append(dir.Tags, t) 764 } 765 } 766 } 767 if created, ok := target["created_at"].(string); ok { 768 if at, err := time.Parse(time.RFC3339Nano, created); err == nil { 769 dir.CreatedAt = at 770 } 771 } 772 if updated, ok := target["updated_at"].(string); ok { 773 if at, err := time.Parse(time.RFC3339Nano, updated); err == nil { 774 dir.UpdatedAt = at 775 } 776 } 777 778 if meta, ok := target["metadata"].(map[string]interface{}); ok { 779 dir.Metadata = vfs.Metadata(meta).RemoveCertifiedMetadata() 780 } 781 782 if meta, ok := target["cozyMetadata"].(map[string]interface{}); ok { 783 dir.CozyMetadata = &vfs.FilesCozyMetadata{} 784 if version, ok := meta["doctypeVersion"].(string); ok { 785 dir.CozyMetadata.DocTypeVersion = version 786 } 787 if version, ok := meta["metadataVersion"].(float64); ok { 788 dir.CozyMetadata.MetadataVersion = int(version) 789 } 790 if created, ok := meta["createdAt"].(string); ok { 791 if at, err := time.Parse(time.RFC3339Nano, created); err == nil { 792 dir.CozyMetadata.CreatedAt = at 793 } 794 } 795 if app, ok := meta["createdByApp"].(string); ok { 796 dir.CozyMetadata.CreatedByApp = app 797 } 798 if version, ok := meta["createdByAppVersion"].(string); ok { 799 dir.CozyMetadata.CreatedByAppVersion = version 800 } 801 if instance, ok := meta["createdOn"].(string); ok { 802 dir.CozyMetadata.CreatedOn = instance 803 } 804 805 if updated, ok := meta["updatedAt"].(string); ok { 806 if at, err := time.Parse(time.RFC3339Nano, updated); err == nil { 807 dir.CozyMetadata.UpdatedAt = at 808 } 809 } 810 if updates, ok := meta["updatedByApps"].([]map[string]interface{}); ok { 811 for _, update := range updates { 812 if slug, ok := update["slug"].(string); ok { 813 entry := &metadata.UpdatedByAppEntry{Slug: slug} 814 if date, ok := update["date"].(string); ok { 815 if at, err := time.Parse(time.RFC3339Nano, date); err == nil { 816 entry.Date = at 817 } 818 } 819 if version, ok := update["version"].(string); ok { 820 entry.Version = version 821 } 822 if instance, ok := update["instance"].(string); ok { 823 entry.Instance = instance 824 } 825 dir.CozyMetadata.UpdatedByApps = append(dir.CozyMetadata.UpdatedByApps, entry) 826 } 827 } 828 } 829 830 // No upload* for directories 831 if account, ok := meta["sourceAccount"].(string); ok { 832 dir.CozyMetadata.SourceAccount = account 833 } 834 if id, ok := meta["sourceAccountIdentifier"].(string); ok { 835 dir.CozyMetadata.SourceIdentifier = id 836 } 837 } 838 } 839 840 // resolveConflictSamePath is used when two files/folders are in conflict 841 // because they have the same path. To resolve the conflict, we take the 842 // file/folder from the owner instance as the winner and rename the other. 843 // 844 // Note: previously, the rule was that the higher id wins, but the rule has 845 // been changed. The new rule helps to minimize the number of exchanges needed 846 // between the cozy instance to converge, and, as such, it helps to avoid 847 // creating more conflicts. 848 // 849 // If the winner is the new file/folder from the other cozy, this function 850 // rename the local file/folder and let the caller retry its operation. 851 // If the winner is the local file/folder, this function returns the new name 852 // and let the caller do its operation with the new name (the caller should 853 // create a dummy revision to let the other cozy know of the renaming). 854 func (s *Sharing) resolveConflictSamePath(inst *instance.Instance, visitorID, pth string) (string, error) { 855 inst.Logger().WithNamespace("replicator"). 856 Infof("Resolve conflict for path=%s (docid=%s)", pth, visitorID) 857 fs := inst.VFS() 858 d, f, err := fs.DirOrFileByPath(pth) 859 if err != nil { 860 return "", err 861 } 862 indexer := vfs.NewCouchdbIndexer(inst) 863 var dirID string 864 if d != nil { 865 dirID = d.DirID 866 } else { 867 dirID = f.DirID 868 } 869 name := conflictName(indexer, dirID, path.Base(pth), f != nil) 870 if s.Owner { 871 return name, nil 872 } 873 if d != nil { 874 old := d.Clone().(*vfs.DirDoc) 875 d.DocName = name 876 d.Fullpath = path.Join(path.Dir(d.Fullpath), d.DocName) 877 return "", fs.UpdateDirDoc(old, d) 878 } 879 old := f.Clone().(*vfs.FileDoc) 880 f.DocName = name 881 f.ResetFullpath() 882 return "", fs.UpdateFileDoc(old, f) 883 } 884 885 // getDirDocFromInstance fetches informations about a directory from the given 886 // member of the sharing. 887 func (s *Sharing) getDirDocFromInstance(inst *instance.Instance, m *Member, creds *Credentials, dirID string) (*vfs.DirDoc, error) { 888 if creds == nil || creds.AccessToken == nil { 889 return nil, ErrInvalidSharing 890 } 891 u, err := url.Parse(m.Instance) 892 if err != nil { 893 return nil, ErrInvalidSharing 894 } 895 opts := &request.Options{ 896 Method: http.MethodGet, 897 Scheme: u.Scheme, 898 Domain: u.Host, 899 Path: "/sharings/" + s.SID + "/io.cozy.files/" + dirID, 900 Headers: request.Headers{ 901 "Accept": "application/json", 902 "Authorization": "Bearer " + creds.AccessToken.AccessToken, 903 }, 904 ParseError: ParseRequestError, 905 } 906 res, err := request.Req(opts) 907 if res != nil && res.StatusCode/100 == 4 { 908 res, err = RefreshToken(inst, err, s, m, creds, opts, nil) 909 } 910 if err != nil { 911 if res != nil && res.StatusCode/100 == 5 { 912 return nil, ErrInternalServerError 913 } 914 return nil, err 915 } 916 defer res.Body.Close() 917 var doc *vfs.DirDoc 918 if err = json.NewDecoder(res.Body).Decode(&doc); err != nil { 919 return nil, err 920 } 921 return doc, nil 922 } 923 924 // getDirDocFromNetwork fetches informations about a directory from the other 925 // cozy instances of this sharing. 926 func (s *Sharing) getDirDocFromNetwork(inst *instance.Instance, dirID string) (*vfs.DirDoc, error) { 927 if !s.Owner { 928 return s.getDirDocFromInstance(inst, &s.Members[0], &s.Credentials[0], dirID) 929 } 930 for i := range s.Credentials { 931 doc, err := s.getDirDocFromInstance(inst, &s.Members[i+1], &s.Credentials[i], dirID) 932 if err == nil { 933 return doc, nil 934 } 935 } 936 return nil, ErrFolderNotFound 937 } 938 939 // recreateParent is used when a file or folder is added by a cozy, and sent to 940 // this instance, but its parent directory was trashed and deleted on this 941 // cozy. To resolve the conflict, this instance will fetch informations from 942 // the other instance about the parent directory and will recreate it. It can 943 // be necessary to recurse if there were several levels of directories deleted. 944 func (s *Sharing) recreateParent(inst *instance.Instance, dirID string) (*vfs.DirDoc, error) { 945 inst.Logger().WithNamespace("replicator"). 946 Debugf("Recreate parent dirID=%s", dirID) 947 doc, err := s.getDirDocFromNetwork(inst, dirID) 948 if err != nil { 949 return nil, fmt.Errorf("recreateParent: %w", err) 950 } 951 fs := inst.VFS() 952 var parent *vfs.DirDoc 953 if doc.DirID == "" { 954 parent, err = s.GetSharingDir(inst) 955 } else { 956 parent, err = fs.DirByID(doc.DirID) 957 if errors.Is(err, os.ErrNotExist) { 958 parent, err = s.recreateParent(inst, doc.DirID) 959 } 960 } 961 if err != nil { 962 return nil, err 963 } 964 doc.DirID = parent.DocID 965 doc.Fullpath = path.Join(parent.Fullpath, doc.DocName) 966 doc.SetRev("") 967 err = fs.CreateDir(doc) 968 if err != nil { 969 // Maybe the directory has been created concurrently, so let's try 970 // again to fetch it from the database 971 if errors.Is(err, os.ErrExist) { 972 return fs.DirByID(dirID) 973 } 974 return nil, fmt.Errorf("recreateParent: %w", err) 975 } 976 return doc, nil 977 } 978 979 // extractNameAndIndexer takes a target document, extracts the name and creates 980 // a sharing indexer with _rev and _revisions 981 func extractNameAndIndexer(inst *instance.Instance, target map[string]interface{}, ref *SharedRef) (string, *sharingIndexer, error) { 982 name, ok := target["name"].(string) 983 if !ok { 984 inst.Logger().WithNamespace("replicator"). 985 Warnf("Missing name for directory %#v", target) 986 return "", nil, ErrInternalServerError 987 } 988 rev, ok := target["_rev"].(string) 989 if !ok { 990 inst.Logger().WithNamespace("replicator"). 991 Warnf("Missing _rev for directory %#v", target) 992 return "", nil, ErrInternalServerError 993 } 994 revs := revsMapToStruct(target["_revisions"]) 995 if revs == nil { 996 inst.Logger().WithNamespace("replicator"). 997 Warnf("Invalid _revisions for directory %#v", target) 998 return "", nil, ErrInternalServerError 999 } 1000 indexer := newSharingIndexer(inst, &bulkRevs{ 1001 Rev: rev, 1002 Revisions: *revs, 1003 }, ref) 1004 return name, indexer, nil 1005 } 1006 1007 type nameConflictResolution int 1008 1009 const ( 1010 delayResolution nameConflictResolution = iota 1011 resolveResolution 1012 ) 1013 1014 // CreateDir creates a directory on this cozy to reflect a change on another 1015 // cozy instance of this sharing. 1016 func (s *Sharing) CreateDir(inst *instance.Instance, target map[string]interface{}, resolution nameConflictResolution) error { 1017 inst.Logger().WithNamespace("replicator"). 1018 Debugf("CreateDir %v (%#v)", target["_id"], target) 1019 ref := SharedRef{ 1020 Infos: make(map[string]SharedInfo), 1021 } 1022 name, indexer, err := extractNameAndIndexer(inst, target, &ref) 1023 if err != nil { 1024 return err 1025 } 1026 fs := inst.VFS().UseSharingIndexer(indexer) 1027 1028 var parent *vfs.DirDoc 1029 if dirID, ok := target["dir_id"].(string); ok { 1030 parent, err = fs.DirByID(dirID) 1031 if errors.Is(err, os.ErrNotExist) { 1032 parent, err = s.recreateParent(inst, dirID) 1033 } 1034 if err != nil { 1035 inst.Logger().WithNamespace("replicator"). 1036 Debugf("Conflict for parent on creating dir: %s", err) 1037 return err 1038 } 1039 } else { 1040 parent, err = s.GetSharingDir(inst) 1041 if err != nil { 1042 return err 1043 } 1044 } 1045 1046 dir, err := vfs.NewDirDocWithParent(name, parent, nil) 1047 if err != nil { 1048 inst.Logger().WithNamespace("replicator"). 1049 Warnf("Cannot initialize dir doc: %s", err) 1050 return err 1051 } 1052 dir.SetID(target["_id"].(string)) 1053 ref.SID = consts.Files + "/" + dir.DocID 1054 copySafeFieldsToDir(target, dir) 1055 rule, ruleIndex := s.findRuleForNewDirectory(dir) 1056 if rule == nil { 1057 return ErrSafety 1058 } 1059 ref.Infos[s.SID] = SharedInfo{Rule: ruleIndex} 1060 err = fs.CreateDir(dir) 1061 if errors.Is(err, os.ErrExist) && resolution == resolveResolution { 1062 name, errr := s.resolveConflictSamePath(inst, dir.DocID, dir.Fullpath) 1063 if errr != nil { 1064 return errr 1065 } 1066 if name != "" { 1067 indexer.IncrementRevision() 1068 dir.DocName = name 1069 dir.Fullpath = path.Join(path.Dir(dir.Fullpath), dir.DocName) 1070 } 1071 err = fs.CreateDir(dir) 1072 } 1073 if err != nil { 1074 inst.Logger().WithNamespace("replicator"). 1075 Debugf("Cannot create dir: %s", err) 1076 return err 1077 } 1078 return nil 1079 } 1080 1081 // prepareDirWithAncestors find the parent directory for dir, and recreates it 1082 // if it is missing. 1083 func (s *Sharing) prepareDirWithAncestors(inst *instance.Instance, dir *vfs.DirDoc, dirID string) error { 1084 if dirID == "" { 1085 parent, err := s.GetSharingDir(inst) 1086 if err != nil { 1087 return err 1088 } 1089 dir.DirID = parent.DocID 1090 dir.Fullpath = path.Join(parent.Fullpath, dir.DocName) 1091 } else if dirID != dir.DirID { 1092 parent, err := inst.VFS().DirByID(dirID) 1093 if errors.Is(err, os.ErrNotExist) { 1094 parent, err = s.recreateParent(inst, dirID) 1095 } 1096 if err != nil { 1097 inst.Logger().WithNamespace("replicator"). 1098 Debugf("Conflict for parent on updating dir: %s", err) 1099 return err 1100 } 1101 dir.DirID = parent.DocID 1102 dir.Fullpath = path.Join(parent.Fullpath, dir.DocName) 1103 } else { 1104 dir.Fullpath = path.Join(path.Dir(dir.Fullpath), dir.DocName) 1105 } 1106 return nil 1107 } 1108 1109 // UpdateDir updates a directory on this cozy to reflect a change on another 1110 // cozy instance of this sharing. 1111 func (s *Sharing) UpdateDir( 1112 inst *instance.Instance, 1113 target map[string]interface{}, 1114 dir *vfs.DirDoc, 1115 ref *SharedRef, 1116 resolution nameConflictResolution, 1117 ) error { 1118 inst.Logger().WithNamespace("replicator"). 1119 Debugf("UpdateDir %v (%#v)", target["_id"], target) 1120 if strings.HasPrefix(dir.Fullpath+"/", vfs.TrashDirName+"/") { 1121 // Don't update a directory in the trash 1122 return nil 1123 } 1124 1125 name, indexer, err := extractNameAndIndexer(inst, target, ref) 1126 if err != nil { 1127 return err 1128 } 1129 1130 chain := revsStructToChain(indexer.bulkRevs.Revisions) 1131 conflict := detectConflict(dir.DocRev, chain) 1132 switch conflict { 1133 case LostConflict: 1134 return nil 1135 case WonConflict: 1136 indexer.WillResolveConflict(dir.DocRev, chain) 1137 case NoConflict: 1138 // Nothing to do 1139 } 1140 1141 fs := inst.VFS().UseSharingIndexer(indexer) 1142 oldDoc := dir.Clone().(*vfs.DirDoc) 1143 dir.DocName = name 1144 dirID, _ := target["dir_id"].(string) 1145 if err = s.prepareDirWithAncestors(inst, dir, dirID); err != nil { 1146 return err 1147 } 1148 copySafeFieldsToDir(target, dir) 1149 1150 err = fs.UpdateDirDoc(oldDoc, dir) 1151 if errors.Is(err, os.ErrExist) && resolution == resolveResolution { 1152 name, errb := s.resolveConflictSamePath(inst, dir.DocID, dir.Fullpath) 1153 if errb != nil { 1154 return errb 1155 } 1156 if name != "" { 1157 indexer.IncrementRevision() 1158 dir.DocName = name 1159 dir.Fullpath = path.Join(path.Dir(dir.Fullpath), dir.DocName) 1160 } 1161 err = fs.UpdateDirDoc(oldDoc, dir) 1162 } 1163 if err != nil { 1164 inst.Logger().WithNamespace("replicator"). 1165 Debugf("Cannot update dir: %s", err) 1166 return err 1167 } 1168 return nil 1169 } 1170 1171 // TrashDir puts the directory in the trash 1172 func (s *Sharing) TrashDir(inst *instance.Instance, dir *vfs.DirDoc) error { 1173 inst.Logger().WithNamespace("replicator"). 1174 Debugf("TrashDir %s (%#v)", dir.DocID, dir) 1175 if strings.HasPrefix(dir.Fullpath+"/", vfs.TrashDirName+"/") { 1176 // nothing to do if the directory is already in the trash 1177 return nil 1178 } 1179 1180 newdir := dir.Clone().(*vfs.DirDoc) 1181 if newdir.CozyMetadata == nil { 1182 newdir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil)) 1183 } else { 1184 newdir.CozyMetadata.UpdatedAt = time.Now() 1185 } 1186 1187 newdir.DirID = consts.TrashDirID 1188 fs := inst.VFS() 1189 exists, err := fs.DirChildExists(newdir.DirID, newdir.DocName) 1190 if err != nil { 1191 return fmt.Errorf("Sharing.TrashDir: %w", err) 1192 } 1193 if exists { 1194 newdir.DocName = conflictName(fs, newdir.DirID, newdir.DocName, true) 1195 } 1196 newdir.Fullpath = path.Join(vfs.TrashDirName, newdir.DocName) 1197 newdir.RestorePath = path.Dir(dir.Fullpath) 1198 if err := s.dissociateDir(inst, dir, newdir); err != nil { 1199 return fmt.Errorf("Sharing.TrashDir: %w", err) 1200 } 1201 return nil 1202 } 1203 1204 func (s *Sharing) dissociateDir(inst *instance.Instance, olddoc, newdoc *vfs.DirDoc) error { 1205 fs := inst.VFS() 1206 1207 newdoc.SetID("") 1208 newdoc.SetRev("") 1209 if err := fs.DissociateDir(olddoc, newdoc); err != nil { 1210 newdoc.DocName = conflictName(fs, newdoc.DirID, newdoc.DocName, true) 1211 newdoc.Fullpath = path.Join(path.Dir(newdoc.Fullpath), newdoc.DocName) 1212 if err := fs.DissociateDir(olddoc, newdoc); err != nil { 1213 return err 1214 } 1215 } 1216 1217 sid := olddoc.DocType() + "/" + olddoc.ID() 1218 var ref SharedRef 1219 if err := couchdb.GetDoc(inst, consts.Shared, sid, &ref); err == nil { 1220 if s.Owner { 1221 ref.Revisions.Add(olddoc.Rev()) 1222 ref.Infos[s.SID] = SharedInfo{ 1223 Rule: ref.Infos[s.SID].Rule, 1224 Binary: false, 1225 Removed: true, 1226 Dissociated: true, 1227 } 1228 _ = couchdb.UpdateDoc(inst, &ref) 1229 } else { 1230 _ = couchdb.DeleteDoc(inst, &ref) 1231 } 1232 } 1233 1234 var errm error 1235 iter := fs.DirIterator(olddoc, nil) 1236 for { 1237 d, f, err := iter.Next() 1238 if errors.Is(err, vfs.ErrIteratorDone) { 1239 break 1240 } 1241 if err != nil { 1242 return err 1243 } 1244 if f != nil { 1245 newf := f.Clone().(*vfs.FileDoc) 1246 newf.DirID = newdoc.DocID 1247 newf.Trashed = true 1248 newf.ResetFullpath() 1249 err = s.dissociateFile(inst, f, newf) 1250 } else { 1251 newd := d.Clone().(*vfs.DirDoc) 1252 newd.DirID = newdoc.DocID 1253 newd.Fullpath = path.Join(newdoc.Fullpath, newd.DocName) 1254 err = s.dissociateDir(inst, d, newd) 1255 } 1256 if err != nil { 1257 errm = multierror.Append(errm, err) 1258 } 1259 } 1260 return errm 1261 } 1262 1263 // TrashFile puts the file in the trash (except if the file has a reference, in 1264 // which case, we keep it in a special folder) 1265 func (s *Sharing) TrashFile(inst *instance.Instance, file *vfs.FileDoc, rule *Rule) error { 1266 inst.Logger().WithNamespace("replicator"). 1267 Debugf("TrashFile %s (%#v)", file.DocID, file) 1268 if file.Trashed { 1269 // Nothing to do if the file is already in the trash 1270 return nil 1271 } 1272 if file.CozyMetadata == nil { 1273 file.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil)) 1274 } else { 1275 file.CozyMetadata.UpdatedAt = time.Now() 1276 } 1277 olddoc := file.Clone().(*vfs.FileDoc) 1278 removeReferencesFromRule(file, rule) 1279 if s.Owner && rule.Selector == couchdb.SelectorReferencedBy { 1280 // Do not move/trash photos removed from an album for the owner 1281 if err := s.dissociateFile(inst, olddoc, file); err != nil { 1282 return fmt.Errorf("Sharing.TrashFile: %w", err) 1283 } 1284 return nil 1285 } 1286 if len(file.ReferencedBy) == 0 { 1287 oldpath, err := olddoc.Path(inst.VFS()) 1288 if err != nil { 1289 return err 1290 } 1291 file.RestorePath = path.Dir(oldpath) 1292 file.Trashed = true 1293 file.DirID = consts.TrashDirID 1294 file.ResetFullpath() 1295 if err := s.dissociateFile(inst, olddoc, file); err != nil { 1296 return fmt.Errorf("Sharing.TrashFile: %w", err) 1297 } 1298 return nil 1299 } 1300 parent, err := s.GetNoLongerSharedDir(inst) 1301 if err != nil { 1302 return fmt.Errorf("Sharing.TrashFile: %w", err) 1303 } 1304 file.DirID = parent.DocID 1305 file.ResetFullpath() 1306 if err := s.dissociateFile(inst, olddoc, file); err != nil { 1307 return fmt.Errorf("Sharing.TrashFile: %w", err) 1308 } 1309 return nil 1310 } 1311 1312 func (s *Sharing) dissociateFile(inst *instance.Instance, olddoc, newdoc *vfs.FileDoc) error { 1313 fs := inst.VFS() 1314 1315 newdoc.SetID("") 1316 newdoc.SetRev("") 1317 if err := fs.DissociateFile(olddoc, newdoc); err != nil { 1318 newdoc.DocName = conflictName(fs, newdoc.DirID, newdoc.DocName, true) 1319 newdoc.ResetFullpath() 1320 if err := fs.DissociateFile(olddoc, newdoc); err != nil { 1321 return err 1322 } 1323 } 1324 1325 sid := olddoc.DocType() + "/" + olddoc.ID() 1326 var ref SharedRef 1327 if err := couchdb.GetDoc(inst, consts.Shared, sid, &ref); err != nil { 1328 if couchdb.IsNotFoundError(err) { 1329 return nil 1330 } 1331 return err 1332 } 1333 if !s.Owner { 1334 return couchdb.DeleteDoc(inst, &ref) 1335 } 1336 ref.Revisions.Add(olddoc.Rev()) 1337 ref.Infos[s.SID] = SharedInfo{ 1338 Rule: ref.Infos[s.SID].Rule, 1339 Binary: false, 1340 Removed: true, 1341 Dissociated: true, 1342 } 1343 return couchdb.UpdateDoc(inst, &ref) 1344 } 1345 1346 func dirToJSONDoc(dir *vfs.DirDoc, instanceURL string) couchdb.JSONDoc { 1347 doc := couchdb.JSONDoc{ 1348 Type: consts.Files, 1349 M: map[string]interface{}{ 1350 "type": dir.Type, 1351 "_id": dir.DocID, 1352 "_rev": dir.DocRev, 1353 "name": dir.DocName, 1354 "created_at": dir.CreatedAt, 1355 "updated_at": dir.UpdatedAt, 1356 "tags": dir.Tags, 1357 "path": dir.Fullpath, 1358 couchdb.SelectorReferencedBy: dir.ReferencedBy, 1359 }, 1360 } 1361 if dir.DirID != "" { 1362 doc.M["dir_id"] = dir.DirID 1363 } 1364 if dir.RestorePath != "" { 1365 doc.M["restore_path"] = dir.RestorePath 1366 } 1367 if len(dir.Metadata) > 0 { 1368 doc.M["metadata"] = dir.Metadata.RemoveCertifiedMetadata() 1369 } 1370 fcm := dir.CozyMetadata 1371 if fcm == nil { 1372 fcm = vfs.NewCozyMetadata(instanceURL) 1373 fcm.CreatedAt = dir.CreatedAt 1374 fcm.UpdatedAt = dir.UpdatedAt 1375 } 1376 doc.M["cozyMetadata"] = fcm.ToJSONDoc() 1377 return doc 1378 } 1379 1380 func fileToJSONDoc(file *vfs.FileDoc, instanceURL string) couchdb.JSONDoc { 1381 doc := couchdb.JSONDoc{ 1382 Type: consts.Files, 1383 M: map[string]interface{}{ 1384 "type": file.Type, 1385 "_id": file.DocID, 1386 "_rev": file.DocRev, 1387 "name": file.DocName, 1388 "created_at": file.CreatedAt, 1389 "updated_at": file.UpdatedAt, 1390 "size": file.ByteSize, 1391 "md5sum": file.MD5Sum, 1392 "mime": file.Mime, 1393 "class": file.Class, 1394 "executable": file.Executable, 1395 "trashed": file.Trashed, 1396 "tags": file.Tags, 1397 couchdb.SelectorReferencedBy: file.ReferencedBy, 1398 }, 1399 } 1400 if file.DirID != "" { 1401 doc.M["dir_id"] = file.DirID 1402 } 1403 if file.RestorePath != "" { 1404 doc.M["restore_path"] = file.RestorePath 1405 } 1406 if len(file.Metadata) > 0 { 1407 doc.M["metadata"] = file.Metadata.RemoveCertifiedMetadata() 1408 } 1409 fcm := file.CozyMetadata 1410 if fcm == nil { 1411 fcm = vfs.NewCozyMetadata(instanceURL) 1412 fcm.CreatedAt = file.CreatedAt 1413 fcm.UpdatedAt = file.UpdatedAt 1414 uploadedAt := file.CreatedAt 1415 fcm.UploadedAt = &uploadedAt 1416 fcm.UploadedOn = instanceURL 1417 } 1418 doc.M["cozyMetadata"] = fcm.ToJSONDoc() 1419 return doc 1420 }