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