github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/upload.go (about) 1 package sharing 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "strings" 14 15 "github.com/cozy/cozy-stack/client/request" 16 "github.com/cozy/cozy-stack/model/instance" 17 "github.com/cozy/cozy-stack/model/instance/lifecycle" 18 "github.com/cozy/cozy-stack/model/vfs" 19 "github.com/cozy/cozy-stack/pkg/config/config" 20 "github.com/cozy/cozy-stack/pkg/consts" 21 "github.com/cozy/cozy-stack/pkg/couchdb" 22 "github.com/cozy/cozy-stack/pkg/realtime" 23 "github.com/labstack/echo/v4" 24 "golang.org/x/sync/errgroup" 25 ) 26 27 // UploadMsg is used for jobs on the share-upload worker. 28 type UploadMsg struct { 29 SharingID string `json:"sharing_id"` 30 Errors int `json:"errors"` 31 } 32 33 // fileCreatorWithContent is a function that can be used to create a file in 34 // the given VFS. The content comes from the function closure. 35 type fileCreatorWithContent func(fs vfs.VFS, newdoc, olddoc *vfs.FileDoc) error 36 37 // Upload starts uploading files for this sharing 38 func (s *Sharing) Upload(inst *instance.Instance, ctx context.Context, errors int) error { 39 mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID+"/upload") 40 if err := mu.Lock(); err != nil { 41 return err 42 } 43 defer mu.Unlock() 44 45 var errm error 46 var members []*Member 47 if !s.Owner { 48 members = append(members, &s.Members[0]) 49 } else { 50 for i, m := range s.Members { 51 if i == 0 { 52 continue 53 } 54 if m.Status == MemberStatusReady { 55 members = append(members, &s.Members[i]) 56 } 57 } 58 } 59 60 lastTry := errors+1 == MaxRetries 61 done := true 62 g, _ := errgroup.WithContext(context.Background()) 63 for i := range members { 64 m := members[i] 65 g.Go(func() error { 66 more, err := s.UploadBatchTo(inst, ctx, m, lastTry) 67 if err != nil { 68 return err 69 } 70 if more { 71 done = false 72 } 73 return nil 74 }) 75 } 76 err := g.Wait() 77 78 if err != nil { 79 s.retryWorker(inst, "share-upload", errors) 80 inst.Logger().WithNamespace("upload").Infof("err=%s\n", err) 81 } else if !done { 82 s.pushJob(inst, "share-upload") 83 } 84 return errm 85 } 86 87 // InitialUpload uploads files to just a member, for the first time 88 func (s *Sharing) InitialUpload(inst *instance.Instance, m *Member) error { 89 mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID+"/upload") 90 if err := mu.Lock(); err != nil { 91 return err 92 } 93 defer mu.Unlock() 94 95 ctx := context.Background() 96 more, err := s.UploadBatchTo(inst, ctx, m, false) 97 if err != nil { 98 return err 99 } 100 if !more { 101 return s.sendInitialEndNotif(inst, m) 102 } 103 104 s.pushJob(inst, "share-upload") 105 return nil 106 } 107 108 // sendInitialEndNotif sends a notification to the recipient that the initial 109 // sync is finished 110 func (s *Sharing) sendInitialEndNotif(inst *instance.Instance, m *Member) error { 111 u, err := url.Parse(m.Instance) 112 if err != nil { 113 return err 114 } 115 c := s.FindCredentials(m) 116 if c == nil || c.AccessToken == nil { 117 return ErrInvalidSharing 118 } 119 opts := &request.Options{ 120 Method: http.MethodDelete, 121 Scheme: u.Scheme, 122 Domain: u.Host, 123 Path: fmt.Sprintf("/sharings/%s/initial", s.SID), 124 Headers: request.Headers{ 125 echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken, 126 }, 127 } 128 res, err := request.Req(opts) 129 if err != nil { 130 return err 131 } 132 res.Body.Close() 133 return nil 134 } 135 136 // UploadBatchTo uploads a batch of files to the given member. It returns false 137 // if there are no more files to upload to this member currently. 138 func (s *Sharing) UploadBatchTo( 139 inst *instance.Instance, 140 ctx context.Context, 141 m *Member, 142 lastTry bool, 143 ) (bool, error) { 144 if m.Instance == "" { 145 return false, ErrInvalidURL 146 } 147 creds := s.FindCredentials(m) 148 if creds == nil { 149 return false, ErrInvalidSharing 150 } 151 152 lastSeq, err := s.getLastSeqNumber(inst, m, "upload") 153 if err != nil { 154 return false, err 155 } 156 inst.Logger().WithNamespace("upload").Debugf("lastSeq = %s", lastSeq) 157 158 batch := &batchUpload{ 159 Sharing: s, 160 Instance: inst, 161 CommitedSeq: lastSeq, 162 } 163 defer func() { 164 if batch.CommitedSeq != lastSeq { 165 _ = s.UpdateLastSequenceNumber(inst, m, "upload", batch.CommitedSeq) 166 } 167 }() 168 169 for i := 0; i < BatchSize; i++ { 170 if ctx.Err() == context.Canceled { 171 return true, nil 172 } 173 file, ruleIndex, err := batch.findNextFileToUpload() 174 if err != nil { 175 return false, err 176 } 177 if file == nil { 178 return false, nil 179 } 180 if err = s.uploadFile(inst, m, file, ruleIndex); err != nil { 181 return false, err 182 } 183 batch.CommitedSeq = batch.CandidateSeq 184 } 185 return true, nil 186 } 187 188 type batchUpload struct { 189 Sharing *Sharing 190 Instance *instance.Instance 191 CandidateSeq string // The sequence number for the next file to try to upload 192 CommitedSeq string // The sequence number for the last successfully uploaded file 193 194 // changes is used to batch calls to the changes feed and improves 195 // performances. 196 changes []couchdb.Change 197 } 198 199 // findNextFileToUpload uses the changes feed to find the next file that needs 200 // to be uploaded. It returns a file document if there is one file to upload, 201 // and the index of the sharing rule that applies to this file. 202 func (b *batchUpload) findNextFileToUpload() (map[string]interface{}, int, error) { 203 seq := b.CommitedSeq 204 for { 205 if len(b.changes) == 0 { 206 response, err := couchdb.GetChanges(b.Instance, &couchdb.ChangesRequest{ 207 DocType: consts.Shared, 208 IncludeDocs: true, 209 Since: seq, 210 Limit: BatchSize, 211 }) 212 if err != nil { 213 return nil, 0, err 214 } 215 if len(response.Results) == 0 { 216 return nil, 0, nil 217 } 218 b.changes = response.Results 219 } 220 change := b.changes[0] 221 b.changes = b.changes[1:] 222 b.CandidateSeq = change.Seq 223 seq = change.Seq 224 infos, ok := change.Doc.Get("infos").(map[string]interface{}) 225 if !ok { 226 continue 227 } 228 info, ok := infos[b.Sharing.SID].(map[string]interface{}) 229 if !ok { 230 continue 231 } 232 if _, ok = info["binary"]; !ok { 233 continue 234 } 235 if _, ok = info["removed"]; ok { 236 continue 237 } 238 idx, ok := info["rule"].(float64) 239 if !ok { 240 continue 241 } 242 rev := extractLastRevision(change.Doc) 243 if rev == "" { 244 continue 245 } 246 docID := strings.SplitN(change.DocID, "/", 2)[1] 247 ir := couchdb.IDRev{ID: docID, Rev: rev} 248 query := []couchdb.IDRev{ir} 249 results, err := couchdb.BulkGetDocs(b.Instance, consts.Files, query) 250 if err != nil { 251 if couchdb.IsDeletedError(err) { 252 continue 253 } 254 return nil, 0, err 255 } 256 if len(results) == 0 { 257 b.Instance.Logger().WithNamespace("upload"). 258 Warnf("missing results for bulk get %v", query) 259 continue 260 } 261 if results[0]["_deleted"] == true { 262 b.Instance.Logger().WithNamespace("upload"). 263 Warnf("cannot upload _deleted file %v", results[0]) 264 return nil, 0, ErrInternalServerError 265 } 266 return results[0], int(idx), nil 267 } 268 } 269 270 // uploadFile uploads one file to the given member. It first try to just send 271 // the metadata, and if it is not enough, it also send the binary. 272 func (s *Sharing) uploadFile(inst *instance.Instance, m *Member, file map[string]interface{}, ruleIndex int) error { 273 inst.Logger().WithNamespace("upload").Debugf("going to upload %#v", file) 274 275 // Do not try to send a trashed file, the trash status will be synchronized 276 // via the CouchDB replication protocol 277 if trashed, _ := file["trashed"].(bool); trashed { 278 return nil 279 } 280 281 creds := s.FindCredentials(m) 282 if creds == nil { 283 return ErrInvalidSharing 284 } 285 u, err := url.Parse(m.Instance) 286 if err != nil { 287 return err 288 } 289 origFileID := file["_id"].(string) 290 origFileRev := file["_rev"].(string) 291 s.TransformFileToSent(file, creds.XorKey, ruleIndex) 292 xoredFileID := file["_id"].(string) 293 body, err := json.Marshal(file) 294 if err != nil { 295 return err 296 } 297 opts := &request.Options{ 298 Method: http.MethodPut, 299 Scheme: u.Scheme, 300 Domain: u.Host, 301 Path: "/sharings/" + s.SID + "/io.cozy.files/" + xoredFileID + "/metadata", 302 Queries: url.Values{"from": {inst.ContextualDomain()}}, 303 Headers: request.Headers{ 304 echo.HeaderAccept: echo.MIMEApplicationJSON, 305 echo.HeaderContentType: echo.MIMEApplicationJSON, 306 echo.HeaderAuthorization: "Bearer " + creds.AccessToken.AccessToken, 307 }, 308 Body: bytes.NewReader(body), 309 ParseError: ParseRequestError, 310 } 311 var res *http.Response 312 res, err = request.Req(opts) 313 if res != nil && res.StatusCode/100 == 4 { 314 res, err = RefreshToken(inst, err, s, m, creds, opts, body) 315 } 316 if err != nil { 317 if res != nil && res.StatusCode/100 == 5 { 318 inst.Logger().WithNamespace("upload"). 319 Warnf("%s got response %d", opts.Path, res.StatusCode) 320 return ErrInternalServerError 321 } 322 return err 323 } 324 defer res.Body.Close() 325 326 if res.StatusCode == 204 { 327 return nil 328 } 329 var resBody KeyToUpload 330 if err = json.NewDecoder(res.Body).Decode(&resBody); err != nil { 331 return err 332 } 333 334 fs := inst.VFS() 335 fileDoc, err := fs.FileByID(origFileID) 336 if err != nil { 337 return err 338 } 339 // If the wrong revision is returned, we should abort and retry later. It 340 // can be caused by CouchDB eventual consistency, or by a change between 341 // when the changes feed was fetched and when the file is loaded. 342 if fileDoc.Rev() != origFileRev { 343 return ErrInternalServerError 344 } 345 346 dstInstance, err := lifecycle.GetInstance(m.InstanceHost()) 347 if err == nil && onSameStack(inst, dstInstance) { 348 err := s.optimizedUploadFile(inst, dstInstance, m, fileDoc, file, resBody) 349 if err != nil { 350 inst.Logger().WithNamespace("upload"). 351 Warnf("optimizedUploadFile failed to upload %s to %s (%s): %s", origFileID, m.Instance, s.ID(), err) 352 } 353 return err 354 } 355 356 content, err := fs.OpenFile(fileDoc) 357 if err != nil { 358 return err 359 } 360 defer content.Close() 361 362 opts2 := &request.Options{ 363 Method: http.MethodPut, 364 Scheme: u.Scheme, 365 Domain: u.Host, 366 Path: "/sharings/" + s.SID + "/io.cozy.files/" + resBody.Key, 367 Queries: url.Values{"from": {inst.ContextualDomain()}}, 368 Headers: request.Headers{ 369 echo.HeaderContentType: fileDoc.Mime, 370 echo.HeaderAuthorization: "Bearer " + creds.AccessToken.AccessToken, 371 }, 372 Body: content, 373 Client: http.DefaultClient, 374 } 375 res2, err := request.Req(opts2) 376 if err != nil { 377 if res2 != nil && res2.StatusCode/100 == 5 { 378 inst.Logger().WithNamespace("upload"). 379 Warnf("%s got response %d", opts2.Path, res2.StatusCode) 380 return ErrInternalServerError 381 } 382 return err 383 } 384 res2.Body.Close() 385 return nil 386 } 387 388 func onSameStack(src, dst *instance.Instance) bool { 389 var srcPort, dstPort string 390 parts := strings.SplitN(src.Domain, ":", 2) 391 if len(parts) > 1 { 392 srcPort = parts[1] 393 } 394 parts = strings.SplitN(dst.Domain, ":", 2) 395 if len(parts) > 1 { 396 dstPort = parts[1] 397 } 398 return srcPort == dstPort 399 } 400 401 func (s *Sharing) optimizedUploadFile( 402 srcInstance, dstInstance *instance.Instance, 403 m *Member, 404 srcFile *vfs.FileDoc, 405 dstFile map[string]interface{}, 406 key KeyToUpload, 407 ) error { 408 srcInstance.Logger().WithNamespace("upload"). 409 Debugf("optimizedUploadFile %s to %s (%s)", srcFile.ID(), m.Instance, s.ID()) 410 411 create := func(fs vfs.VFS, newdoc, olddoc *vfs.FileDoc) error { 412 return fs.CopyFileFromOtherFS(newdoc, olddoc, srcInstance.VFS(), srcFile) 413 } 414 415 dstSharing, err := FindSharing(dstInstance, s.ID()) 416 if err != nil { 417 return err 418 } 419 if !dstSharing.Active { 420 return ErrInvalidSharing 421 } 422 return dstSharing.HandleFileUpload(dstInstance, key.Key, create) 423 } 424 425 // FileDocWithRevisions is the struct of the payload for synchronizing a file 426 type FileDocWithRevisions struct { 427 *vfs.FileDoc 428 Revisions RevsStruct `json:"_revisions"` 429 } 430 431 // Clone is part of the couchdb.Doc interface 432 func (f *FileDocWithRevisions) Clone() couchdb.Doc { 433 panic("FileDocWithRevisions must not be cloned") 434 } 435 436 // KeyToUpload contains the key for uploading a file (when syncing metadata is 437 // not enough) 438 type KeyToUpload struct { 439 Key string `json:"key"` 440 } 441 442 func (s *Sharing) createUploadKey(inst *instance.Instance, target *FileDocWithRevisions) (*KeyToUpload, error) { 443 key, err := getStore().Save(inst, target) 444 if err != nil { 445 return nil, err 446 } 447 return &KeyToUpload{Key: key}, nil 448 } 449 450 // SyncFile tries to synchronize a file with just the metadata. If it can't, 451 // it will return a key to upload the content. 452 func (s *Sharing) SyncFile(inst *instance.Instance, target *FileDocWithRevisions) (*KeyToUpload, error) { 453 inst.Logger().WithNamespace("upload").Debugf("SyncFile %#v", target) 454 455 if len(target.MD5Sum) == 0 { 456 return nil, vfs.ErrInvalidHash 457 } 458 sid := consts.Files + "/" + target.DocID 459 mu := config.Lock().ReadWrite(inst, "shared/"+sid) 460 if err := mu.Lock(); err != nil { 461 return nil, err 462 } 463 defer mu.Unlock() 464 var ref SharedRef 465 current, err := inst.VFS().FileByID(target.DocID) 466 if err != nil { 467 if errors.Is(err, os.ErrNotExist) { 468 // XXX Even if the file does not exist, it may have existed in the 469 // past and have been disociated. In that case, we need to check 470 // that what we received is not just the echo, or we will recreate 471 // a deleted file for no reason. 472 err = couchdb.GetDoc(inst, consts.Shared, sid, &ref) 473 if err == nil { 474 if sub, _ := ref.Revisions.Find(target.DocRev); sub != nil { 475 // It's just the echo, there is nothing to do 476 return nil, nil 477 } 478 } else if !couchdb.IsNotFoundError(err) { 479 return nil, err 480 } 481 if rule, _ := s.findRuleForNewFile(target.FileDoc); rule == nil { 482 return nil, ErrSafety 483 } 484 return s.createUploadKey(inst, target) 485 } 486 return nil, err 487 } 488 489 err = couchdb.GetDoc(inst, consts.Shared, sid, &ref) 490 if err != nil { 491 if !couchdb.IsNotFoundError(err) { 492 return nil, err 493 } 494 // XXX It happens that the job for creating the io.cozy.shared has 495 // been lost (stack restart for example), and we need to do 496 // something to avoid having the sharing stuck. The most efficient 497 // way to do that is to check that the file is actually in the 498 // sharing directory, and if it is the case, to create the missing 499 // io.cozy.shared. 500 ref, err = s.fixMissingShared(inst, current) 501 if err != nil { 502 return nil, ErrSafety 503 } 504 } 505 if infos, ok := ref.Infos[s.SID]; !ok || (infos.Removed && !infos.Dissociated) { 506 return nil, ErrSafety 507 } 508 if sub, _ := ref.Revisions.Find(target.DocRev); sub != nil { 509 // It's just the echo, there is nothing to do 510 return nil, nil 511 } 512 if !bytes.Equal(target.MD5Sum, current.MD5Sum) { 513 return s.createUploadKey(inst, target) 514 } 515 return nil, s.updateFileMetadata(inst, target, current, &ref) 516 } 517 518 // prepareFileWithAncestors find the parent directory for file, and recreates it 519 // if it is missing. 520 func (s *Sharing) prepareFileWithAncestors(inst *instance.Instance, newdoc *vfs.FileDoc, dirID string) error { 521 // Case 1: there is a rule for sharing this file 522 if s.hasExplicitRuleForFile(newdoc) { 523 return nil 524 } 525 526 // Case 2: the file is in a directory that is shared 527 if dirID == "" { 528 parent, err := s.GetSharingDir(inst) 529 if err != nil { 530 return err 531 } 532 newdoc.DirID = parent.DocID 533 } else if dirID != newdoc.DirID { 534 parent, err := inst.VFS().DirByID(dirID) 535 if errors.Is(err, os.ErrNotExist) { 536 parent, err = s.recreateParent(inst, dirID) 537 } 538 if err != nil { 539 inst.Logger().WithNamespace("upload"). 540 Debugf("Conflict for parent on sync file: %s", err) 541 return err 542 } 543 newdoc.DirID = parent.DocID 544 } 545 return nil 546 } 547 548 // updateFileMetadata updates a file document when only some metadata has 549 // changed, but not the content. 550 func (s *Sharing) updateFileMetadata(inst *instance.Instance, target *FileDocWithRevisions, newdoc *vfs.FileDoc, ref *SharedRef) error { 551 indexer := newSharingIndexer(inst, &bulkRevs{ 552 Rev: target.DocRev, 553 Revisions: target.Revisions, 554 }, ref) 555 556 chain := revsStructToChain(target.Revisions) 557 conflict := detectConflict(newdoc.DocRev, chain) 558 switch conflict { 559 case LostConflict: 560 return nil 561 case WonConflict: 562 indexer.WillResolveConflict(newdoc.DocRev, chain) 563 case NoConflict: 564 // Nothing to do 565 } 566 567 fs := inst.VFS().UseSharingIndexer(indexer) 568 olddoc := newdoc.Clone().(*vfs.FileDoc) 569 newdoc.DocName = target.DocName 570 if err := s.prepareFileWithAncestors(inst, newdoc, target.DirID); err != nil { 571 return err 572 } 573 newdoc.ResetFullpath() 574 copySafeFieldsToFile(target.FileDoc, newdoc) 575 infos := ref.Infos[s.SID] 576 rule := &s.Rules[infos.Rule] 577 newdoc.ReferencedBy = buildReferencedBy(target.FileDoc, newdoc, rule) 578 579 err := fs.UpdateFileDoc(olddoc, newdoc) 580 if errors.Is(err, os.ErrExist) { 581 pth, errp := newdoc.Path(fs) 582 if errp != nil { 583 return errp 584 } 585 name, errr := s.resolveConflictSamePath(inst, newdoc.DocID, pth) 586 if errr != nil { 587 return errr 588 } 589 if name != "" { 590 indexer.IncrementRevision() 591 newdoc.DocName = name 592 newdoc.ResetFullpath() 593 } 594 err = fs.UpdateFileDoc(olddoc, newdoc) 595 } 596 if err != nil { 597 inst.Logger().WithNamespace("upload"). 598 Debugf("Cannot update file: %s", err) 599 return err 600 } 601 return nil 602 } 603 604 // HandleFileUpload is used to receive a file upload when synchronizing just 605 // the metadata was not enough. 606 func (s *Sharing) HandleFileUpload(inst *instance.Instance, key string, create fileCreatorWithContent) error { 607 target, err := getStore().Get(inst, key) 608 if err != nil { 609 return err 610 } 611 if target == nil { 612 return ErrMissingFileMetadata 613 } 614 inst.Logger().WithNamespace("upload").Debugf("HandleFileUpload %#v %#v", target.FileDoc, target.Revisions) 615 sid := consts.Files + "/" + target.DocID 616 mu := config.Lock().ReadWrite(inst, "shared/"+sid) 617 if err = mu.Lock(); err != nil { 618 return err 619 } 620 defer mu.Unlock() 621 622 current, err := inst.VFS().FileByID(target.DocID) 623 if err != nil && !errors.Is(err, os.ErrNotExist) { 624 inst.Logger().WithNamespace("upload"). 625 Warnf("Upload has failed: %s", err) 626 return err 627 } 628 629 if current == nil { 630 return s.UploadNewFile(inst, target, create) 631 } 632 return s.UploadExistingFile(inst, target, current, create) 633 } 634 635 // UploadNewFile is used to receive a new file. 636 func (s *Sharing) UploadNewFile( 637 inst *instance.Instance, 638 target *FileDocWithRevisions, 639 create fileCreatorWithContent, 640 ) error { 641 inst.Logger().WithNamespace("upload").Debugf("UploadNewFile") 642 ref := SharedRef{ 643 Infos: make(map[string]SharedInfo), 644 } 645 indexer := newSharingIndexer(inst, &bulkRevs{ 646 Rev: target.Rev(), 647 Revisions: target.Revisions, 648 }, &ref) 649 fs := inst.VFS().UseSharingIndexer(indexer) 650 651 rule, ruleIndex := s.findRuleForNewFile(target.FileDoc) 652 if rule == nil { 653 return ErrSafety 654 } 655 656 var err error 657 var parent *vfs.DirDoc 658 var addReferencedBy bool 659 if target.DirID != "" { 660 parent, err = fs.DirByID(target.DirID) 661 if errors.Is(err, os.ErrNotExist) { 662 parent, err = s.recreateParent(inst, target.DirID) 663 } 664 if err != nil { 665 inst.Logger().WithNamespace("upload"). 666 Infof("Conflict for parent on file upload: %s", err) 667 } 668 } else if target.DocID == rule.Values[0] { 669 parentID := s.cleanShortcutID(inst) 670 if parentID != "" { 671 parent, err = fs.DirByID(parentID) 672 } else { 673 parent, err = EnsureSharedWithMeDir(inst) 674 } 675 addReferencedBy = true 676 } else { 677 parent, err = s.GetSharingDir(inst) 678 } 679 if err != nil { 680 return err 681 } 682 683 // XXX In some cases, we have to add a fake revision to the created file 684 // because it was already known by CouchDB. For example: 685 // - Alice has a shared folder with Bob 686 // - inside this folder, there is a directory named foo 687 // - there is a file named bar inside foo, at the revision 1-123 688 // - Alice moves foo outside of the sharing 689 // - the sharing replication deletes bar on Bob's instance (revision 2-456) 690 // - later, Alice moves again foo inside the sharing (with bar) 691 // - we are in this function for bar, and if we try to recreate the file 692 // with revision 1-123, it will still be seen as deleted by CouchDB 693 // (revision 2-456 wins) => so, we create a revision 2-789. 694 var fake couchdb.JSONDoc 695 if err := couchdb.GetDoc(inst, consts.Files, target.DocID, &fake); couchdb.IsDeletedError(err) { 696 indexer.IncrementRevision() 697 } 698 699 newdoc, err := vfs.NewFileDoc(target.DocName, parent.DocID, target.Size(), target.MD5Sum, 700 target.Mime, target.Class, target.CreatedAt, target.Executable, false, false, target.Tags) 701 if err != nil { 702 return err 703 } 704 newdoc.SetID(target.DocID) 705 ref.SID = consts.Files + "/" + newdoc.DocID 706 copySafeFieldsToFile(target.FileDoc, newdoc) 707 708 ref.Infos[s.SID] = SharedInfo{Rule: ruleIndex, Binary: true} 709 newdoc.ReferencedBy = buildReferencedBy(target.FileDoc, nil, rule) 710 if addReferencedBy { 711 ref := couchdb.DocReference{ 712 ID: s.SID, 713 Type: consts.Sharings, 714 } 715 newdoc.ReferencedBy = append(newdoc.ReferencedBy, ref) 716 } 717 718 err = create(fs, newdoc, nil) 719 if errors.Is(err, os.ErrExist) { 720 pth, errp := newdoc.Path(fs) 721 if errp != nil { 722 return errp 723 } 724 name, errr := s.resolveConflictSamePath(inst, newdoc.DocID, pth) 725 if errr != nil { 726 return errr 727 } 728 if name != "" { 729 indexer.IncrementRevision() 730 newdoc.DocName = name 731 newdoc.ResetFullpath() 732 } 733 err = create(fs, newdoc, nil) 734 } 735 if err != nil { 736 inst.Logger().WithNamespace("upload"). 737 Debugf("Cannot create file: %s", err) 738 return err 739 } 740 if s.NbFiles > 0 { 741 s.countReceivedFiles(inst) 742 } 743 return nil 744 } 745 746 // countReceivedFiles counts the number of files received during the initial 747 // sync, and pushs an event to the real-time system with this count 748 func (s *Sharing) countReceivedFiles(inst *instance.Instance) { 749 count := 0 750 req := &couchdb.ViewRequest{ 751 Key: s.SID, 752 IncludeDocs: true, 753 } 754 var res couchdb.ViewResponse 755 err := couchdb.ExecView(inst, couchdb.SharedDocsBySharingID, req, &res) 756 if err == nil { 757 for _, row := range res.Rows { 758 var doc SharedRef 759 if err = json.Unmarshal(row.Doc, &doc); err != nil { 760 continue 761 } 762 if doc.Infos[s.SID].Binary { 763 count++ 764 } 765 } 766 } 767 768 if count >= s.NbFiles { 769 if err = s.EndInitial(inst); err != nil { 770 inst.Logger().WithNamespace("sharing"). 771 Errorf("Can't save sharing %v: %s", s, err) 772 } 773 return 774 } 775 776 doc := couchdb.JSONDoc{ 777 Type: consts.SharingsInitialSync, 778 M: map[string]interface{}{ 779 "_id": s.SID, 780 "count": count, 781 }, 782 } 783 realtime.GetHub().Publish(inst, realtime.EventUpdate, &doc, nil) 784 } 785 786 // UploadExistingFile is used to receive new content for an existing file. 787 // 788 // Note: if file was renamed + its content has changed, we modify the content 789 // first, then rename it, not trying to do both at the same time. We do it in 790 // this order because the difficult case is if one operation succeeds and the 791 // other fails (if the two succeeds, we are fine; if the two fails, we just 792 // retry), and in that case, it is easier to manage a conflict on dir_id+name 793 // than on content: a conflict on different content is resolved by a copy of 794 // the file (which is not what we want), a conflict of name+dir_id, the higher 795 // revision wins and it should be the good one in our case. 796 func (s *Sharing) UploadExistingFile( 797 inst *instance.Instance, 798 target *FileDocWithRevisions, 799 newdoc *vfs.FileDoc, 800 create fileCreatorWithContent, 801 ) error { 802 inst.Logger().WithNamespace("upload").Debugf("UploadExistingFile") 803 var ref SharedRef 804 err := couchdb.GetDoc(inst, consts.Shared, consts.Files+"/"+target.DocID, &ref) 805 if err != nil { 806 if couchdb.IsNotFoundError(err) { 807 return ErrSafety 808 } 809 return err 810 } 811 indexer := newSharingIndexer(inst, &bulkRevs{ 812 Rev: target.Rev(), 813 Revisions: target.Revisions, 814 }, &ref) 815 fs := inst.VFS().UseSharingIndexer(indexer) 816 olddoc := newdoc.Clone().(*vfs.FileDoc) 817 818 infos, ok := ref.Infos[s.SID] 819 if !ok || (infos.Removed && !infos.Dissociated) { 820 return ErrSafety 821 } 822 rule := &s.Rules[infos.Rule] 823 newdoc.ReferencedBy = buildReferencedBy(target.FileDoc, olddoc, rule) 824 copySafeFieldsToFile(target.FileDoc, newdoc) 825 newdoc.DocName = target.DocName 826 if err := s.prepareFileWithAncestors(inst, newdoc, target.DirID); err != nil { 827 return err 828 } 829 newdoc.ResetFullpath() 830 newdoc.ByteSize = target.ByteSize 831 newdoc.MD5Sum = target.MD5Sum 832 833 chain := revsStructToChain(target.Revisions) 834 conflict := detectConflict(newdoc.DocRev, chain) 835 switch conflict { 836 case LostConflict: 837 return s.uploadLostConflict(inst, target, newdoc, create) 838 case WonConflict: 839 if err = s.uploadWonConflict(inst, olddoc); err != nil { 840 return err 841 } 842 case NoConflict: 843 // Nothing to do 844 } 845 indexer.WillResolveConflict(newdoc.DocRev, chain) 846 847 // Easy case: only the content has changed, not its path 848 if newdoc.DocName == olddoc.DocName && newdoc.DirID == olddoc.DirID { 849 return create(fs, newdoc, olddoc) 850 } 851 852 stash := indexer.StashRevision(false) 853 tmpdoc := newdoc.Clone().(*vfs.FileDoc) 854 tmpdoc.DocName = olddoc.DocName 855 tmpdoc.DirID = olddoc.DirID 856 tmpdoc.ResetFullpath() 857 if err := create(fs, tmpdoc, olddoc); err != nil { 858 return err 859 } 860 861 indexer.UnstashRevision(stash) 862 newdoc.DocRev = tmpdoc.DocRev 863 newdoc.InternalID = tmpdoc.InternalID 864 err = fs.UpdateFileDoc(tmpdoc, newdoc) 865 if errors.Is(err, os.ErrExist) { 866 pth, errp := newdoc.Path(fs) 867 if errp != nil { 868 return errp 869 } 870 name, errr := s.resolveConflictSamePath(inst, newdoc.DocID, pth) 871 if errr != nil { 872 return errr 873 } 874 if name != "" { 875 indexer.IncrementRevision() 876 newdoc.DocName = name 877 newdoc.ResetFullpath() 878 } 879 err = fs.UpdateFileDoc(tmpdoc, newdoc) 880 } 881 return err 882 } 883 884 // uploadLostConflict manages an upload where a file is in conflict, and the 885 // uploaded file version goes to a new file. 886 func (s *Sharing) uploadLostConflict( 887 inst *instance.Instance, 888 target *FileDocWithRevisions, 889 newdoc *vfs.FileDoc, 890 create fileCreatorWithContent, 891 ) error { 892 rev := target.Rev() 893 inst.Logger().WithNamespace("upload").Debugf("uploadLostConflict %s", rev) 894 indexer := newSharingIndexer(inst, &bulkRevs{ 895 Rev: rev, 896 Revisions: revsChainToStruct([]string{rev}), 897 }, nil) 898 fs := inst.VFS().UseSharingIndexer(indexer) 899 newdoc.DocID = conflictID(newdoc.DocID, rev) 900 if _, err := fs.FileByID(newdoc.DocID); !errors.Is(err, os.ErrNotExist) { 901 return err 902 } 903 newdoc.DocName = conflictName(indexer, newdoc.DirID, newdoc.DocName, true) 904 newdoc.DocRev = "" 905 newdoc.ResetFullpath() 906 if err := create(fs, newdoc, nil); err != nil { 907 inst.Logger().WithNamespace("upload").Debugf("1. loser = %#v", newdoc) 908 return err 909 } 910 return nil 911 } 912 913 // uploadWonConflict manages an upload where a file is in conflict, and the 914 // existing file is copied to a new file to let the upload succeed. 915 func (s *Sharing) uploadWonConflict(inst *instance.Instance, src *vfs.FileDoc) error { 916 rev := src.Rev() 917 inst.Logger().WithNamespace("upload").Debugf("uploadWonConflict %s", rev) 918 indexer := newSharingIndexer(inst, &bulkRevs{ 919 Rev: rev, 920 Revisions: revsChainToStruct([]string{rev}), 921 }, nil) 922 fs := inst.VFS().UseSharingIndexer(indexer) 923 dst := src.Clone().(*vfs.FileDoc) 924 dst.DocID = conflictID(dst.DocID, rev) 925 if _, err := fs.FileByID(dst.DocID); !errors.Is(err, os.ErrNotExist) { 926 return err 927 } 928 dst.DocName = conflictName(indexer, dst.DirID, dst.DocName, true) 929 dst.ResetFullpath() 930 content, err := fs.OpenFile(src) 931 if err != nil { 932 return err 933 } 934 defer content.Close() 935 file, err := fs.CreateFile(dst, nil) 936 if err != nil { 937 return err 938 } 939 inst.Logger().WithNamespace("upload").Debugf("2. loser = %#v", dst) 940 return copyFileContent(inst, file, content) 941 } 942 943 // copyFileContent will copy the body of the HTTP request to the file, and 944 // close the file descriptor at the end. 945 func copyFileContent(inst *instance.Instance, file vfs.File, body io.ReadCloser) error { 946 _, err := io.Copy(file, body) 947 if cerr := file.Close(); cerr != nil && err == nil { 948 err = cerr 949 inst.Logger().WithNamespace("upload"). 950 Infof("Cannot close file descriptor: %s", err) 951 } 952 return err 953 }