github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/replicator.go (about) 1 package sharing 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net/http" 10 "net/url" 11 "strings" 12 "time" 13 14 "github.com/cozy/cozy-stack/client/request" 15 "github.com/cozy/cozy-stack/model/bitwarden/settings" 16 "github.com/cozy/cozy-stack/model/instance" 17 "github.com/cozy/cozy-stack/model/job" 18 "github.com/cozy/cozy-stack/pkg/config/config" 19 "github.com/cozy/cozy-stack/pkg/consts" 20 "github.com/cozy/cozy-stack/pkg/couchdb" 21 "github.com/cozy/cozy-stack/pkg/couchdb/revision" 22 "github.com/cozy/cozy-stack/pkg/realtime" 23 "github.com/cozy/cozy-stack/pkg/safehttp" 24 "github.com/labstack/echo/v4" 25 "golang.org/x/sync/errgroup" 26 ) 27 28 // MaxRetries is the maximal number of retries for a replicator 29 const MaxRetries = 5 30 31 // InitialBackoffPeriod is the initial duration to wait for the first retry 32 // (each next retry will wait 4 times longer than its previous retry) 33 const InitialBackoffPeriod = 1 * time.Minute 34 35 // BatchSize is the maximal number of documents manipulated at once by the 36 // replicator 37 const BatchSize = 400 38 39 // ReplicateMsg is used for jobs on the share-replicate worker. 40 type ReplicateMsg struct { 41 SharingID string `json:"sharing_id"` 42 Errors int `json:"errors"` 43 } 44 45 // Replicate starts a replicator on this sharing. 46 func (s *Sharing) Replicate(inst *instance.Instance, errors int) error { 47 mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID) 48 if err := mu.Lock(); err != nil { 49 return err 50 } 51 defer mu.Unlock() 52 53 pending := false 54 var err error 55 if !s.Owner { 56 pending, err = s.ReplicateTo(inst, &s.Members[0], false) 57 } else { 58 g, _ := errgroup.WithContext(context.Background()) 59 for i := range s.Members { 60 if i == 0 { 61 continue 62 } 63 m := &s.Members[i] 64 g.Go(func() error { 65 if m.Status == MemberStatusReady { 66 p, err := s.ReplicateTo(inst, m, false) 67 if err != nil { 68 return err 69 } 70 if p { 71 pending = true 72 } 73 } 74 return nil 75 }) 76 } 77 err = g.Wait() 78 } 79 if err != nil { 80 s.retryWorker(inst, "share-replicate", errors) 81 } else if pending { 82 s.pushJob(inst, "share-replicate") 83 } 84 return err 85 } 86 87 // pushJob adds a new job to continue on the pending documents in the changes feed 88 func (s *Sharing) pushJob(inst *instance.Instance, worker string) { 89 inst.Logger().WithNamespace("replicator"). 90 Debugf("Push a new job for worker %s for sharing %s", worker, s.SID) 91 msg, err := job.NewMessage(&ReplicateMsg{ 92 SharingID: s.SID, 93 Errors: 0, 94 }) 95 if err != nil { 96 inst.Logger().WithNamespace("replicator"). 97 Warnf("Error on push job to %s: %s", worker, err) 98 return 99 } 100 _, err = job.System().PushJob(inst, &job.JobRequest{ 101 WorkerType: worker, 102 Message: msg, 103 }) 104 if err != nil { 105 inst.Logger().WithNamespace("replicator"). 106 Warnf("Error on push job to %s: %s", worker, err) 107 return 108 } 109 } 110 111 // retryWorker will add a job to retry a failed replication or upload 112 func (s *Sharing) retryWorker(inst *instance.Instance, worker string, errors int) { 113 inst.Logger().WithNamespace("replicator"). 114 Debugf("Retry worker %s for sharing %s", worker, s.SID) 115 backoff := InitialBackoffPeriod << uint(errors*2) 116 errors++ 117 if errors == MaxRetries { 118 inst.Logger().WithNamespace("replicator").Warnf("Max retries reached") 119 return 120 } 121 msg, err := job.NewMessage(&ReplicateMsg{ 122 SharingID: s.SID, 123 Errors: errors, 124 }) 125 if err != nil { 126 inst.Logger().WithNamespace("replicator"). 127 Warnf("Error on retry to %s: %s", worker, err) 128 return 129 } 130 t, err := job.NewTrigger(inst, job.TriggerInfos{ 131 Type: "@in", 132 WorkerType: worker, 133 Arguments: backoff.String(), 134 }, msg) 135 if err != nil { 136 inst.Logger().WithNamespace("replicator"). 137 Warnf("Error on retry to %s: %s", worker, err) 138 return 139 } 140 if err = job.System().AddTrigger(t); err != nil { 141 inst.Logger().WithNamespace("replicator"). 142 Warnf("Error on retry to %s: %s", worker, err) 143 } 144 } 145 146 func (s *Sharing) InitialReplication(inst *instance.Instance, m *Member) error { 147 for i := 0; i < 1000; i++ { 148 pending, err := s.ReplicateTo(inst, m, true) 149 if err != nil { 150 return err 151 } 152 if !pending { 153 return nil 154 } 155 } 156 return ErrInternalServerError 157 } 158 159 // ReplicateTo starts a replicator on this sharing to the given member. 160 // http://docs.couchdb.org/en/stable/replication/protocol.html 161 // https://github.com/pouchdb/pouchdb/blob/master/packages/node_modules/pouchdb-replication/src/replicate.js 162 func (s *Sharing) ReplicateTo(inst *instance.Instance, m *Member, initial bool) (bool, error) { 163 if m.Instance == "" { 164 return false, ErrInvalidURL 165 } 166 creds := s.FindCredentials(m) 167 if creds == nil { 168 return false, ErrInvalidSharing 169 } 170 171 lastSeq, err := s.getLastSeqNumber(inst, m, "replicator") 172 if err != nil { 173 return false, err 174 } 175 inst.Logger().WithNamespace("replicator").Debugf("lastSeq = %s", lastSeq) 176 177 feed, err := s.callChangesFeed(inst, lastSeq) 178 if err != nil { 179 if errors.Is(err, errRevokeSharing) { 180 if s.Owner { 181 return false, s.Revoke(inst) 182 } else { 183 return false, s.RevokeRecipientBySelf(inst, false) 184 } 185 } 186 return false, err 187 } 188 if feed.Seq == lastSeq { 189 return false, nil 190 } 191 inst.Logger().WithNamespace("replicator").Debugf("changes = %#v", feed.Changes) 192 193 changes := &feed.Changes 194 if len(changes.Changed) > 0 { 195 var missings *Missings 196 if initial || len(changes.Changed) == len(changes.Removed) { 197 missings = transformChangesInMissings(changes) 198 } else { 199 missings, err = s.callRevsDiff(inst, m, creds, changes) 200 if err != nil { 201 return false, err 202 } 203 } 204 inst.Logger().WithNamespace("replicator").Debugf("missings = %#v", missings) 205 206 docs, errb := s.getMissingDocs(inst, missings, changes) 207 if errb != nil { 208 return false, errb 209 } 210 inst.Logger().WithNamespace("replicator").Debugf("docs = %#v", docs) 211 212 err = s.sendBulkDocs(inst, m, creds, docs, feed.RuleIndexes) 213 if err != nil { 214 return false, err 215 } 216 } 217 218 err = s.UpdateLastSequenceNumber(inst, m, "replicator", feed.Seq) 219 return feed.Pending, err 220 } 221 222 // getLastSeqNumber returns the last sequence number of the previous 223 // replication to this member 224 func (s *Sharing) getLastSeqNumber(inst *instance.Instance, m *Member, worker string) (string, error) { 225 id, err := s.replicationID(m) 226 if err != nil { 227 return "", err 228 } 229 result, err := couchdb.GetLocal(inst, consts.Shared, id+"/"+worker) 230 if couchdb.IsNotFoundError(err) { 231 return "", nil 232 } 233 if err != nil { 234 return "", err 235 } 236 seq, _ := result["last_seq"].(string) 237 return seq, nil 238 } 239 240 // UpdateLastSequenceNumber updates the last sequence number for this 241 // replication if it's superior to the number in CouchDB 242 func (s *Sharing) UpdateLastSequenceNumber(inst *instance.Instance, m *Member, worker, seq string) error { 243 id, err := s.replicationID(m) 244 if err != nil { 245 return err 246 } 247 result, err := couchdb.GetLocal(inst, consts.Shared, id+"/"+worker) 248 if err != nil { 249 if !couchdb.IsNotFoundError(err) { 250 return err 251 } 252 result = make(map[string]interface{}) 253 } else { 254 if prev, ok := result["last_seq"].(string); ok { 255 if revision.Generation(seq) <= revision.Generation(prev) { 256 return nil 257 } 258 } 259 } 260 result["last_seq"] = seq 261 return couchdb.PutLocal(inst, consts.Shared, id+"/"+worker, result) 262 } 263 264 // ClearLastSequenceNumbers removes the last sequence numbers for a member 265 func (s *Sharing) ClearLastSequenceNumbers(inst *instance.Instance, m *Member) error { 266 errr := s.clearLastSequenceNumber(inst, m, "replicator") 267 erru := s.clearLastSequenceNumber(inst, m, "upload") 268 if errr != nil { 269 return errr 270 } 271 return erru 272 } 273 274 // clearLastSequenceNumber removes a last sequence number for a member on a given worker 275 func (s *Sharing) clearLastSequenceNumber(inst *instance.Instance, m *Member, worker string) error { 276 id, err := s.replicationID(m) 277 if err != nil { 278 return err 279 } 280 err = couchdb.DeleteLocal(inst, consts.Shared, id+"/"+worker) 281 if couchdb.IsNotFoundError(err) { 282 return nil 283 } 284 return err 285 } 286 287 // replicationID gives an identifier for this replicator 288 func (s *Sharing) replicationID(m *Member) (string, error) { 289 for i := range s.Members { 290 if &s.Members[i] == m { 291 id := fmt.Sprintf("sharing-%s-%d", s.SID, i) 292 return id, nil 293 } 294 } 295 return "", ErrMemberNotFound 296 } 297 298 // Changed is a map of "doctype/docid" -> [revisions] 299 type Changed map[string][]string 300 301 // Removed is a set of "doctype/docid" 302 type Removed map[string]struct{} 303 304 // Changes is a struct with informations from the changes feed of io.cozy.shared 305 type Changes struct { 306 Changed Changed 307 Removed Removed 308 } 309 310 func extractLastRevision(doc couchdb.JSONDoc) string { 311 var rev string 312 subtree := doc.Get("revisions") 313 for { 314 m, ok := subtree.(map[string]interface{}) 315 if !ok { 316 break 317 } 318 rev = m["rev"].(string) 319 branches, ok := m["branches"].([]interface{}) 320 if !ok || len(branches) == 0 { 321 break 322 } 323 subtree = branches[0] 324 } 325 return rev 326 } 327 328 func extractRevisionsSlice(doc couchdb.JSONDoc) []string { 329 slice := []string{} 330 subtree := doc.Get("revisions") 331 for { 332 m, ok := subtree.(map[string]interface{}) 333 if !ok { 334 break 335 } 336 rev := m["rev"].(string) 337 slice = append(slice, rev) 338 branches, ok := m["branches"].([]interface{}) 339 if !ok || len(branches) == 0 { 340 break 341 } 342 subtree = branches[0] 343 } 344 return slice 345 } 346 347 // changesResponse contains the useful informations from a call to the changes 348 // feed in the replicator context 349 type changesResponse struct { 350 // Changes is the list of changed documents 351 Changes Changes 352 // RuleIndexes is a mapping between the "doctype/docid" -> rule index 353 RuleIndexes map[string]int 354 // Seq is the sequence number after these changes 355 Seq string 356 // Pending is true if there are some other changes in the feed after those 357 Pending bool 358 } 359 360 // errRevokeSharing is a sentinel value that can be returned by callChangesFeed. 361 // It means that a document fetched from the changes that has been removed, and 362 // the sharing rule for it has the revoke action. So, the caller should check 363 // for this error, and it is the case, it should revoke the sharing. 364 var errRevokeSharing = errors.New("Sharing must be revoked") 365 366 // callChangesFeed fetches the last changes from the changes feed 367 // http://docs.couchdb.org/en/stable/api/database/changes.html 368 func (s *Sharing) callChangesFeed(inst *instance.Instance, since string) (*changesResponse, error) { 369 response, err := couchdb.GetChanges(inst, &couchdb.ChangesRequest{ 370 DocType: consts.Shared, 371 IncludeDocs: true, 372 Since: since, 373 Limit: BatchSize, 374 }) 375 if err != nil { 376 return nil, err 377 } 378 res := changesResponse{ 379 Changes: Changes{ 380 Changed: make(Changed), 381 Removed: make(Removed), 382 }, 383 RuleIndexes: make(map[string]int), 384 Seq: response.LastSeq, 385 Pending: response.Pending > 0, 386 } 387 for _, r := range response.Results { 388 infos, ok := r.Doc.Get("infos").(map[string]interface{}) 389 if !ok { 390 continue 391 } 392 info, ok := infos[s.SID].(map[string]interface{}) 393 if !ok { 394 continue 395 } 396 idx, ok := info["rule"].(float64) 397 if !ok { 398 continue 399 } 400 res.RuleIndexes[r.DocID] = int(idx) 401 if _, ok = info["removed"]; ok { 402 rule := s.Rules[int(idx)] 403 if rule.Remove == ActionRuleRevoke { 404 return nil, errRevokeSharing 405 } 406 res.Changes.Removed[r.DocID] = struct{}{} 407 } 408 if strings.HasPrefix(r.DocID, consts.Files+"/") { 409 if rev := extractLastRevision(r.Doc); rev != "" { 410 res.Changes.Changed[r.DocID] = []string{rev} 411 } 412 } else { 413 res.Changes.Changed[r.DocID] = extractRevisionsSlice(r.Doc) 414 } 415 } 416 return &res, nil 417 } 418 419 // Missings is a struct for the response of _revs_diff 420 type Missings map[string]MissingEntry 421 422 // MissingEntry is a struct with the missing revisions for an id 423 type MissingEntry struct { 424 Missing []string `json:"missing"` 425 } 426 427 // transformChangesInMissings is used for the initial replication (revs_diff is 428 // not called), to prepare the payload for the bulk_get calls 429 func transformChangesInMissings(changes *Changes) *Missings { 430 missings := make(Missings, len(changes.Changed)) 431 for key, revs := range changes.Changed { 432 missings[key] = MissingEntry{ 433 Missing: []string{revs[len(revs)-1]}, 434 } 435 } 436 return &missings 437 } 438 439 // callRevsDiff asks the other cozy to compute the _revs_diff. 440 // This function does the ID transformation for files in both ways. 441 // http://docs.couchdb.org/en/stable/api/database/misc.html#db-revs-diff 442 func (s *Sharing) callRevsDiff(inst *instance.Instance, m *Member, creds *Credentials, changes *Changes) (*Missings, error) { 443 u, err := url.Parse(m.Instance) 444 if err != nil { 445 return nil, err 446 } 447 // "io.cozy.files/docid" for recipient -> "io.cozy.files/docid" for sender 448 xored := make(map[string]string) 449 // "doctype/docid" -> [leaf revisions] 450 leafRevs := make(Changed, len(changes.Changed)) 451 for key, revs := range changes.Changed { 452 if len(creds.XorKey) > 0 { 453 old := key 454 parts := strings.SplitN(key, "/", 2) 455 parts[1] = XorID(parts[1], creds.XorKey) 456 key = parts[0] + "/" + parts[1] 457 xored[key] = old 458 leafRevs[key] = revs[len(revs)-1:] 459 } else { 460 leafRevs[key] = revs 461 } 462 } 463 body, err := json.Marshal(leafRevs) 464 if err != nil { 465 return nil, err 466 } 467 opts := &request.Options{ 468 Method: http.MethodPost, 469 Scheme: u.Scheme, 470 Domain: u.Host, 471 Path: "/sharings/" + s.SID + "/_revs_diff", 472 Headers: request.Headers{ 473 echo.HeaderAccept: echo.MIMEApplicationJSON, 474 echo.HeaderContentType: echo.MIMEApplicationJSON, 475 echo.HeaderAuthorization: "Bearer " + creds.AccessToken.AccessToken, 476 }, 477 Body: bytes.NewReader(body), 478 ParseError: ParseRequestError, 479 } 480 var res *http.Response 481 res, err = request.Req(opts) 482 if res != nil && res.StatusCode/100 == 4 { 483 res, err = RefreshToken(inst, err, s, m, creds, opts, body) 484 } 485 if err != nil { 486 if res != nil && res.StatusCode/100 == 5 { 487 return nil, ErrInternalServerError 488 } 489 return nil, err 490 } 491 defer res.Body.Close() 492 493 missings := make(Missings) 494 if err = json.NewDecoder(res.Body).Decode(&missings); err != nil { 495 return nil, err 496 } 497 for k, v := range missings { 498 if old, ok := xored[k]; ok { 499 missings[old] = v 500 delete(missings, k) 501 } 502 } 503 return &missings, nil 504 } 505 506 // ComputeRevsDiff takes a map of id->[revisions] and returns the missing 507 // revisions for those documents on the current instance. 508 func (s *Sharing) ComputeRevsDiff(inst *instance.Instance, changed Changed) (*Missings, error) { 509 inst.Logger().WithNamespace("replicator"). 510 Debugf("ComputeRevsDiff %#v", changed) 511 ids := make([]string, 0, len(changed)) 512 for id := range changed { 513 ids = append(ids, id) 514 } 515 results := make([]SharedRef, 0, len(changed)) 516 req := couchdb.AllDocsRequest{Keys: ids} 517 err := couchdb.GetAllDocs(inst, consts.Shared, &req, &results) 518 if err != nil { 519 return nil, err 520 } 521 missings := make(Missings) 522 for id, revs := range changed { 523 missings[id] = MissingEntry{Missing: revs} 524 } 525 for _, result := range results { 526 if _, ok := changed[result.SID]; !ok { 527 continue 528 } 529 if _, ok := result.Infos[s.SID]; !ok { 530 continue 531 } 532 notFounds := changed[result.SID][:0] 533 for _, r := range changed[result.SID] { 534 if sub, _ := result.Revisions.Find(r); sub == nil { 535 notFounds = append(notFounds, r) 536 } 537 } 538 if len(notFounds) == 0 { 539 delete(missings, result.SID) 540 } else { 541 missings[result.SID] = MissingEntry{ 542 Missing: notFounds, 543 } 544 } 545 } 546 return &missings, nil 547 } 548 549 // DocsList is a slice of raw documents 550 type DocsList []map[string]interface{} 551 552 // DocsByDoctype is a map of doctype -> slice of documents of this doctype 553 type DocsByDoctype map[string]DocsList 554 555 // getMissingDocs fetches the documents in bulk, partitionned by their doctype. 556 // https://github.com/apache/couchdb-documentation/pull/263/files 557 func (s *Sharing) getMissingDocs(inst *instance.Instance, missings *Missings, changes *Changes) (*DocsByDoctype, error) { 558 docs := make(DocsByDoctype) 559 queries := make(map[string][]couchdb.IDRev) // doctype -> payload for _bulk_get 560 for key, missing := range *missings { 561 parts := strings.SplitN(key, "/", 2) 562 if len(parts) != 2 { 563 return nil, ErrInternalServerError 564 } 565 doctype := parts[0] 566 if _, ok := changes.Removed[key]; ok { 567 revisions := changes.Changed[key] 568 docs[doctype] = append(docs[doctype], map[string]interface{}{ 569 "_id": parts[1], 570 "_rev": revisions[len(revisions)-1], 571 "_revisions": revsChainToStruct(revisions), 572 "_deleted": true, 573 }) 574 continue 575 } 576 for _, rev := range missing.Missing { 577 ir := couchdb.IDRev{ID: parts[1], Rev: rev} 578 queries[doctype] = append(queries[doctype], ir) 579 } 580 } 581 582 for doctype, query := range queries { 583 results, err := couchdb.BulkGetDocs(inst, doctype, query) 584 if err != nil { 585 return nil, err 586 } 587 docs[doctype] = append(docs[doctype], results...) 588 } 589 return &docs, nil 590 } 591 592 // sendBulkDocs takes a bulk of documents and send them to the other cozy. 593 // This function does the id and dir_id transformation before sending files. 594 // http://docs.couchdb.org/en/stable/api/database/bulk-api.html#db-bulk-docs 595 // https://wiki.apache.org/couchdb/HTTP_Bulk_Document_API#Posting_Existing_Revisions 596 // https://gist.github.com/nono/42aee18de6314a621f9126f284e303bb 597 func (s *Sharing) sendBulkDocs(inst *instance.Instance, m *Member, creds *Credentials, docsByDoctype *DocsByDoctype, ruleIndexes map[string]int) error { 598 u, err := url.Parse(m.Instance) 599 if err != nil { 600 return err 601 } 602 for doctype, docs := range *docsByDoctype { 603 switch doctype { 604 case consts.Files: 605 s.SortFilesToSent(docs) 606 for i, file := range docs { 607 fileID := file["_id"].(string) 608 s.TransformFileToSent(file, creds.XorKey, ruleIndexes[fileID]) 609 docs[i] = file 610 } 611 case consts.BitwardenCiphers: 612 for i, doc := range docs { 613 s.transformCipherToSent(doc, creds.XorKey) 614 docs[i] = doc 615 } 616 default: 617 for i, doc := range docs { 618 id := doc["_id"].(string) 619 doc["_id"] = XorID(id, creds.XorKey) 620 docs[i] = doc 621 } 622 } 623 (*docsByDoctype)[doctype] = docs 624 } 625 body, err := json.Marshal(docsByDoctype) 626 if err != nil { 627 return err 628 } 629 opts := &request.Options{ 630 Method: http.MethodPost, 631 Scheme: u.Scheme, 632 Domain: u.Host, 633 Path: "/sharings/" + s.SID + "/_bulk_docs", 634 Headers: request.Headers{ 635 echo.HeaderAccept: echo.MIMEApplicationJSON, 636 echo.HeaderContentType: echo.MIMEApplicationJSON, 637 echo.HeaderAuthorization: "Bearer " + creds.AccessToken.AccessToken, 638 }, 639 Body: bytes.NewReader(body), 640 ParseError: ParseRequestError, 641 Client: safehttp.ClientWithKeepAlive, 642 } 643 res, err := request.Req(opts) 644 if res != nil && res.StatusCode/100 == 4 { 645 res, err = RefreshToken(inst, err, s, m, creds, opts, body) 646 } 647 if err != nil { 648 if res != nil && res.StatusCode/100 == 5 { 649 return ErrInternalServerError 650 } 651 return err 652 } 653 res.Body.Close() 654 return nil 655 } 656 657 // ApplyBulkDocs is a multi-doctypes version of the POST _bulk_docs endpoint of CouchDB 658 func (s *Sharing) ApplyBulkDocs(inst *instance.Instance, payload DocsByDoctype) error { 659 mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID+"/_bulk_docs") 660 if err := mu.Lock(); err != nil { 661 return err 662 } 663 defer mu.Unlock() 664 665 var refs []*SharedRef 666 667 for doctype, docs := range payload { 668 inst.Logger().WithNamespace("replicator"). 669 Debugf("Apply bulk docs %s: %#v", doctype, docs) 670 if doctype == consts.Files { 671 err := s.ApplyBulkFiles(inst, docs) 672 if err != nil { 673 return err 674 } 675 continue 676 } 677 var okDocs, docsToUpdate DocsList 678 var newRefs, existingRefs []*SharedRef 679 newDocs, existingDocs, err := partitionDocsPayload(inst, doctype, docs) 680 if err == nil { 681 okDocs, newRefs = s.filterDocsToAdd(inst, doctype, newDocs) 682 docsToUpdate, existingRefs, err = s.filterDocsToUpdate(inst, doctype, existingDocs) 683 if err != nil { 684 return err 685 } 686 okDocs = append(okDocs, docsToUpdate...) 687 } else { 688 okDocs, newRefs = s.filterDocsToAdd(inst, doctype, docs) 689 if len(okDocs) > 0 { 690 if err = couchdb.CreateDB(inst, doctype); err != nil { 691 return err 692 } 693 } 694 } 695 if len(okDocs) > 0 { 696 if err = couchdb.BulkForceUpdateDocs(inst, doctype, okDocs); err != nil { 697 return err 698 } 699 for _, doc := range okDocs { 700 d := couchdb.JSONDoc{M: doc, Type: doctype} 701 event := realtime.EventUpdate 702 if doc["_deleted"] != nil { 703 event = realtime.EventDelete 704 } 705 couchdb.RTEvent(inst, event, &d, nil) 706 } 707 refs = append(refs, newRefs...) 708 refs = append(refs, existingRefs...) 709 } 710 711 // XXX the bitwarden clients synchronize the ciphers only if the 712 // revision date from GET /bitwarden/api/accounts/revision-date has 713 // changed. So, we update it here! 714 if doctype == consts.BitwardenCiphers { 715 if setting, err := settings.Get(inst); err == nil { 716 _ = settings.UpdateRevisionDate(inst, setting) 717 } 718 } 719 } 720 721 refsToUpdate := make([]interface{}, len(refs)) 722 for i, ref := range refs { 723 refsToUpdate[i] = ref 724 } 725 olds := make([]interface{}, len(refsToUpdate)) 726 return couchdb.BulkUpdateDocs(inst, consts.Shared, refsToUpdate, olds) 727 } 728 729 // partitionDocsPayload returns two slices: the first with documents that are new, 730 // the second with documents that already exist on this cozy and must be updated. 731 func partitionDocsPayload(inst *instance.Instance, doctype string, docs DocsList) (news DocsList, existings DocsList, err error) { 732 ids := make([]string, len(docs)) 733 for i, doc := range docs { 734 _, ok := doc["_rev"].(string) 735 if !ok { 736 return nil, nil, ErrMissingRev 737 } 738 ids[i], ok = doc["_id"].(string) 739 if !ok { 740 return nil, nil, ErrMissingID 741 } 742 } 743 results := make([]interface{}, 0, len(docs)) 744 req := couchdb.AllDocsRequest{Keys: ids} 745 if err = couchdb.GetAllDocs(inst, doctype, &req, &results); err != nil { 746 return nil, nil, err 747 } 748 for i, doc := range docs { 749 if results[i] == nil { 750 news = append(news, doc) 751 } else { 752 existings = append(existings, doc) 753 } 754 } 755 return news, existings, nil 756 } 757 758 // filterDocsToAdd returns a subset of the docs slice with just the documents 759 // that match a rule of the sharing. It also returns a reference documents to 760 // put in the io.cozy.shared database. 761 // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating 762 func (s *Sharing) filterDocsToAdd(inst *instance.Instance, doctype string, docs DocsList) (DocsList, []*SharedRef) { 763 filtered := docs[:0] 764 refs := make([]*SharedRef, 0, len(docs)) 765 for _, doc := range docs { 766 if _, ok := doc["_deleted"]; ok { 767 continue 768 } 769 r := -1 770 for i, rule := range s.Rules { 771 if rule.Accept(doctype, doc) { 772 r = i 773 break 774 } 775 } 776 if r >= 0 { 777 ref := SharedRef{ 778 SID: doctype + "/" + doc["_id"].(string), 779 Revisions: &RevsTree{Rev: doc["_rev"].(string)}, 780 Infos: map[string]SharedInfo{ 781 s.SID: {Rule: r}, 782 }, 783 } 784 refs = append(refs, &ref) 785 filtered = append(filtered, doc) 786 } 787 } 788 return filtered, refs 789 } 790 791 // filterDocsToUpdate returns a subset of the docs slice with just the documents 792 // that are referenced for this sharing in the io.cozy.shared database. 793 func (s *Sharing) filterDocsToUpdate(inst *instance.Instance, doctype string, docs DocsList) (DocsList, []*SharedRef, error) { 794 ids := make([]string, len(docs)) 795 for i, doc := range docs { 796 id, ok := doc["_id"].(string) 797 if !ok { 798 return nil, nil, ErrMissingID 799 } 800 ids[i] = doctype + "/" + id 801 } 802 refs, err := FindReferences(inst, ids) 803 if err != nil { 804 return nil, nil, err 805 } 806 807 filtered := docs[:0] 808 frefs := refs[:0] 809 for i, doc := range docs { 810 if refs[i] != nil { 811 infos, ok := refs[i].Infos[s.SID] 812 if ok && !infos.Removed { 813 rev := doc["_rev"].(string) 814 if sub, _ := refs[i].Revisions.Find(rev); sub == nil { 815 revs := revsMapToStruct(doc["_revisions"]) 816 if revs != nil && len(revs.IDs) > 0 { 817 chain := revsStructToChain(*revs) 818 refs[i].Revisions.InsertChain(chain) 819 } 820 } 821 if _, ok := doc["_deleted"]; ok { 822 infos.Removed = true 823 refs[i].Infos[s.SID] = infos 824 } 825 frefs = append(frefs, refs[i]) 826 filtered = append(filtered, doc) 827 } 828 } 829 } 830 831 return filtered, frefs, nil 832 } 833 834 func (s *Sharing) transformCipherToSent(doc map[string]interface{}, xorKey []byte) { 835 id := doc["_id"].(string) 836 doc["_id"] = XorID(id, xorKey) 837 if orgID, ok := doc["organization_id"].(string); ok { 838 doc["organization_id"] = XorID(orgID, xorKey) 839 } 840 }