Skip to main content

lb_rs/subscribers/
syncer.rs

1use std::{
2    collections::{HashMap, HashSet},
3    num::NonZeroUsize,
4    sync::Arc,
5    thread,
6    time::{Duration, Instant},
7};
8
9use futures::{StreamExt, stream};
10use tokio::sync::{Mutex, broadcast::error::TryRecvError};
11use tokio::time;
12use usvg::Transform;
13use uuid::Uuid;
14
15use crate::{
16    Lb, LbErrKind, LbResult,
17    io::network::ApiError,
18    model::{
19        ValidationFailure,
20        access_info::UserAccessMode,
21        account::Account,
22        api::{
23            ChangeDocRequestV2, GetDocRequest, GetFileIdsRequest, GetUpdatesRequestV2,
24            GetUsernameError, GetUsernameRequest, UpsertDebugInfoRequest, UpsertRequestV2,
25        },
26        chat,
27        crypto::{AESKey, DecryptedDocument, EncryptedDocument},
28        errors::{LbErr, Unexpected},
29        file::ShareMode,
30        file_like::FileLike,
31        file_metadata::{DocumentHmac, FileDiff, FileType, Owner},
32        filename::{DocumentType, NameComponents},
33        lazy::LazyTree,
34        signed_meta::SignedMeta,
35        staged::StagedTreeLikeMut,
36        svg::{self, buffer::u_transform_to_bezier, element::Element},
37        symkey, text,
38        tree_like::TreeLike,
39        validate,
40    },
41    service::events::{Actor, Event, SyncIncrement},
42};
43
44pub type Syncer = Arc<Mutex<SyncState>>;
45
46#[derive(Default)]
47pub struct SyncState {
48    /// the starting point for updates for this sync pass
49    last_synced: u64,
50
51    /// if our pull is successful, this is the timestamp we will commit
52    updates_as_of: u64,
53
54    /// changes we pulled from the server, post deduplication
55    remote_changes: Vec<SignedMeta>,
56
57    /// did we pull a root on this pass?
58    new_root: Option<Uuid>,
59
60    /// what docs did we pull as a result of this sync
61    pulled_docs: Vec<Uuid>,
62}
63
64// we are gonna have a fetch metadata fn which will get the docs that it needs to get, the ones
65// that match should_fetch
66//
67// should_fetch is going to be a tree fn that will return true if:
68//     is md or svg that descends from
69
70impl Lb {
71    #[instrument(level = "debug", skip(self), err(Debug))]
72    pub async fn sync(&self) -> LbResult<()> {
73        let mut sync_state = self.syncer.lock().await;
74        self.events.sync_update(SyncIncrement::SyncStarted);
75
76        let pipeline: LbResult<()> = async {
77            self.pull_updates(&mut sync_state).await?;
78            self.push_local_changes().await?;
79            Ok(())
80        }
81        .await;
82
83        self.events.sync_update(SyncIncrement::SyncFinished(
84            pipeline.as_ref().err().map(|err| err.kind.clone()),
85        ));
86
87        self.cleanup().await?;
88
89        pipeline?;
90
91        let account = self.get_account()?.clone();
92
93        #[cfg(not(target_family = "wasm"))]
94        if account.is_beta() {
95            self.send_debug_info(account).await;
96        }
97
98        Ok(())
99    }
100
101    pub(crate) async fn pull_updates(&self, sync_state: &mut SyncState) -> LbResult<()> {
102        self.inital_sync_state(sync_state).await?;
103        self.process_deletions().await?;
104        self.fetch_meta(sync_state).await?;
105        self.fetch_required_docs(sync_state).await?;
106        // todo: should this inform a re-pull?
107        self.merge(sync_state).await?;
108        self.commit_last_synced(sync_state).await?;
109        self.send_pull_events(sync_state).await?;
110
111        if !self.config.background_work {
112            self.populate_pk_cache().await?;
113        }
114
115        Ok(())
116    }
117
118    pub(crate) async fn push_local_changes(&self) -> LbResult<()> {
119        self.push_meta().await?;
120        self.push_docs().await?;
121
122        Ok(())
123    }
124
125    async fn inital_sync_state(&self, state: &mut SyncState) -> LbResult<()> {
126        let tx = self.ro_tx().await;
127        let db = tx.db();
128
129        *state = Default::default();
130        state.last_synced = db.last_synced.as_ref().copied().unwrap_or_default() as u64;
131
132        Ok(())
133    }
134
135    pub(crate) async fn process_deletions(&self) -> LbResult<()> {
136        let server_ids = self
137            .client
138            .request(self.get_account()?, GetFileIdsRequest {})
139            .await?
140            .ids;
141
142        let mut tx = self.begin_tx().await;
143        let db = tx.db();
144
145        let mut local = db.base_metadata.stage(&db.local_metadata).to_lazy();
146        let base_ids = local.tree.base.ids();
147
148        let mut prunable_ids = base_ids;
149        prunable_ids.retain(|id| !server_ids.contains(id));
150        for id in prunable_ids.clone() {
151            prunable_ids.extend(local.descendants(&id)?);
152        }
153        for id in &prunable_ids {
154            if let Some(base_file) = local.tree.base.maybe_find(id) {
155                self.docs
156                    .delete(*id, base_file.document_hmac().copied())
157                    .await?;
158            }
159            if let Some(local_file) = local.maybe_find(id) {
160                self.docs
161                    .delete(*id, local_file.document_hmac().copied())
162                    .await?;
163            }
164        }
165
166        let mut base_staged = (&mut db.base_metadata).to_lazy().stage(None);
167        base_staged.tree.removed = prunable_ids.iter().copied().collect();
168        base_staged.promote()?;
169
170        let mut local_staged = (&mut db.local_metadata).to_lazy().stage(None);
171        local_staged.tree.removed = prunable_ids.iter().copied().collect();
172        local_staged.promote()?;
173
174        if !prunable_ids.is_empty() {
175            self.events.meta_changed(Actor::Sync);
176        }
177
178        Ok(())
179    }
180
181    async fn fetch_meta(&self, state: &mut SyncState) -> LbResult<()> {
182        let updates = self
183            .client
184            .request(
185                self.get_account()?,
186                GetUpdatesRequestV2 { since_metadata_version: state.last_synced },
187            )
188            .await?;
189
190        let tx = self.ro_tx().await;
191        let db = tx.db();
192
193        // this loop implicitly prunes remote orphans
194        let mut without_orphans = Vec::new();
195        let me = Owner(self.keychain.get_pk()?);
196        let remote = db.base_metadata.stage(updates.file_metadata).to_lazy();
197        for id in remote.tree.staged.ids() {
198            let meta = remote.find(&id)?;
199            if remote.maybe_find_parent(meta).is_some()
200                || meta
201                    .user_access_keys()
202                    .iter()
203                    .any(|k| k.encrypted_for == me.0)
204            {
205                without_orphans.push(remote.find(&id)?.clone());
206            }
207        }
208
209        // this is what actually performs the deduplication
210        let pruned_tree = db.base_metadata.stage(without_orphans).pruned()?.to_lazy();
211        let (_, deduped_changes) = pruned_tree.unstage();
212
213        // initialize root if this is the first pull on this device
214        let mut root_id = None;
215        if db.root.as_ref().is_none() {
216            let root = deduped_changes
217                .all_files()?
218                .into_iter()
219                .find(|f| f.is_root())
220                .ok_or(LbErrKind::RootNonexistent)?;
221            root_id = Some(*root.id());
222        }
223
224        state.remote_changes = deduped_changes;
225        state.updates_as_of = updates.as_of_metadata_version;
226        state.new_root = root_id;
227
228        Ok(())
229    }
230
231    async fn fetch_required_docs(&self, state: &mut SyncState) -> LbResult<()> {
232        let mut docs_to_pull = vec![];
233
234        let tx = self.ro_tx().await;
235        let db = tx.db();
236
237        let mut files_with_local_edits = vec![];
238        let local = db.base_metadata.stage(&db.local_metadata);
239        for id in local.staged.ids() {
240            if let Some(base) = local.base.maybe_find(&id) {
241                if let Some(local_hmac) = local.find(&id)?.document_hmac() {
242                    if Some(local_hmac) != base.document_hmac() {
243                        files_with_local_edits.push(id);
244                        println!("local edits found");
245                    }
246                }
247            }
248        }
249
250        let mut remote = db
251            .base_metadata
252            .stage(state.remote_changes.clone())
253            .to_lazy();
254
255        for id in remote.tree.staged.ids() {
256            if remote.calculate_deleted(&id)? {
257                continue;
258            }
259            let remote_hmac = remote.find(&id)?.document_hmac().cloned();
260            let base_hmac = remote
261                .tree
262                .base
263                .maybe_find(&id)
264                .and_then(|f| f.document_hmac())
265                .cloned();
266            if base_hmac == remote_hmac {
267                continue;
268            }
269
270            if let Some(remote_hmac) = remote_hmac {
271                // pull a file if we have a prior base, this is our heuristic -- do they have the
272                // ability to edit this file while we release the lock and are pulling all the
273                // files
274                if self.docs.exists(id, base_hmac) && !self.docs.exists(id, Some(remote_hmac)) {
275                    docs_to_pull.push((id, remote_hmac));
276                }
277
278                // this clause captures documents which went from being new -> multiple parties
279                // having updates. We'll still need the updates
280                if files_with_local_edits.contains(&id)
281                    && !docs_to_pull
282                        .iter()
283                        .any(|(already_pulling, _)| already_pulling == &id)
284                {
285                    if let Some(base_hmac) = base_hmac {
286                        if !self.docs.exists(id, Some(base_hmac)) {
287                            // this scenario basically only comes up in tests
288                            // someone modifies a file directly without reading the prior version
289                            docs_to_pull.push((id, base_hmac));
290                        }
291                    }
292                    docs_to_pull.push((id, remote_hmac));
293                }
294            }
295        }
296        drop(tx);
297
298        let futures = docs_to_pull
299            .into_iter()
300            .map(|(id, hmac)| async move { self.ensure_doc_available(id, hmac).await.map(|_| id) });
301
302        let mut stream = stream::iter(futures).buffer_unordered(
303            thread::available_parallelism()
304                .unwrap_or(NonZeroUsize::new(4).unwrap())
305                .into(),
306        );
307
308        while let Some(fut) = stream.next().await {
309            let id = fut?;
310            state.pulled_docs.push(id);
311        }
312
313        Ok(())
314    }
315
316    pub(crate) async fn ensure_doc_available(
317        &self, id: Uuid, hmac: DocumentHmac,
318    ) -> LbResult<Option<EncryptedDocument>> {
319        // todo: in a lot of cases there is a list of ids we're trying to get, it would be better
320        // if the caller managed the event updates, the status would be more meaningful for longer
321
322        // in this world, can we get stuck pushing a doc as well? Probably fine for now
323        if self.docs.exists(id, Some(hmac)) {
324            return Ok(None);
325        }
326
327        self.events
328            .sync_update(SyncIncrement::PullingDocument(id, true));
329        let remote_document = self
330            .client
331            .request(self.get_account()?, GetDocRequest { id, hmac })
332            .await?;
333        self.docs
334            .insert(id, Some(hmac), &remote_document.content)
335            .await?;
336        self.events
337            .sync_update(SyncIncrement::PullingDocument(id, false));
338
339        Ok(Some(remote_document.content))
340    }
341
342    pub(crate) async fn fetch_doc(
343        &self, id: Uuid, hmac: DocumentHmac,
344    ) -> LbResult<EncryptedDocument> {
345        match self.ensure_doc_available(id, hmac).await? {
346            Some(doc) => Ok(doc),
347            None => self.docs.get(id, Some(hmac)).await,
348        }
349    }
350
351    /// Pulls remote changes and constructs a changeset Merge such that Stage<Stage<Stage<Base, Remote>, Local>, Merge> is valid.
352    /// Promotes Base to Stage<Base, Remote> and Local to Stage<Local, Merge>
353    async fn merge(&self, state: &mut SyncState) -> LbResult<()> {
354        let mut tx = self.begin_tx().await;
355        let db = tx.db();
356        let start = Instant::now();
357
358        let remote_changes = &state.remote_changes;
359
360        // fetch document updates and local documents for merge
361        let me = Owner(self.keychain.get_pk()?);
362
363        // compute merge changes
364        let merge_changes = {
365            // assemble trees
366            let mut base = (&db.base_metadata).to_lazy();
367            let remote_unlazy = (&db.base_metadata).to_staged(remote_changes);
368            let mut remote = remote_unlazy.as_lazy();
369            let mut local = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
370
371            // changeset constraints - these evolve as we try to assemble changes and encounter validation failures
372            let mut files_to_unmove: HashSet<Uuid> = HashSet::new();
373            let mut files_to_unshare: HashSet<Uuid> = HashSet::new();
374            let mut links_to_delete: HashSet<Uuid> = HashSet::new();
375            let mut rename_increments: HashMap<Uuid, usize> = HashMap::new();
376            // Id and key share one lifetime: the merge, including construction retries.
377            // A new key per attempt encrypts with the key cached by the failed attempt.
378            let mut duplicate_file_ids: HashMap<Uuid, (Uuid, AESKey)> = HashMap::new();
379
380            'merge_construction: loop {
381                // process just the edits which allow us to check deletions in the result
382                let mut deletions = {
383                    let mut deletions = remote_unlazy.stage(Vec::new()).to_lazy();
384
385                    // creations
386                    let mut deletion_creations = HashSet::new();
387                    for id in db.local_metadata.ids() {
388                        if remote.maybe_find(&id).is_none() && !links_to_delete.contains(&id) {
389                            deletion_creations.insert(id);
390                        }
391                    }
392                    'drain_creations: while !deletion_creations.is_empty() {
393                        'choose_a_creation: for id in &deletion_creations {
394                            // create
395                            let id = *id;
396                            let local_file = local.find(&id)?.clone();
397                            let result = deletions.create_unvalidated(
398                                id,
399                                symkey::generate_key(),
400                                local_file.parent(),
401                                &local.name(&id, &self.keychain)?,
402                                local_file.file_type(),
403                                &self.keychain,
404                            );
405                            match result {
406                                Ok(_) => {
407                                    deletion_creations.remove(&id);
408                                    continue 'drain_creations;
409                                }
410                                Err(ref err) => match err.kind {
411                                    LbErrKind::FileParentNonexistent => {
412                                        continue 'choose_a_creation;
413                                    }
414                                    _ => {
415                                        result?;
416                                    }
417                                },
418                            }
419                        }
420                        return Err(LbErrKind::Unexpected(format!(
421                            "sync failed to find a topomodelal order for file creations: {deletion_creations:?}"
422                        ))
423                        .into());
424                    }
425
426                    // moves (creations happen first in case a file is moved into a new folder)
427                    for id in db.local_metadata.ids() {
428                        let local_file = local.find(&id)?.clone();
429                        if let Some(base_file) = db.base_metadata.maybe_find(&id).cloned() {
430                            if !local_file.explicitly_deleted()
431                                && local_file.parent() != base_file.parent()
432                                && !files_to_unmove.contains(&id)
433                            {
434                                // move
435                                deletions.move_unvalidated(
436                                    &id,
437                                    local_file.parent(),
438                                    &self.keychain,
439                                )?;
440                            }
441                        }
442                    }
443
444                    // deletions (moves happen first in case a file is moved into a deleted folder)
445                    for id in db.local_metadata.ids() {
446                        let local_file = local.find(&id)?.clone();
447                        if local_file.explicitly_deleted() {
448                            // delete
449                            deletions.delete_unvalidated(&id, &self.keychain)?;
450                        }
451                    }
452                    deletions
453                };
454
455                // process all edits, dropping non-deletion edits for files that will be implicitly deleted
456                let mut merge = {
457                    let mut merge = remote_unlazy.stage(Vec::new()).to_lazy();
458
459                    // creations and edits of created documents
460                    let mut creations = HashSet::new();
461                    for id in db.local_metadata.ids() {
462                        if deletions.maybe_find(&id).is_some()
463                            && !deletions.calculate_deleted(&id)?
464                            && remote.maybe_find(&id).is_none()
465                            && !links_to_delete.contains(&id)
466                        {
467                            creations.insert(id);
468                        }
469                    }
470                    'drain_creations: while !creations.is_empty() {
471                        'choose_a_creation: for id in &creations {
472                            // create
473                            let id = *id;
474                            let local_file = local.find(&id)?.clone();
475                            let result = merge.create_unvalidated(
476                                id,
477                                local.decrypt_key(&id, &self.keychain)?,
478                                local_file.parent(),
479                                &local.name(&id, &self.keychain)?,
480                                local_file.file_type(),
481                                &self.keychain,
482                            );
483                            match result {
484                                Ok(_) => {
485                                    creations.remove(&id);
486                                    continue 'drain_creations;
487                                }
488                                Err(ref err) => match err.kind {
489                                    LbErrKind::FileParentNonexistent => {
490                                        continue 'choose_a_creation;
491                                    }
492                                    _ => {
493                                        result?;
494                                    }
495                                },
496                            }
497                        }
498                        return Err(LbErrKind::Unexpected(format!(
499                            "sync failed to find a topomodelal order for file creations: {creations:?}"
500                        ))
501                        .into());
502                    }
503
504                    // moves, renames, edits, and shares
505                    // creations happen first in case a file is moved into a new folder
506                    for id in db.local_metadata.ids() {
507                        // skip files that are already deleted or will be deleted
508                        if deletions.maybe_find(&id).is_none()
509                            || deletions.calculate_deleted(&id)?
510                            || (remote.maybe_find(&id).is_some()
511                                && remote.calculate_deleted(&id)?)
512                        {
513                            continue;
514                        }
515
516                        let local_file = local.find(&id)?.clone();
517                        let local_name = local.name(&id, &self.keychain)?;
518                        let maybe_base_file = base.maybe_find(&id).cloned();
519                        let maybe_remote_file = remote.maybe_find(&id).cloned();
520                        if let Some(ref base_file) = maybe_base_file {
521                            let base_name = base.name(&id, &self.keychain)?;
522                            let remote_file = remote.find(&id)?.clone();
523                            let remote_name = remote.name(&id, &self.keychain)?;
524
525                            // move
526                            if local_file.parent() != base_file.parent()
527                                && remote_file.parent() == base_file.parent()
528                                && !files_to_unmove.contains(&id)
529                            {
530                                merge.move_unvalidated(&id, local_file.parent(), &self.keychain)?;
531                            }
532
533                            // rename
534                            if local_name != base_name && remote_name == base_name {
535                                merge.rename_unvalidated(&id, &local_name, &self.keychain)?;
536                            }
537                        }
538
539                        // share
540                        let mut remote_keys = HashMap::new();
541                        if let Some(ref remote_file) = maybe_remote_file {
542                            for key in remote_file.user_access_keys() {
543                                remote_keys.insert(
544                                    (Owner(key.encrypted_by), Owner(key.encrypted_for)),
545                                    (key.mode, key.deleted),
546                                );
547                            }
548                        }
549                        for key in local_file.user_access_keys() {
550                            let (by, for_) = (Owner(key.encrypted_by), Owner(key.encrypted_for));
551                            if let Some(&(remote_mode, remote_deleted)) =
552                                remote_keys.get(&(by, for_))
553                            {
554                                // upgrade share
555                                if key.mode > remote_mode || !key.deleted && remote_deleted {
556                                    let mode = match key.mode {
557                                        UserAccessMode::Read => ShareMode::Read,
558                                        UserAccessMode::Write => ShareMode::Write,
559                                        UserAccessMode::Owner => continue,
560                                    };
561                                    merge.add_share_unvalidated(id, for_, mode, &self.keychain)?;
562                                }
563                                // delete share
564                                if key.deleted && !remote_deleted {
565                                    merge.delete_share_unvalidated(
566                                        &id,
567                                        Some(for_.0),
568                                        &self.keychain,
569                                    )?;
570                                }
571                            } else {
572                                // add share
573                                let mode = match key.mode {
574                                    UserAccessMode::Read => ShareMode::Read,
575                                    UserAccessMode::Write => ShareMode::Write,
576                                    UserAccessMode::Owner => continue,
577                                };
578                                merge.add_share_unvalidated(id, for_, mode, &self.keychain)?;
579                            }
580                        }
581
582                        // share deletion due to conflicts
583                        if files_to_unshare.contains(&id) {
584                            merge.delete_share_unvalidated(&id, None, &self.keychain)?;
585                        }
586
587                        // rename due to path conflict
588                        if let Some(&rename_increment) = rename_increments.get(&id) {
589                            let name = NameComponents::from(&local_name)
590                                .generate_incremented(rename_increment)
591                                .to_name();
592                            merge.rename_unvalidated(&id, &name, &self.keychain)?;
593                        }
594
595                        // edit
596                        let base_hmac = maybe_base_file.and_then(|f| f.document_hmac().cloned());
597                        let remote_hmac =
598                            maybe_remote_file.and_then(|f| f.document_hmac().cloned());
599                        let local_hmac = local_file.document_hmac().cloned();
600                        if merge.access_mode(me, &id)? >= Some(UserAccessMode::Write)
601                            && local_hmac != base_hmac
602                        {
603                            if remote_hmac != base_hmac && remote_hmac != local_hmac {
604                                // merge
605                                let merge_name = merge.name(&id, &self.keychain)?;
606                                let document_type =
607                                    DocumentType::from_file_name_using_extension(&merge_name);
608
609                                // todo these accesses are potentially problematic
610                                // maybe not if service/docs is the persion doing network io
611                                let base_document =
612                                    self.read_document_helper(id, &mut base).await?;
613                                let remote_document =
614                                    self.read_document_helper(id, &mut remote).await?;
615                                let local_document =
616                                    self.read_document_helper(id, &mut local).await?;
617
618                                match document_type {
619                                    DocumentType::Text => {
620                                        // 3-way merge
621                                        // todo: a couple more clones than necessary
622                                        let base_document =
623                                            String::from_utf8_lossy(&base_document).to_string();
624                                        let remote_document =
625                                            String::from_utf8_lossy(&remote_document).to_string();
626                                        let local_document =
627                                            String::from_utf8_lossy(&local_document).to_string();
628                                        let merged_document =
629                                            text::buffer::Buffer::from(base_document.as_str())
630                                                .merge(local_document, remote_document);
631                                        let encrypted_document = merge
632                                            .update_document_unvalidated(
633                                                &id,
634                                                &merged_document.into_bytes(),
635                                                &self.keychain,
636                                            )?;
637                                        let hmac = merge.find(&id)?.document_hmac().copied();
638                                        self.docs.insert(id, hmac, &encrypted_document).await?;
639                                    }
640                                    DocumentType::Drawing => {
641                                        let base_document =
642                                            String::from_utf8_lossy(&base_document).to_string();
643                                        let remote_document =
644                                            String::from_utf8_lossy(&remote_document).to_string();
645                                        let local_document =
646                                            String::from_utf8_lossy(&local_document).to_string();
647
648                                        let base_buffer = svg::buffer::Buffer::new(&base_document);
649                                        let remote_buffer =
650                                            svg::buffer::Buffer::new(&remote_document);
651                                        let mut local_buffer =
652                                            svg::buffer::Buffer::new(&local_document);
653
654                                        for (_, el) in local_buffer.elements.iter_mut() {
655                                            if let Element::Path(path) = el {
656                                                path.data.apply_transform(u_transform_to_bezier(
657                                                    &Transform::from(
658                                                        local_buffer
659                                                            .weak_viewport_settings
660                                                            .master_transform,
661                                                    ),
662                                                ));
663                                            }
664                                        }
665                                        svg::buffer::Buffer::reload(
666                                            &mut local_buffer.elements,
667                                            &mut local_buffer.weak_images,
668                                            &mut local_buffer.weak_path_pressures,
669                                            &mut local_buffer.weak_viewport_settings,
670                                            &base_buffer,
671                                            &remote_buffer,
672                                        );
673
674                                        let merged_document = local_buffer.serialize();
675                                        let encrypted_document = merge
676                                            .update_document_unvalidated(
677                                                &id,
678                                                &merged_document.into_bytes(),
679                                                &self.keychain,
680                                            )?;
681                                        let hmac = merge.find(&id)?.document_hmac().copied();
682                                        self.docs.insert(id, hmac, &encrypted_document).await?;
683                                    }
684                                    DocumentType::Chat => {
685                                        // line-union of append-only JSONL turns
686                                        let merged_document = chat::Buffer::merge(
687                                            &base_document,
688                                            &local_document,
689                                            &remote_document,
690                                        );
691                                        let encrypted_document = merge
692                                            .update_document_unvalidated(
693                                                &id,
694                                                &merged_document,
695                                                &self.keychain,
696                                            )?;
697                                        let hmac = merge.find(&id)?.document_hmac().copied();
698                                        self.docs.insert(id, hmac, &encrypted_document).await?;
699                                    }
700                                    DocumentType::Other => {
701                                        // duplicate file
702                                        let merge_parent = *merge.find(&id)?.parent();
703                                        let (duplicate_id, duplicate_key) =
704                                            if let Some(&tracked) = duplicate_file_ids.get(&id) {
705                                                tracked
706                                            } else {
707                                                let duplicate_id = Uuid::new_v4();
708                                                let duplicate_key = symkey::generate_key();
709                                                duplicate_file_ids
710                                                    .insert(id, (duplicate_id, duplicate_key));
711                                                rename_increments.insert(duplicate_id, 1);
712                                                (duplicate_id, duplicate_key)
713                                            };
714
715                                        let mut merge_name = merge_name;
716                                        merge_name = NameComponents::from(&merge_name)
717                                            .generate_incremented(
718                                                rename_increments
719                                                    .get(&duplicate_id)
720                                                    .copied()
721                                                    .unwrap_or_default(),
722                                            )
723                                            .to_name();
724
725                                        merge.create_unvalidated(
726                                            duplicate_id,
727                                            duplicate_key,
728                                            &merge_parent,
729                                            &merge_name,
730                                            FileType::Document,
731                                            &self.keychain,
732                                        )?;
733                                        let encrypted_document = merge
734                                            .update_document_unvalidated(
735                                                &duplicate_id,
736                                                &local_document,
737                                                &self.keychain,
738                                            )?;
739                                        let duplicate_hmac =
740                                            merge.find(&duplicate_id)?.document_hmac().copied();
741                                        self.docs
742                                            .insert(
743                                                duplicate_id,
744                                                duplicate_hmac,
745                                                &encrypted_document,
746                                            )
747                                            .await?;
748                                    }
749                                }
750                            } else {
751                                let local_file = local.find(&id)?;
752                                merge.overwrite_document_hmac_unvalidated(
753                                    &id,
754                                    local_file.document_hmac().copied(),
755                                    local_file.doc_size(),
756                                    &self.keychain,
757                                )?;
758                            }
759                        }
760                    }
761
762                    // deletes
763                    // moves happen first in case a file is moved into a deleted folder
764                    for id in db.local_metadata.ids() {
765                        if db.base_metadata.maybe_find(&id).is_some()
766                            && deletions.calculate_deleted(&id)?
767                            && !merge.calculate_deleted(&id)?
768                        {
769                            // delete
770                            merge.delete_unvalidated(&id, &self.keychain)?;
771                        }
772                    }
773                    for &id in &links_to_delete {
774                        // delete
775                        if merge.maybe_find(&id).is_some() && !merge.calculate_deleted(&id)? {
776                            merge.delete_unvalidated(&id, &self.keychain)?;
777                        }
778                    }
779
780                    merge
781                };
782
783                // validate; handle failures by introducing changeset constraints
784                for link in merge.ids() {
785                    if !merge.calculate_deleted(&link)? {
786                        if let FileType::Link { target } = merge.find(&link)?.file_type() {
787                            if merge.maybe_find(&target).is_some()
788                                && merge.calculate_deleted(&target)?
789                            {
790                                // delete links to deleted files
791                                if links_to_delete.insert(link) {
792                                    continue 'merge_construction;
793                                } else {
794                                    return Err(LbErrKind::Unexpected(format!(
795                                        "sync failed to resolve broken link (deletion): {link:?}"
796                                    ))
797                                    .into());
798                                }
799                            }
800                        }
801                    }
802                }
803
804                let validate_result = merge.validate(me);
805                match validate_result {
806                    // merge changeset is valid
807                    Ok(_) => {
808                        let (_, merge_changes) = merge.unstage();
809                        break merge_changes;
810                    }
811                    Err(ref err) => match err.kind {
812                        LbErrKind::Validation(ref vf) => match vf {
813                            // merge changeset has resolvable validation errors and needs modification
814                            ValidationFailure::Cycle(ids) => {
815                                // revert all local moves in the cycle
816                                let mut progress = false;
817                                for &id in ids {
818                                    if db.local_metadata.maybe_find(&id).is_some()
819                                        && files_to_unmove.insert(id)
820                                    {
821                                        progress = true;
822                                    }
823                                }
824                                if !progress {
825                                    return Err(LbErrKind::Unexpected(format!(
826                                        "sync failed to resolve cycle: {ids:?}"
827                                    ))
828                                    .into());
829                                }
830                            }
831                            ValidationFailure::PathConflict(ids) => {
832                                // pick one local id and generate a non-conflicting filename
833                                let mut progress = false;
834                                for &id in ids {
835                                    if duplicate_file_ids.values().any(|&(dup, _)| dup == id) {
836                                        *rename_increments.entry(id).or_insert(0) += 1;
837                                        progress = true;
838                                        break;
839                                    }
840                                }
841                                if !progress {
842                                    for &id in ids {
843                                        if db.local_metadata.maybe_find(&id).is_some() {
844                                            *rename_increments.entry(id).or_insert(0) += 1;
845                                            progress = true;
846                                            break;
847                                        }
848                                    }
849                                }
850                                if !progress {
851                                    return Err(LbErrKind::Unexpected(format!(
852                                        "sync failed to resolve path conflict: {ids:?}"
853                                    ))
854                                    .into());
855                                }
856                            }
857                            ValidationFailure::SharedLink { link, shared_ancestor } => {
858                                // if ancestor is newly shared, delete share, otherwise delete link
859                                let mut progress = false;
860                                if let Some(base_shared_ancestor) = base.maybe_find(shared_ancestor)
861                                {
862                                    if !base_shared_ancestor.is_shared()
863                                        && files_to_unshare.insert(*shared_ancestor)
864                                    {
865                                        progress = true;
866                                    }
867                                }
868                                if !progress && links_to_delete.insert(*link) {
869                                    progress = true;
870                                }
871                                if !progress {
872                                    return Err(LbErrKind::Unexpected(format!(
873                                    "sync failed to resolve shared link: link: {link:?}, shared_ancestor: {shared_ancestor:?}"
874                                )).into());
875                                }
876                            }
877                            ValidationFailure::DuplicateLink { target } => {
878                                // delete local link with this target
879                                let mut progress = false;
880                                if let Some(link) = local.linked_by(target)? {
881                                    if links_to_delete.insert(link) {
882                                        progress = true;
883                                    }
884                                }
885                                if !progress {
886                                    return Err(LbErrKind::Unexpected(format!(
887                                        "sync failed to resolve duplicate link: target: {target:?}"
888                                    ))
889                                    .into());
890                                }
891                            }
892                            ValidationFailure::BrokenLink(link) => {
893                                // delete local link with this target
894                                if !links_to_delete.insert(*link) {
895                                    return Err(LbErrKind::Unexpected(format!(
896                                        "sync failed to resolve broken link: {link:?}"
897                                    ))
898                                    .into());
899                                }
900                            }
901                            ValidationFailure::OwnedLink(link) => {
902                                // if target is newly owned, unmove target, otherwise delete link
903                                let mut progress = false;
904                                if let Some(remote_link) = remote.maybe_find(link) {
905                                    if let FileType::Link { target } = remote_link.file_type() {
906                                        let remote_target = remote.find(&target)?;
907                                        if remote_target.owner() != me
908                                            && files_to_unmove.insert(target)
909                                        {
910                                            progress = true;
911                                        }
912                                    }
913                                }
914                                if !progress && links_to_delete.insert(*link) {
915                                    progress = true;
916                                }
917                                if !progress {
918                                    return Err(LbErrKind::Unexpected(format!(
919                                        "sync failed to resolve owned link: {link:?}"
920                                    ))
921                                    .into());
922                                }
923                            }
924                            // merge changeset has unexpected validation errors
925                            ValidationFailure::Orphan(_)
926                            | ValidationFailure::NonFolderWithChildren(_)
927                            | ValidationFailure::FileWithDifferentOwnerParent(_)
928                            | ValidationFailure::FileNameTooLong(_)
929                            | ValidationFailure::DeletedFileUpdated(_)
930                            | ValidationFailure::NonDecryptableFileName(_) => {
931                                validate_result?;
932                            }
933                        },
934                        // merge changeset has unexpected errors
935                        _ => {
936                            validate_result?;
937                        }
938                    },
939                }
940            }
941        };
942
943        // base = remote; local = merge
944        (&mut db.base_metadata)
945            .to_staged(remote_changes.clone())
946            .to_lazy()
947            .promote()?;
948        db.local_metadata.clear()?;
949        (&mut db.local_metadata)
950            .to_staged(merge_changes)
951            .to_lazy()
952            .promote()?;
953
954        // todo who else calls this did they manage locks right?
955        // self.cleanup_local_metadata()?;
956        db.base_metadata.stage(&mut db.local_metadata).prune()?;
957
958        if start.elapsed() > web_time::Duration::from_millis(100) {
959            warn!("sync merge held lock for {:?}", start.elapsed());
960        }
961
962        Ok(())
963    }
964
965    async fn send_pull_events(&self, state: &mut SyncState) -> LbResult<()> {
966        if state.new_root.is_some() {
967            self.events.signed_in();
968        }
969
970        if !state.remote_changes.is_empty() {
971            self.events.meta_changed(Actor::Sync);
972
973            let owner = Owner(self.keychain.get_pk()?);
974            if state.remote_changes.iter().any(|f| f.owner() != owner) {
975                self.events.pending_shares_changed();
976            }
977        }
978
979        for &doc in &state.pulled_docs {
980            self.events.doc_written(doc, Actor::Sync);
981        }
982
983        Ok(())
984    }
985
986    async fn commit_last_synced(&self, state: &mut SyncState) -> LbResult<()> {
987        let mut tx = self.begin_tx().await;
988        let db = tx.db();
989        db.last_synced.replace(state.updates_as_of as i64)?;
990
991        if let Some(root) = state.new_root {
992            db.root.replace(root)?;
993        }
994
995        Ok(())
996    }
997
998    async fn populate_pk_cache(&self) -> LbResult<()> {
999        // todo: is this the move?
1000        let mut missing_owners = HashSet::new();
1001        {
1002            let tx = self.ro_tx().await;
1003            let db = tx.db();
1004            for file in db.base_metadata.iter().map(|(_, value)| value) {
1005                for user_access_key in file.user_access_keys() {
1006                    let enc_by = Owner(user_access_key.encrypted_by);
1007                    let enc_for = Owner(user_access_key.encrypted_for);
1008
1009                    if !db.pub_key_lookup.contains_key(&enc_by) {
1010                        missing_owners.insert(enc_by);
1011                    }
1012
1013                    if !db.pub_key_lookup.contains_key(&enc_for) {
1014                        missing_owners.insert(enc_for);
1015                    }
1016                }
1017            }
1018        }
1019
1020        let mut new_owners = HashMap::new();
1021        {
1022            for owner in missing_owners {
1023                let username_result = self
1024                    .client
1025                    .request(self.get_account().unwrap(), GetUsernameRequest { key: owner.0 })
1026                    .await;
1027                new_owners.insert(owner, username_result);
1028            }
1029        }
1030
1031        let mut tx = self.begin_tx().await;
1032        let db = tx.db();
1033
1034        let have_updates = !new_owners.is_empty();
1035        for (owner, username) in new_owners {
1036            let username = match username {
1037                Err(ApiError::Endpoint(GetUsernameError::UserNotFound)) => "<unknown>".to_string(),
1038                Ok(username) => username.username,
1039                _ => continue, // todo: possibly add some logging here
1040            };
1041
1042            db.pub_key_lookup.insert(owner, username).unwrap();
1043        }
1044
1045        if have_updates {
1046            self.events.meta_changed(Actor::Sync);
1047        }
1048
1049        Ok(())
1050    }
1051
1052    /// Updates remote and base metadata to local.
1053    async fn push_meta(&self) -> LbResult<()> {
1054        let mut updates = vec![];
1055        let mut local_changes_no_digests = Vec::new();
1056
1057        let tx = self.ro_tx().await;
1058        let db = tx.db();
1059
1060        // remote = local
1061        let local = db.base_metadata.stage(&db.local_metadata).to_lazy();
1062
1063        for id in local.tree.staged.ids() {
1064            let mut local_change = local.tree.staged.find(&id)?.timestamped_value.value.clone();
1065            let maybe_base_file = local.tree.base.maybe_find(&id);
1066
1067            // change everything but document hmac and re-sign
1068            local_change.set_hmac_and_size(
1069                maybe_base_file.and_then(|f| f.document_hmac().copied()),
1070                maybe_base_file.and_then(|f| *f.timestamped_value.value.doc_size()),
1071            );
1072            let local_change = local_change.sign(&self.keychain)?;
1073
1074            local_changes_no_digests.push(local_change.clone());
1075            let file_diff = FileDiff { old: maybe_base_file.cloned(), new: local_change };
1076            updates.push(file_diff);
1077        }
1078
1079        drop(tx);
1080
1081        if !updates.is_empty() {
1082            self.client
1083                .request(self.get_account()?, UpsertRequestV2 { updates: updates.clone() })
1084                .await?;
1085        }
1086
1087        let mut tx = self.begin_tx().await;
1088        let db = tx.db();
1089
1090        // base = local
1091        (&mut db.base_metadata)
1092            .to_lazy()
1093            .stage(local_changes_no_digests)
1094            .promote()?;
1095        db.base_metadata.stage(&mut db.local_metadata).prune()?;
1096
1097        tx.end();
1098
1099        Ok(())
1100    }
1101
1102    /// Updates remote and base files to local. Assumes metadata is already pushed for all new files.
1103    // todo: make this so that all document updates are attempted and we don't just return the
1104    // first error. Once an attempt is made we can return any or all errors, either would be an
1105    // improvement
1106    async fn push_docs(&self) -> LbResult<()> {
1107        let mut updates = vec![];
1108        let mut local_changes_digests_only = vec![];
1109
1110        let tx = self.ro_tx().await;
1111        let db = tx.db();
1112        let start = Instant::now();
1113
1114        let local = db.base_metadata.stage(&db.local_metadata).to_lazy();
1115
1116        for id in local.tree.staged.ids() {
1117            let base_file = local.tree.base.find(&id)?.clone();
1118
1119            // change only document hmac and re-sign
1120            let mut local_change = base_file.timestamped_value.value.clone();
1121            local_change.set_hmac_and_size(
1122                local.find(&id)?.document_hmac().copied(),
1123                *local.find(&id)?.timestamped_value.value.doc_size(),
1124            );
1125
1126            if base_file.document_hmac() == local_change.document_hmac()
1127                || local_change.document_hmac().is_none()
1128            {
1129                continue;
1130            }
1131
1132            let local_change = local_change.sign(&self.keychain)?;
1133
1134            updates.push(FileDiff { old: Some(base_file), new: local_change.clone() });
1135            local_changes_digests_only.push(local_change);
1136            self.events
1137                .sync_update(SyncIncrement::PushingDocument(id, true));
1138        }
1139
1140        drop(tx);
1141        if start.elapsed() > web_time::Duration::from_millis(100) {
1142            warn!("sync push_docs held lock for {:?}", start.elapsed());
1143        }
1144
1145        let futures = updates.clone().into_iter().map(|diff| self.push_doc(diff));
1146
1147        let mut stream = stream::iter(futures).buffer_unordered(
1148            thread::available_parallelism()
1149                .unwrap_or(NonZeroUsize::new(4).unwrap())
1150                .into(),
1151        );
1152
1153        let mut docs_without_errors = vec![];
1154        let mut last_error: Option<LbErr> = None;
1155
1156        while let Some(fut) = stream.next().await {
1157            match fut {
1158                Ok(id) => {
1159                    docs_without_errors.push(id);
1160                    self.events
1161                        .sync_update(SyncIncrement::PushingDocument(id, false));
1162                }
1163                Err(err) => {
1164                    last_error = Some(err);
1165                }
1166            }
1167        }
1168
1169        local_changes_digests_only.retain(|f| docs_without_errors.contains(f.id()));
1170
1171        let mut tx = self.begin_tx().await;
1172        let db = tx.db();
1173        // base = local (metadata)
1174        (&mut db.base_metadata)
1175            .to_lazy()
1176            .stage(local_changes_digests_only)
1177            .promote()?;
1178
1179        db.base_metadata.stage(&mut db.local_metadata).prune()?;
1180
1181        tx.end();
1182
1183        if let Some(err) = last_error { Err(err) } else { Ok(()) }
1184    }
1185
1186    async fn push_doc(&self, diff: FileDiff<SignedMeta>) -> LbResult<Uuid> {
1187        let id = *diff.new.id();
1188        let hmac = diff.new.document_hmac();
1189        let local_document_change = self.docs.get(id, hmac.copied()).await?;
1190        self.client
1191            .request(
1192                self.get_account()?,
1193                ChangeDocRequestV2 { diff, new_content: local_document_change },
1194            )
1195            .await?;
1196
1197        Ok(id)
1198    }
1199
1200    #[cfg(not(target_family = "wasm"))]
1201    async fn send_debug_info(&self, account: Account) {
1202        use crate::service::debug;
1203
1204        let max_panic_time = debug::latest_panic_time(&self.config.writeable_path)
1205            .await
1206            .unwrap_or_else(|e| {
1207                warn!("could not enumerate panic files: {e:?}");
1208                None
1209            });
1210
1211        let last_sent = {
1212            let tx = self.ro_tx().await;
1213            tx.db().last_extracted_panic.as_ref().copied()
1214        };
1215
1216        let should_send = match (last_sent, max_panic_time) {
1217            (None, _) => true,
1218            (Some(prev), Some(cur)) => cur > prev,
1219            (Some(_), None) => false,
1220        };
1221
1222        if !should_send {
1223            return;
1224        }
1225
1226        let debug_info = self
1227            .debug_info("none provided - sync".to_string(), false)
1228            .await
1229            .unwrap();
1230
1231        let new_marker = max_panic_time.unwrap_or(0);
1232        let bg_self = self.clone();
1233        let task = async move {
1234            match bg_self
1235                .client
1236                .request(&account, UpsertDebugInfoRequest { debug_info })
1237                .await
1238            {
1239                Ok(_) => {
1240                    let mut tx = bg_self.begin_tx().await;
1241                    if let Err(e) = tx.db().last_extracted_panic.replace(new_marker) {
1242                        warn!("could not record last_extracted_panic: {e:?}");
1243                    }
1244                    tx.end();
1245                }
1246                Err(e) => warn!("send_debug_info failed: {e:?}"),
1247            }
1248        };
1249
1250        if self.config.background_work {
1251            tokio::spawn(task);
1252        } else {
1253            task.await;
1254        }
1255    }
1256
1257    async fn read_document_helper<T>(
1258        &self, id: Uuid, tree: &mut LazyTree<T>,
1259    ) -> LbResult<DecryptedDocument>
1260    where
1261        T: TreeLike<F = SignedMeta>,
1262    {
1263        let file = tree.find(&id)?;
1264        validate::is_document(file)?;
1265        let hmac = file.document_hmac().copied();
1266
1267        if tree.calculate_deleted(&id)? {
1268            return Err(LbErrKind::FileNonexistent.into());
1269        }
1270
1271        let doc = match hmac {
1272            Some(hmac) => {
1273                let doc = self.docs.get(id, Some(hmac)).await?;
1274                tree.decrypt_document(&id, &doc, &self.keychain)?
1275            }
1276            None => vec![],
1277        };
1278
1279        Ok(doc)
1280    }
1281
1282    /// for tests only
1283    #[doc(hidden)]
1284    pub async fn server_dirty_ids(&self) -> LbResult<Vec<Uuid>> {
1285        let mut state = self.syncer.lock().await;
1286        self.inital_sync_state(&mut state).await?;
1287        self.process_deletions().await?;
1288        self.fetch_meta(&mut state).await?;
1289
1290        let server_ids = state.remote_changes.iter().map(|f| *f.id()).collect();
1291
1292        Ok(server_ids)
1293    }
1294
1295    pub(crate) fn setup_syncer(&self) {
1296        if self.config.background_work {
1297            self.clone().local_change_worker();
1298            self.clone().periodic_sync_worker();
1299            self.clone().post_sync_worker();
1300        }
1301    }
1302
1303    fn local_change_worker(self) {
1304        #[cfg(not(target_family = "wasm"))]
1305        tokio::spawn(async move {
1306            let mut events = self.subscribe();
1307
1308            let sync_criteria = |e: Event| {
1309                matches!(
1310                    e,
1311                    Event::MetadataChanged(Actor::User(_))
1312                        | Event::DocumentWritten(_, Actor::User(_))
1313                )
1314            };
1315
1316            loop {
1317                time::sleep(Duration::from_millis(500)).await;
1318                let mut should_sync = false;
1319
1320                // drain the current channel, so we don't sync for each keystroke if they pile up
1321                loop {
1322                    let event = events.try_recv();
1323                    match event {
1324                        Ok(event) => {
1325                            if sync_criteria(event) {
1326                                should_sync = true;
1327                            }
1328                        }
1329                        Err(TryRecvError::Empty) => break,
1330                        _ => {
1331                            panic!(
1332                                "unexpected broadcast receive error, returning local_change_worker"
1333                            );
1334                        }
1335                    }
1336                }
1337
1338                // empty channel + nothing interesting has happened, sit and wait for something
1339                // interesting
1340                if !should_sync {
1341                    let event = events.recv().await.unwrap();
1342                    if sync_criteria(event) {
1343                        self.sync().await.map_unexpected().log_and_ignore();
1344                    } else {
1345                        continue;
1346                    }
1347                }
1348            }
1349        });
1350    }
1351
1352    fn periodic_sync_worker(self) {
1353        #[cfg(not(target_family = "wasm"))]
1354        tokio::spawn(async move {
1355            loop {
1356                if self.user_active().await {
1357                    tokio::time::sleep(Duration::from_secs(3)).await;
1358                } else {
1359                    tokio::select! {
1360                        _ = tokio::time::sleep(Duration::from_secs(5 * 60)) => {}
1361                        _ = self.user_wake.notified() => {}
1362                    }
1363                }
1364                self.sync().await.map_unexpected().log_and_ignore();
1365            }
1366        });
1367    }
1368
1369    pub(crate) async fn user_active(&self) -> bool {
1370        let last_seen = self.user_last_seen.read().await;
1371        last_seen.elapsed() < Duration::from_secs(15)
1372    }
1373
1374    fn post_sync_worker(self) {
1375        #[cfg(not(target_family = "wasm"))]
1376        tokio::spawn(async move {
1377            let mut events = self.subscribe();
1378
1379            loop {
1380                let event = events.recv().await.unwrap();
1381                if let Event::Sync(SyncIncrement::SyncFinished(_)) = event {
1382                    self.fetcher().await.map_unexpected().log_and_ignore();
1383                    self.populate_pk_cache()
1384                        .await
1385                        .map_unexpected()
1386                        .log_and_ignore();
1387                };
1388            }
1389        });
1390    }
1391
1392    async fn fetcher(&self) -> LbResult<()> {
1393        let mut files_to_pull = vec![];
1394
1395        let tx = self.ro_tx().await;
1396        let db = tx.db();
1397
1398        let Some(root) = db.root.as_ref() else {
1399            return Ok(());
1400        };
1401
1402        // we can only fetch things we know the server knows about
1403        let mut tree = db.base_metadata.stage(None).to_lazy();
1404
1405        for id in tree.descendants_using_links(root)? {
1406            let file = tree.find(&id)?;
1407            let hmac = file.document_hmac().copied();
1408
1409            // skip non-documents
1410            if !file.is_document() {
1411                continue;
1412            }
1413
1414            // skip deleted files
1415            if tree.calculate_deleted(&id)? {
1416                continue;
1417            }
1418
1419            if self.docs.exists(id, hmac) {
1420                continue;
1421            }
1422
1423            // skip non-first-party files
1424            let name = tree.name(&id, &self.keychain)?;
1425            if !name.ends_with(".md") && !name.ends_with(".svg") {
1426                continue;
1427            }
1428
1429            files_to_pull.push((id, hmac));
1430        }
1431
1432        drop(tx);
1433
1434        // this could all be done in parallel, but for now going to not do it that way
1435        // benefits: less work, but also ensures that a file that needs to be fetched immediately
1436        // can be
1437        for (id, hmac) in files_to_pull {
1438            if let Some(hmac) = hmac {
1439                self.ensure_doc_available(id, hmac).await?;
1440            }
1441        }
1442
1443        Ok(())
1444    }
1445}