1use super::*;
5#[cfg(test)]
6use coven_crypto::custody::Keychain;
7use coven_crypto::custody::{InMemoryCustody, PersonEntries, StoreKeychain};
8use coven_database::Database;
9use coven_format::codes::{InviteCode, RestoreCode};
10use coven_foundation::files::StoreDirAccess;
11use coven_foundation::files::{
12 BootstrapDirectoryError, BootstrapStore, ReplacedBy, ReplacementStore,
13};
14use coven_storage::{
15 ConnectionCredentials, InviteStorage, RestoreStorage, StorageCredentials, StorageFailure,
16};
17use coven_sync::{HeldIntervals, Parks, SequenceSync, SyncOften};
18use std::{sync::Arc, time::Duration};
19use tokio::sync::watch;
20
21#[path = "bootstrap_authentication.rs"]
22mod authentication;
23use authentication::{account_sign_in, join_sign_in, provider_auth, SignInCustody};
24use coven_sync::SessionSignIn;
25
26#[path = "bootstrap_commit.rs"]
27mod commit;
28
29#[path = "bootstrap_join.rs"]
30mod join;
31
32#[path = "bootstrap_admitted.rs"]
33mod admitted;
34
35#[path = "bootstrap_replace.rs"]
36mod replace;
37pub(super) use replace::{restore_device, resume_replacement};
38
39#[path = "bootstrap_reset.rs"]
40mod reset;
41pub(super) use reset::reset_device;
42
43#[path = "bootstrap_restore.rs"]
44mod restore;
45#[cfg(test)]
46use restore::keychain_code;
47pub use restore::{restore_from_code, restore_from_saved_code};
48pub(super) use restore::{resume_restore, unfinished_restores};
49
50#[path = "restore_credentials.rs"]
51mod credentials;
52use credentials::restore_connection;
53
54#[path = "bootstrap_unfinished.rs"]
55mod unfinished;
56use unfinished::{begin_installation, kept, Kept};
57
58#[path = "bootstrap_end.rs"]
59mod end;
60use end::{record_ending, remove_unfinished};
61
62#[path = "bootstrap_error.rs"]
63mod error;
64pub use error::BootstrapError;
65
66#[path = "bootstrap_status.rs"]
67mod status;
68pub use status::BootstrapStatus;
69use status::{loading, open_installation};
70
71pub async fn join_with_invite(
91 builder: CovenBuilder,
92 code: &str,
93 joiner: Joiner,
94 device_name: &str,
95 on_status: impl Fn(BootstrapStatus),
96 control: &JoinControl,
97) -> Result<Option<CovenHandle>, BootstrapError> {
98 let cancel = control.cancel.subscribe();
101 Box::pin(bootstrap_device(
102 builder,
103 BootstrapRequest::Join {
104 code: coven_sync::read_invite_code(code)?,
105 terms: RequestTerms::Given { device_name: device_name.into(), joiner },
106 held: control.often.subscribe(),
107 parks: control.parks.clone(),
108 },
109 on_status,
110 &cancel,
111 ))
112 .await
113}
114
115pub(super) async fn unfinished_joins(
119 builder: &CovenBuilder,
120) -> Result<Vec<InviteInfo>, BootstrapError> {
121 let keychain = builder.keychain()?;
122 let mut joins = Vec::new();
123 for store in builder
124 .directories
125 .unfinished_first_installations()
126 .await
127 .map_err(BootstrapDirectoryError::from)?
128 {
129 let entries = StoreKeychain::new(keychain.clone(), store);
132 let Kept::Join(text) = kept(builder, &entries, store).await? else {
133 continue;
134 };
135 let info = match coven_sync::decode_code_info(text.as_str())? {
136 CodeInfo::Invite(info) => info,
137 CodeInfo::Restore(_) => {
138 return Err(CodeError::WrongKind {
139 expected: CodeKind::Invite,
140 actual: CodeKind::Restore,
141 }
142 .into())
143 }
144 };
145 CodeError::require_store(store, info.metadata.store_id)?;
146 joins.push(info);
147 }
148 Ok(joins)
149}
150
151pub(super) async fn resume_join(
154 builder: CovenBuilder,
155 store: StoreId,
156 on_status: impl Fn(BootstrapStatus),
157 control: &JoinControl,
158) -> Result<Option<CovenHandle>, BootstrapError> {
159 let code = saved_invite_code(&StoreKeychain::new(builder.keychain()?, store), store)?
160 .ok_or(BootstrapError::NoUnfinishedJoin(store))?;
161 let cancel = control.cancel.subscribe();
162 Box::pin(bootstrap_device(
163 builder,
164 BootstrapRequest::Join {
165 code,
166 terms: RequestTerms::Saved,
167 held: control.often.subscribe(),
168 parks: control.parks.clone(),
169 },
170 on_status,
171 &cancel,
172 ))
173 .await
174}
175
176fn saved_invite_code(
178 keychain: &StoreKeychain,
179 store: StoreId,
180) -> Result<Option<InviteCode>, BootstrapError> {
181 let Some(text) = keychain.invite_code()? else {
182 return Ok(None);
183 };
184 let code = coven_sync::read_invite_code(text.as_str())?;
185 CodeError::require_store(store, code.store)?;
186 Ok(Some(code))
187}
188
189pub struct JoinControl {
192 cancel: watch::Sender<bool>,
193 often: SyncOften,
194 parks: Parks,
196}
197
198impl JoinControl {
199 pub fn new() -> Self {
201 Self { cancel: watch::channel(false).0, often: SyncOften::new(), parks: Parks::new() }
202 }
203
204 pub fn cancel(&self) {
206 self.cancel.send_replace(true);
207 }
208
209 pub fn check_often(&self, interval: Duration) -> SyncOftenGuard {
215 self.often.hold(interval)
216 }
217
218 #[cfg(any(test, feature = "test-utils"))]
221 pub fn parked_from_now(&self) -> impl std::future::Future<Output = ()> + Send + 'static {
222 self.parks.parked_from_now()
223 }
224}
225
226enum BootstrapRequest {
227 Restore {
228 code: RestoreCode,
229 data: ConnectionCredentials,
230 sign_in: SignInCustody,
233 name: String,
234 },
235 Join {
236 code: InviteCode,
237 terms: RequestTerms,
238 held: HeldIntervals,
240 parks: Parks,
241 },
242}
243
244enum RequestTerms {
246 Given { device_name: String, joiner: Joiner },
248 Saved,
250}
251
252async fn bootstrap_device(
255 mut builder: CovenBuilder,
256 request: BootstrapRequest,
257 status: impl Fn(BootstrapStatus),
258 cancel: &watch::Receiver<bool>,
259) -> Result<Option<CovenHandle>, BootstrapError> {
260 if matches!(request, BootstrapRequest::Restore { .. }) {
261 check_cancel(cancel)?;
262 }
263 let keychain = builder.keychain()?;
264 let id = match &request {
265 BootstrapRequest::Restore { code, .. } => code.store,
266 BootstrapRequest::Join { code, .. } => code.store,
267 };
268 let device_entries = StoreKeychain::new(keychain.clone(), id);
269 let clock = builder.clock.clone();
270 let started = clock.elapsed();
271 #[cfg(test)]
272 let recorder = builder.loading_recorder.clone();
273 if builder.directories.deletion_recorded(id).await.map_err(BootstrapDirectoryError::from)? {
276 let directory = builder.directories.store_dir(&id);
277 let keys = StoreKeychain::new(keychain.clone(), id);
278 crate::coven::finish_deletion::<BootstrapError>(&directory, keys).await?;
279 }
280 let pending = begin_installation(&builder, id, &device_entries).await?;
281 let other = match &request {
286 BootstrapRequest::Restore { .. } => device_entries.invite_code()?.is_some(),
287 BootstrapRequest::Join { .. } => device_entries.restore_record()?.is_some(),
288 };
289 if other {
290 return Err(BootstrapDirectoryError::from(StoreLockError::BootstrapPending(id)).into());
291 }
292 if let BootstrapRequest::Join { code, .. } = &request {
296 if coven_sync::ended_for(&pending.directory(), code)? {
297 return remove_unfinished(&device_entries, &pending, Ok(None));
298 }
299 }
300 let result =
301 prepare_and_load(&mut builder, Installing::New(&pending), &request, None, &status, cancel)
302 .await;
303 match result {
304 Ok(Some(Loaded {
305 code,
306 location,
307 provider,
308 credentials,
309 sign_in,
310 ring,
311 database,
312 storage,
313 state,
314 waits,
315 invitee,
316 })) => {
317 let requests = storage.requests();
318 status(BootstrapStatus::Committing);
319 let name = match &request {
324 BootstrapRequest::Join { code, .. } => {
325 commit::joined_name(&database, code, invitee).await
326 }
327 BootstrapRequest::Restore { .. } => commit::loaded_name(&database).await,
328 };
329 let name = match name {
330 Ok(name) => crate::storage::record_opened(
331 &database,
332 &clock,
333 coven_database::RecordedLocation::Connected(provider),
334 Some(clock.now()),
335 )
336 .await
337 .map(|()| name)
338 .map_err(BootstrapError::from),
339 Err(error) => Err(error),
340 };
341 let result = match name.and_then(|name| {
342 if matches!(request, BootstrapRequest::Restore { .. })
346 && check_cancel(cancel).is_err()
347 {
348 return Err(BootstrapError::RestoreRegistered);
349 }
350 builder.authentication.take();
351 let credentials = match &sign_in {
352 SignInCustody::Device { credentials, .. } => Some(credentials.clone()),
356 _ => None,
357 };
358 let owners = builder.owners(
359 pending.directory(),
360 Arc::new(StoreKeychain::new(keychain, id)),
361 crate::builder::Continuing::Loaded { state, storage, credentials },
362 )?;
363 Ok((owners, name))
364 }) {
365 Ok((owners, name)) => {
368 let publishing = clock.elapsed();
369 let published = commit::publish_bootstrap(
370 &pending,
371 code,
372 location,
373 name,
374 credentials,
375 sign_in,
376 ring,
377 owners,
378 database.clone(),
379 waits,
380 )
381 .await;
382 tracing::info!(
383 published = published.is_ok(),
384 time = ?(clock.elapsed() - publishing),
385 "committed keys and credentials and published the store"
386 );
387 tracing::info!(
388 restore = matches!(request, BootstrapRequest::Restore { .. }),
389 published = published.is_ok(),
390 time = ?(clock.elapsed() - started),
391 %requests,
392 "ended the new installation's bootstrap"
393 );
394 published
395 }
396 Err(error) => Err(error),
397 };
398 let result = match &request {
401 BootstrapRequest::Join { .. } => {
402 forget_kept(result, || device_entries.delete_invite_code())
403 }
404 BootstrapRequest::Restore { .. } => {
405 forget_kept(result, || device_entries.delete_restore_record())
406 }
407 };
408 match result {
409 Ok(handle) => match &request {
410 BootstrapRequest::Join { .. } => {
411 let (progress, watch) = watch::channel(None);
412 #[cfg(test)]
413 if let Some(recorder) = &recorder {
414 recorder.watch(watch.clone());
415 }
416 admitted::load_joined(handle, &status, &clock, progress, watch)
417 .await
418 .map(Some)
419 }
420 BootstrapRequest::Restore { .. } => Ok(Some(handle)),
421 },
422 Err(error @ BootstrapError::Published { .. }) => Err(error),
423 Err(error) => combine(Err(error), close_database(&database).await),
426 }
427 }
428 Ok(None) => match record_ending(&request, &pending) {
430 Ok(()) => remove_unfinished(&device_entries, &pending, Ok(None)),
431 Err(error) => Err(error),
432 },
433 Err(error) => match &request {
434 BootstrapRequest::Restore { .. } => {
435 restore::ended(&builder, &pending, &device_entries, error).await
436 }
437 BootstrapRequest::Join { .. } if matches!(error, BootstrapError::Cancelled) => {
438 match record_ending(&request, &pending) {
439 Ok(()) => remove_unfinished(&device_entries, &pending, Err(error)),
440 Err(recording) => combine(Err(error), Err(recording)),
441 }
442 }
443 BootstrapRequest::Join { .. } => Err(error),
446 },
447 }
448}
449
450fn forget_kept(
454 result: Result<CovenHandle, BootstrapError>,
455 delete: impl FnOnce() -> Result<(), KeyError>,
456) -> Result<CovenHandle, BootstrapError> {
457 let (handle, published) = match result {
458 Ok(handle) => (handle, None),
459 Err(BootstrapError::Published { handle, source }) => (handle, Some(*source)),
460 Err(error) => return Err(error),
461 };
462 let deleted = delete().map_err(BootstrapError::from);
463 match (published.map_or(Ok(()), Err), deleted) {
464 (Ok(()), Ok(())) => Ok(handle),
465 (failed, deleted) => Err(BootstrapError::Published {
466 handle,
467 source: Box::new(combine(failed, deleted).expect_err("a failure")),
468 }),
469 }
470}
471
472#[derive(Clone, Copy)]
475enum Installing<'a> {
476 New(&'a BootstrapStore),
477 Replacing(&'a ReplacementStore),
478}
479
480impl Installing<'_> {
481 fn directory(&self) -> StoreDir {
482 match self {
483 Self::New(pending) => pending.directory(),
484 Self::Replacing(replacement) => replacement.directory(),
485 }
486 }
487}
488
489enum Admission {
490 Restoring,
491 Joining(join::Joining),
492}
493
494impl Admission {
495 fn cancellable<T>(&self, result: Result<T, BootstrapError>) -> Result<T, BootstrapError> {
499 match (result, self) {
500 (Err(BootstrapError::Cancelled), Self::Joining(joining)) if joining.sent() => {
501 Err(BootstrapError::JoinRequestSent)
502 }
503 (result, _) => result,
504 }
505 }
506
507 fn accepted(&self) -> bool {
510 matches!(self, Self::Joining(joining) if joining.accepted())
511 }
512
513 async fn leave_cancelled<T>(
517 &self,
518 sync: &SequenceSync,
519 result: Result<T, BootstrapError>,
520 ) -> Result<T, BootstrapError> {
521 match (result, self) {
522 (Err(BootstrapError::Cancelled), Self::Joining(joining)) if joining.accepted() => {
523 combine(Err(BootstrapError::Cancelled), joining.leave(sync).await)
524 }
525 (result, _) => result,
526 }
527 }
528}
529
530enum Finishing {
532 Publish { labelled: StorageConfig },
536 Register { access: coven_format::MemberAccess, registered: bool, labelled: StorageConfig },
540}
541
542struct Loaded {
544 code: RestoreCode,
545 location: StorageConfig,
548 provider: CloudProvider,
550 credentials: StorageCredentials,
551 sign_in: SignInCustody,
552 ring: StoreKeyring,
553 database: Database,
554 storage: Arc<coven_storage::StorageConnection>,
555 state: Arc<coven_sync::SequenceState>,
557 waits: Arc<coven_sync::RetryWaits>,
558 invitee: Option<String>,
561}
562
563async fn prepare_and_load(
569 builder: &mut CovenBuilder,
570 pending: Installing<'_>,
571 request: &BootstrapRequest,
572 prepare: Option<replace::Prepare>,
573 status: &impl Fn(BootstrapStatus),
574 cancel: &watch::Receiver<bool>,
575) -> Result<Option<Loaded>, BootstrapError> {
576 let directory = pending.directory();
577 let (member, mut data, mut sign_in, device_name, mut admission) = match request {
578 BootstrapRequest::Restore { code, data, sign_in, name } => (
579 MemberIdentity::new(code.member, code.member_keys.clone()),
580 data.clone(),
581 sign_in.clone(),
582 name.clone(),
583 Admission::Restoring,
584 ),
585 BootstrapRequest::Join { code, terms, held, parks } => {
586 let (joining, data, sign_in) = join::prepare_request(
587 builder,
588 &directory,
589 code,
590 terms,
591 held.clone(),
592 parks.clone(),
593 )?;
594 (
595 joining.identity(),
596 data,
597 sign_in,
598 joining.device_name().to_owned(),
599 Admission::Joining(joining),
600 )
601 }
602 };
603 if !admission.accepted() {
606 admission.cancellable(check_cancel(cancel))?;
607 }
608 let recording = match (request, pending) {
611 (BootstrapRequest::Restore { code, name, .. }, Installing::New(_)) => {
612 Some(restore::Recording::new(builder, code.store, name)?)
613 }
614 (BootstrapRequest::Restore { code, name, .. }, Installing::Replacing(_)) => {
615 Some(restore::Recording::replacement(builder, code.store, name)?)
616 }
617 _ => None,
618 };
619 let mut prepare = prepare;
620 let provider = data.location.provider();
621 let settings = directory.settings().map_err(CovenError::from)?;
622 let state = Arc::new(coven_sync::SequenceState::new());
623 let ring = Arc::new(KeySession::open(Arc::new(InMemoryCustody::<StoreKeyring>::empty()))?);
624 let identity = Arc::new(KeySession::open(Arc::new(InMemoryCustody::new(member.clone())))?);
625 let replacement = match pending {
627 Installing::Replacing(replacement) => Some(replacement),
628 Installing::New(_) => None,
629 };
630 let clock = builder.clock.clone();
633 let database = open_installation(builder.database(directory.clone())?, status).await?;
634 status(BootstrapStatus::Connecting);
635 let connected = async {
636 let connecting = clock.elapsed();
637 let waits = Arc::new(
638 coven_sync::RetryWaits::open(database.clone(), builder.clock.clone())
639 .await
640 .map_err(CovenError::from)?,
641 );
642 data.location = crate::authentication::label_location(
643 builder.connector().as_ref(),
644 data.location.clone(),
645 provider_auth(&data, &sign_in)?,
646 waits.clone(),
647 )
648 .await
649 .map_err(SyncError::from)?;
650 let storage = builder
651 .connector()
652 .connect(
653 data.location.clone(),
654 provider_auth(&data, &sign_in)?,
655 settings.device_id,
656 state.clone(),
657 waits.clone(),
658 builder.transfers.clone(),
659 )
660 .await
661 .map_err(SyncError::from)?;
662 if matches!(admission, Admission::Restoring) {
663 storage.check_location_claim(settings.id).await.map_err(SyncError::from)?;
664 }
665 tracing::info!(
666 ?provider,
667 time = ?(clock.elapsed() - connecting),
668 requests = %storage.requests(),
669 "connected to storage"
670 );
671 let files = coven_sync::Files::new(
674 coven_database::FileDatabase::new(database.clone()),
675 directory.clone(),
676 None,
677 state.clone(),
678 waits.clone(),
679 builder.ids.clone(),
680 builder.transfers.clone(),
681 clock.clone(),
682 coven_foundation::network_watch::NetworkWatch::unwatched().view(),
683 );
684 let sync = SequenceSync::open(
685 database.clone(),
686 storage.clone(),
687 directory.clone(),
688 settings.device_id,
689 ring.clone(),
690 identity,
691 Arc::new(PersonEntries::new(builder.keychain()?, settings.id)),
692 state.clone(),
693 builder.clock.clone(),
694 builder.ids.clone(),
695 waits.clone(),
696 files.clone(),
697 )
698 .await;
699 match sync {
700 Ok(sync) => Ok::<_, BootstrapError>((waits, storage, files, Arc::new(sync))),
701 Err(error) => {
702 files.close().await;
703 Err(error.into())
704 }
705 }
706 }
707 .await;
708 let (waits, storage, files, sync) = match connected {
709 Ok(connected) => connected,
710 Err(error) => {
711 return combine(sign_in.refused(provider, Err(error)), close_database(&database).await)
712 }
713 };
714 #[cfg(test)]
715 if let Some(recorder) = &builder.loading_recorder {
716 recorder.watch_progress(sync.subscribe_progress());
717 }
718 let mut accepted_label = None;
722 let loading = async {
723 if let Admission::Joining(joining) = &mut admission {
724 if !joining.accept(&sync, status).await? {
725 return Ok(None);
726 }
727 let labelled = storage.labelled_config().await.map_err(SyncError::from)?;
728 accepted_label = Some(labelled.clone());
729 if !joining.admit(&sync, &database, status).await? {
730 return Ok(None);
731 }
732 return Ok(Some(Finishing::Publish { labelled }));
736 }
737 let unlocking = (clock.elapsed(), storage.requests());
741 loading(&sync, sync.unlock(), status).await?;
742 tracing::info!(
743 time = ?(clock.elapsed() - unlocking.0),
744 requests = %storage.requests().since(&unlocking.1),
745 "read the store forward with this member's keys"
746 );
747 let labelled = storage.labelled_config().await.map_err(SyncError::from)?;
749 let registered = database
750 .store_state()
751 .await?
752 .devices
753 .get(&settings.device_id)
754 .is_some_and(|device| device.member == member.member());
755 status(BootstrapStatus::Registering);
759 let access = crate::storage::member_access(&data.credentials, &storage)
760 .await
761 .map_err(SyncError::from)?;
762 sync.check_access(&access).await?;
763 Ok::<_, BootstrapError>(Some(Finishing::Register { access, registered, labelled }))
764 };
765 let mut cancellation = cancel.clone();
766 let loaded = tokio::select! {
767 biased;
768 _ = cancelled(&mut cancellation) => Err(BootstrapError::Cancelled),
769 result = loading => result,
770 };
771 let loaded = match (loaded, &admission, accepted_label) {
775 (Err(BootstrapError::Cancelled), Admission::Joining(_), Some(labelled))
776 if ring.read()?.is_some() =>
777 {
778 Ok(Some(Finishing::Publish { labelled }))
779 }
780 (loaded, ..) => loaded,
781 };
782 let result = match loaded {
783 Ok(Some(Finishing::Publish { labelled })) => {
785 data.location = labelled;
786 Ok(true)
787 }
788 Ok(Some(Finishing::Register { access, registered, labelled })) => {
789 data.location = labelled;
790 let registering = async {
794 if !registered {
795 check_cancel(cancel)?;
796 match (replacement, &recording) {
797 (Some(replacement), Some(recording)) => {
798 sign_in = replace::record_replacement(
799 replacement,
800 recording,
801 &database,
802 prepare.take(),
803 (&member, settings.id, &data),
804 sign_in.clone(),
805 )
806 .await?;
807 }
808 (None, Some(recording)) => {
809 sign_in =
810 recording.record(&member, settings.id, &data, sign_in.clone())?;
811 }
812 _ => {}
813 }
814 let replaces =
815 replacement.map(|replacement| replacement.device_reset().old_device);
816 let registering = (clock.elapsed(), storage.requests());
817 sync.register_device(device_name, replaces).await?;
818 tracing::info!(
819 time = ?(clock.elapsed() - registering.0),
820 requests = %storage.requests().since(®istering.1),
821 "registered this device"
822 );
823 }
824 let state = database.store_state().await?;
825 if let Some(change) =
826 crate::storage::access_change(&state, member.member(), access)?
827 {
828 check_cancel(cancel)?;
829 sync.administer(change).await?;
830 }
831 if let Some(replacement) = replacement {
833 check_cancel(cancel)?;
834 #[cfg(test)]
835 if builder.fail_device_reset_notice {
836 database.test_fail_device_reset().await.map_err(CovenError::from)?;
837 }
838 reset::carry_discarded_provider_work(&sync, &database, replacement).await?;
839 }
840 Ok::<_, BootstrapError>(true)
841 };
842 tokio::pin!(registering);
843 let ended = tokio::select! {
844 biased;
845 _ = cancelled(&mut cancellation) => None,
846 result = &mut registering => Some(result),
847 };
848 match ended {
849 Some(result) => result,
850 None => match registering.await {
851 Err(error) if crate::task::CarriesBug::bug(&error).is_some() => Err(error),
852 _ => Err(BootstrapError::Cancelled),
853 },
854 }
855 }
856 Ok(None) => Ok(false),
857 Err(error) => Err(error),
858 };
859 let result = admission.leave_cancelled(&sync, admission.cancellable(result)).await;
860 let result = sign_in.refused(provider, result);
861 files.close().await;
862 drop(sync);
863 let result = result.and_then(|loaded| {
864 if !loaded {
865 return Ok(None);
866 }
867 let ring = ring.read()?.ok_or(SyncError::from(StorageFailure::MemberKeysMissing))?;
868 Ok(Some((
869 coven_sync::location_code(settings.id, &member, data.location.clone())?,
870 data.location,
871 data.credentials,
872 ring,
873 )))
874 });
875 let invitee = match &admission {
876 Admission::Joining(joining) => joining.invitee(),
877 Admission::Restoring => None,
878 };
879 match result {
880 Ok(Some((code, location, credentials, ring))) => Ok(Some(Loaded {
881 code,
882 location,
883 provider,
884 credentials,
885 sign_in,
886 ring,
887 database,
888 storage,
889 state,
890 waits,
891 invitee,
892 })),
893 result => {
894 let closed = close_database(&database).await;
895 combine(result.map(|_| None), closed)
896 }
897 }
898}
899
900fn check_cancel(cancel: &watch::Receiver<bool>) -> Result<(), BootstrapError> {
901 if *cancel.borrow() {
902 Err(BootstrapError::Cancelled)
903 } else {
904 Ok(())
905 }
906}
907
908async fn cancelled(cancel: &mut watch::Receiver<bool>) {
909 loop {
910 if *cancel.borrow_and_update() {
911 return;
912 }
913 if cancel.changed().await.is_err() {
914 std::future::pending::<()>().await;
915 }
916 }
917}
918
919async fn close_database(database: &Database) -> Result<(), BootstrapError> {
921 database.close().await.map_err(CovenError::from).map_err(BootstrapError::from)
922}
923
924fn combine<T>(
925 result: Result<T, BootstrapError>,
926 cleanup: Result<(), BootstrapError>,
927) -> Result<T, BootstrapError> {
928 match (result, cleanup) {
929 (result, Ok(())) => result,
930 (Ok(_), Err(error)) => Err(error),
931 (Err(operation), Err(cleanup)) => Err(BootstrapError::Cleanup {
932 operation: Box::new(operation),
933 cleanup: Box::new(cleanup),
934 }),
935 }
936}
937
938#[cfg(test)]
941fn files_under(path: &std::path::Path) -> Vec<(std::path::PathBuf, Option<Vec<u8>>)> {
942 let mut found = Vec::new();
943 for entry in std::fs::read_dir(path).unwrap() {
944 let entry = entry.unwrap();
945 let relative = std::path::PathBuf::from(entry.file_name());
946 if entry.file_type().unwrap().is_dir() {
947 found.extend(
948 files_under(&entry.path())
949 .into_iter()
950 .map(|(inner, bytes)| (relative.join(inner), bytes)),
951 );
952 found.push((relative, None));
953 } else {
954 found.push((relative, Some(std::fs::read(entry.path()).unwrap())));
955 }
956 }
957 found.sort();
958 found
959}
960
961#[cfg(test)]
962#[path = "bootstrap_tests.rs"]
963mod tests;