1use super::*;
10use coven_crypto::custody::StoreKeychain;
11use coven_database::{OperationOutcome, OperationRejection, UndoCause};
12use coven_sync::SetupJournal;
13
14pub(crate) enum SetupRun {
16 Idle,
18 Called { cancelled: bool },
21 Checking { cancelled: bool },
24 Finishing,
27}
28
29pub(super) struct SetupCall(Arc<StorageConnections>);
32
33impl SetupCall {
34 fn journaled(self) -> Result<bool, StorageSetupError> {
37 let mut run = lock_unpoisoned(&self.0.setup);
38 match *run {
39 SetupRun::Called { cancelled } => {
40 *run = SetupRun::Checking { cancelled };
41 Ok(cancelled)
42 }
43 SetupRun::Idle | SetupRun::Checking { .. } | SetupRun::Finishing => {
44 Err(setup_error(DbError::Invariant { what: "a setup call runs alone" }.into()))
45 }
46 }
47 }
48}
49
50impl Drop for SetupCall {
51 fn drop(&mut self) {
52 let mut run = lock_unpoisoned(&self.0.setup);
53 if matches!(*run, SetupRun::Called { .. }) {
54 *run = SetupRun::Idle;
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum SetupCancellation {
62 Undoing,
65 Finishing,
67 NoSetup,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub(crate) enum SetupStop {
75 Called,
77 Journaled,
79 Detected,
81 Checked,
83 KeyKept,
85 Claiming,
87 Created,
89 Committed,
91 Finished,
93}
94
95#[cfg(test)]
97pub(crate) enum SetupHook {
98 Crash(SetupStop),
101 Pause {
103 at: SetupStop,
104 entered: tokio::sync::oneshot::Sender<()>,
105 resume: tokio::sync::oneshot::Receiver<()>,
106 },
107}
108
109#[cfg(test)]
111#[derive(Debug, thiserror::Error)]
112#[error("setup stopped as a crash would")]
113struct Crashed;
114
115#[cfg(test)]
117fn crashed(error: &(dyn std::error::Error + 'static)) -> bool {
118 let mut cause = Some(error);
119 while let Some(error) = cause {
120 if error.is::<Crashed>() {
121 return true;
122 }
123 cause = error.source();
124 }
125 false
126}
127
128struct FinishingConnections {
131 storage: Arc<StorageConnection>,
132 setup: Arc<StorageConnection>,
133}
134
135#[derive(Debug, thiserror::Error)]
137#[error("{operation}; cleanup failed: {cleanup}")]
138struct SetupCleanup {
139 operation: StorageSetupError,
140 #[source]
141 cleanup: SyncError,
142}
143
144pub(super) fn with_cleanup(operation: StorageSetupError, cleanup: SyncError) -> StorageSetupError {
145 StorageSetupError::Internal(Box::new(SetupCleanup { operation, cleanup }))
146}
147
148pub(crate) async fn settle_opened(
154 database: &coven_database::Database,
155 keychain: &Arc<StoreKeychain>,
156 keys: &Arc<KeySession<StoreKeyring>>,
157) -> Result<bool, SyncError> {
158 let running = SetupJournal::running(database).await?;
159 if running.as_ref().is_some_and(|journal| journal.location().is_some()) {
160 return Ok(true);
161 }
162 let creating = running.as_ref().is_some_and(SetupJournal::creating);
163 let (kept, held) = (keychain.clone(), keys.clone());
164 let bugs = database.bugs().clone();
165 crate::task::blocking_completing("undo storage setup's local state", bugs, move || {
166 if creating {
169 held.forget()?;
170 }
171 if kept.setup_credentials()?.is_some() {
172 kept.delete_setup_credentials()?;
173 }
174 Ok::<_, KeyError>(())
175 })
176 .await??;
177 if let Some(journal) = running {
178 journal.end(database, OperationOutcome::Undone(UndoCause::Interrupted)).await?;
179 }
180 Ok(false)
181}
182
183impl StorageConnections {
184 pub(crate) fn cancel_setup(&self) -> SetupCancellation {
187 let answer = match &mut *lock_unpoisoned(&self.setup) {
188 SetupRun::Idle => SetupCancellation::NoSetup,
189 SetupRun::Called { cancelled } | SetupRun::Checking { cancelled } => {
190 *cancelled = true;
191 SetupCancellation::Undoing
192 }
193 SetupRun::Finishing => SetupCancellation::Finishing,
194 };
195 self.setup_cancelled.notify_waiters();
196 answer
197 }
198
199 pub(super) fn setup_finishing(&self) -> bool {
201 matches!(*lock_unpoisoned(&self.setup), SetupRun::Finishing)
202 }
203
204 pub(super) fn check_cancel(&self) -> Result<(), StorageSetupError> {
205 match *lock_unpoisoned(&self.setup) {
206 SetupRun::Checking { cancelled: true } => Err(StorageSetupError::Cancelled),
207 _ => Ok(()),
208 }
209 }
210
211 async fn cancelled(&self) {
213 loop {
214 let notified = self.setup_cancelled.notified();
215 tokio::pin!(notified);
216 notified.as_mut().enable();
217 if self.check_cancel().is_err() {
218 return;
219 }
220 notified.await;
221 }
222 }
223
224 pub(super) async fn reached(
226 &self,
227 #[cfg_attr(not(test), expect(unused_variables, reason = "only tests stop setup"))]
228 at: SetupStop,
229 ) -> Result<(), StorageError> {
230 #[cfg(test)]
231 {
232 let hook = {
233 let mut held = lock_unpoisoned(&self.setup_hook);
234 match &*held {
235 Some(SetupHook::Crash(stop) | SetupHook::Pause { at: stop, .. })
236 if *stop == at =>
237 {
238 held.take()
239 }
240 _ => None,
241 }
242 };
243 match hook {
244 Some(SetupHook::Crash(_)) => {
245 return Err(StorageFailure::Network.with_source(Crashed));
246 }
247 Some(SetupHook::Pause { entered, resume, .. }) => {
248 entered.send(()).expect("the test waits for the pause");
249 resume.await.expect("the test resumes the pause");
250 }
251 None => {}
252 }
253 }
254 Ok(())
255 }
256
257 #[cfg(test)]
259 pub(crate) fn test_setup_hook(&self, hook: SetupHook) {
260 *lock_unpoisoned(&self.setup_hook) = Some(hook);
261 }
262
263 #[cfg(test)]
265 pub(crate) fn test_replace_crash(&self, at: coven_sync::ReplaceStop) {
266 self.codes.test_replace_crash(at);
267 }
268
269 pub(super) fn start_setup(self: &Arc<Self>) -> Result<SetupCall, StorageSetupError> {
273 let mut run = lock_unpoisoned(&self.setup);
274 if !matches!(*run, SetupRun::Idle) {
275 return Err(StorageSetupError::SetupRunning);
276 }
277 *run = SetupRun::Called { cancelled: false };
278 Ok(SetupCall(self.clone()))
279 }
280
281 pub(super) async fn begin_setup(
285 &self,
286 call: SetupCall,
287 device_name: &str,
288 credentials: &StorageCredentials,
289 ) -> Result<SetupJournal, StorageSetupError> {
290 self.reached(SetupStop::Called).await?;
291 let creating = self.codes.location().await.map_err(setup_error)?.is_none();
292 let journal = SetupJournal::start(&self.database, creating, device_name)
293 .await
294 .map_err(setup_error)?;
295 match call.journaled() {
296 Ok(false) => {}
297 Ok(true) => return Err(self.undo_setup(&journal, StorageSetupError::Cancelled).await),
298 Err(error) => return Err(self.undo_setup(&journal, error).await),
299 }
300 let kept = credentials
301 .encode()
302 .map_err(StorageSetupError::from)
303 .and_then(|bytes| Ok(self.keychain.set_setup_credentials(&bytes)?));
304 if let Err(error) = kept {
305 return Err(self.undo_setup(&journal, error).await);
306 }
307 self.reached(SetupStop::Journaled).await?;
308 Ok(journal)
309 }
310
311 pub(super) async fn run_setup<T>(
316 self: &Arc<Self>,
317 mut journal: SetupJournal,
318 steps: impl AsyncFnOnce(&mut SetupJournal) -> Result<T, StorageSetupError>,
319 ) -> Result<T, StorageSetupError> {
320 let error = match steps(&mut journal).await {
321 Ok(done) => return Ok(done),
322 Err(error) => error,
323 };
324 #[cfg(test)]
325 if crashed(&error) {
326 return Err(error);
327 }
328 if journal.location().is_none() {
329 return Err(self.undo_setup(&journal, error).await);
330 }
331 if self.setup_finishing() {
332 if let Err(cleanup) = self.start_locked().await {
333 return Err(with_cleanup(error, cleanup));
334 }
335 }
336 Err(error)
337 }
338
339 async fn undo_setup(
343 &self,
344 journal: &SetupJournal,
345 error: StorageSetupError,
346 ) -> StorageSetupError {
347 let outcome = match &error {
348 StorageSetupError::Cancelled => OperationOutcome::Cancelled,
349 error => OperationOutcome::Undone(UndoCause::Failed { error: error.to_string() }),
350 };
351 let undone = async {
352 if journal.creating() {
355 self.forget_keys()?;
356 }
357 self.keychain.delete_setup_credentials()?;
358 journal.end(&self.database, outcome).await?;
359 *lock_unpoisoned(&self.setup) = SetupRun::Idle;
360 Ok::<_, SyncError>(())
361 }
362 .await;
363 match undone {
364 Ok(()) => error,
365 Err(cleanup) => with_cleanup(error, cleanup),
366 }
367 }
368
369 async fn pass_point(
372 &self,
373 journal: &mut SetupJournal,
374 location: StorageConfig,
375 ) -> Result<(), StorageSetupError> {
376 {
377 let mut run = lock_unpoisoned(&self.setup);
378 if matches!(*run, SetupRun::Checking { cancelled: true }) {
379 return Err(StorageSetupError::Cancelled);
380 }
381 *run = SetupRun::Finishing;
382 }
383 if let Err(error) = journal.claim(&self.database, location).await {
384 *lock_unpoisoned(&self.setup) = SetupRun::Checking { cancelled: false };
385 return Err(setup_error(error));
386 }
387 Ok(())
388 }
389
390 pub(super) async fn setup_connection(
401 &self,
402 journal: &mut SetupJournal,
403 config: StorageConfig,
404 auth: ProviderAuth,
405 sign_in: Option<Arc<SessionSignIn>>,
406 waits: SetupWaits,
407 progress: &mut ProgressReport<StorageSetupStep>,
408 ) -> Result<StorageSetup, StorageSetupError> {
409 progress.begin(StorageSetupStep::Connecting, 1);
410 let credentials = auth.credentials();
411 let storage = self
412 .connector
413 .connect(
414 config.clone(),
415 auth,
416 self.device,
417 self.state.clone(),
418 self.waits.clone(),
419 self.transfers.clone(),
420 )
421 .await?;
422 let setup = Arc::new(storage.during_setup(waits));
423 if !journal.creating() {
424 setup.check_location_claim(self.store()).await?;
425 }
426 let config = setup.labelled_config().await?;
429 let path = ObjectPath::file(self.store(), self.device, ObjectId(self.ids.new_id()));
430 let plaintext = b"coven storage check";
431 let header = coven_format::file::FileHeader::new(plaintext.len() as u64);
432 let key = coven_crypto::FileKey::generate()
433 .map_err(|error| StorageSetupError::Internal(Box::new(error)))?;
434 let mut bytes = header.encode().to_vec();
435 bytes.extend(
436 header
437 .seal_chunk(&key, path.as_str(), 0, plaintext)
438 .map_err(|error| StorageSetupError::Internal(Box::new(error)))?,
439 );
440 {
441 let _permit = self.state.permit_provider_check(&path);
442 coven_storage::check_provider(setup.as_ref(), &path, &bytes).await?;
443 }
444 progress.done(1);
445 self.reached(SetupStop::Checked).await?;
446 self.check_cancel()?;
447 if journal.creating() {
448 self.keep_creation_key().map_err(setup_error)?;
450 self.reached(SetupStop::KeyKept).await?;
451 }
452 let mut live = self.live.lock().await;
453 let sync = self.sequence(storage.clone(), &live).await.map_err(setup_error)?;
454 let claiming = Arc::new(sync.reconnect(setup.clone()));
457 if !journal.creating() {
458 let relayed = std::sync::Mutex::new(&mut *progress);
459 let loading = claiming.subscribe_progress().forward(claiming.unlock(), |load| {
460 lock_unpoisoned(&relayed).relay(setup_step::loading(load))
461 });
462 tokio::select! {
464 biased;
465 () = self.cancelled() => return Err(StorageSetupError::Cancelled),
466 loaded = loading => loaded.map_err(setup_error)?,
467 }
468 }
469 self.pass_point(journal, config.clone()).await?;
470 self.reached(SetupStop::Claiming).await?;
471 let restore_code = self
472 .finish_setup(
473 &mut live,
474 journal,
475 sync,
476 claiming,
477 FinishingConnections { storage, setup },
478 credentials,
479 sign_in,
480 false,
481 true,
482 progress,
483 )
484 .await
485 .map_err(setup_error)?;
486 Ok(StorageSetup {
487 connection: ConnectedStorage { storage: config, key_state: StoreKeyState::Available },
488 restore_code,
489 })
490 }
491
492 pub(super) async fn resume_setup(
497 &self,
498 live: &mut ConnectionState,
499 journal: SetupJournal,
500 ) -> Result<(), SyncError> {
501 let location = journal
502 .location()
503 .ok_or(DbError::Invariant { what: "a finishing setup is past its point of no return" })?
504 .clone();
505 let bytes = self
506 .keychain
507 .setup_credentials()?
508 .ok_or(DbError::Invariant { what: "a finishing setup keeps its credentials" })?;
509 let credentials = StorageCredentials::decode(bytes.as_bytes())?;
510 let (auth, sign_in) = match &credentials {
511 StorageCredentials::S3(keys) => (ProviderAuth::S3(keys.clone()), None),
512 StorageCredentials::CloudKit => (ProviderAuth::CloudKit, None),
513 StorageCredentials::OAuth(tokens) => {
514 let sign_in = SessionSignIn::new(tokens.clone(), self.clients.clone());
515 let auth = ProviderAuth::OAuth { tokens: tokens.clone(), custody: sign_in.clone() };
516 (auth, Some(sign_in))
517 }
518 };
519 let storage = self
520 .connector
521 .connect(
522 location,
523 auth,
524 self.device,
525 self.state.clone(),
526 self.waits.clone(),
527 self.transfers.clone(),
528 )
529 .await?;
530 let sync = self.sequence(storage.clone(), live).await?;
531 let claiming = Arc::new(sync.reconnect(storage.clone()));
532 let mut progress =
533 ProgressReport::new(tokio::sync::watch::channel(None).0, self.clock.clone());
534 self.finish_setup(
535 live,
536 &journal,
537 sync,
538 claiming,
539 FinishingConnections { storage: storage.clone(), setup: storage },
540 credentials,
541 sign_in,
542 true,
543 false,
544 &mut progress,
545 )
546 .await
547 .map(drop)
548 }
549
550 #[expect(
558 clippy::too_many_arguments,
559 reason = "the call and a crash's resumption differ in each"
560 )]
561 async fn finish_setup(
562 &self,
563 live: &mut ConnectionState,
564 journal: &SetupJournal,
565 sync: Arc<SequenceSync>,
566 claiming: Arc<SequenceSync>,
567 candidate: FinishingConnections,
568 credentials: StorageCredentials,
569 sign_in: Option<Arc<SessionSignIn>>,
570 catch_up: bool,
571 start: bool,
572 progress: &mut ProgressReport<StorageSetupStep>,
573 ) -> Result<SecretText, SyncError> {
574 let location = journal
575 .location()
576 .ok_or(DbError::Invariant { what: "a finishing setup is past its point of no return" })?
577 .clone();
578 let provider = location.provider();
579 let FinishingConnections { storage, setup } = candidate;
580 let access = member_access(&credentials, &setup).await?;
581 let prepared = Self::with_passes_held(live, async {
584 let claimed = if journal.creating() {
585 progress.begin(StorageSetupStep::CreatingStore, 1);
586 match setup.claim_location(self.store()).await {
587 Ok(()) => {
588 self.claim_creation(&claiming, access, journal.device_name(), progress)
589 .await
590 }
591 Err(error) => Err(error.into()),
592 }
593 } else {
594 self.claim_access(&claiming, access, catch_up, progress).await
595 };
596 let slot = match claimed {
597 Ok(slot) => slot,
598 Err(error) if refused(&error) => {
599 return Err(self.refuse_setup(journal, error).await)
600 }
601 Err(error) => return Err(error),
602 };
603 self.reached(SetupStop::Created).await?;
604 let (commit, restore_code) = self
605 .codes
606 .prepare_setup(ConnectionCredentials { location, credentials }, sign_in)
607 .await?;
608 self.files.set_storage(Some(storage), async { commit().map(drop) }).await?;
611 self.reached(SetupStop::Committed).await?;
612 Ok::<_, SyncError>((slot, restore_code))
613 })
614 .await;
615 let (slot, restore_code) = prepared?;
616 let outcome = match slot {
617 Some(slot) => OperationOutcome::Landed(slot),
618 None => OperationOutcome::Satisfied,
619 };
620 journal.end(&self.database, outcome).await?;
621 *lock_unpoisoned(&self.setup) = SetupRun::Idle;
622 self.reached(SetupStop::Finished).await?;
623 if let Err(error) = self.keychain.delete_setup_credentials() {
626 tracing::warn!("a finished setup's own credentials can't be deleted yet: {error}");
627 }
628 let released = self.close_live(live).await;
629 let started = self.activate(live, sync, start).await;
630 let recorded = coven_database::RecordedLocation::Connected(provider);
636 if let Err(written) = self.record_location(recorded, Some(self.clock.now())).await {
637 tracing::warn!("storage set up after setup can't be recorded: {written}");
638 }
639 let result = match (released, started) {
640 (Ok(()), result) | (result, Ok(())) => result,
641 (Err(operation), Err(cleanup)) => Err(SyncError::cleanup(operation, cleanup)),
642 };
643 if let Err(error) = result {
644 let failure = LastPass::failed(error);
645 if let Err(written) = self.show(failure.clone()).await {
646 tracing::warn!(
647 ?failure,
648 "how sync failed after setup can't be recorded: {written}"
649 );
650 }
651 }
652 Ok(restore_code)
653 }
654
655 async fn claim_creation(
660 &self,
661 sync: &Arc<SequenceSync>,
662 access: coven_format::MemberAccess,
663 device_name: &str,
664 progress: &mut ProgressReport<StorageSetupStep>,
665 ) -> Result<Option<coven_merge::Slot>, SyncError> {
666 let member = self.identity.read()?.ok_or(StorageFailure::MemberKeysMissing)?;
667 let ring = self.keep_creation_key()?;
668 let key = ring.store_key_ids().next().ok_or(StorageFailure::MemberKeysMissing)?;
669 let key_hash = ring.store_key(key)?.hash();
670 sync.settle_operations().await?;
671 if self.database.store_state().await?.store.is_none() {
672 sync.administer_at(coven_format::admin::StoreChange::CreateStore {
673 store: self.store(),
674 name: self.database.store_name().await.map_err(SyncError::from)?,
675 admin: member.keys().public_keys(),
676 access,
677 device_name: device_name.into(),
678 key,
679 key_hash,
680 })
681 .await?;
682 }
683 progress.done(1);
684 Ok(Some(coven_merge::Slot::FIRST))
685 }
686
687 fn keep_creation_key(&self) -> Result<StoreKeyring, SyncError> {
692 let ring = match self.with_keys(|keys| keys.read())? {
693 Some(ring) => ring,
694 None => StoreKeyring::new(coven_crypto::StoreKey::generate(KeyId(self.ids.new_id()))?),
695 };
696 self.persist_keys(&ring)?;
697 Ok(ring)
698 }
699
700 async fn claim_access(
703 &self,
704 sync: &Arc<SequenceSync>,
705 access: coven_format::MemberAccess,
706 catch_up: bool,
707 progress: &mut ProgressReport<StorageSetupStep>,
708 ) -> Result<Option<coven_merge::Slot>, SyncError> {
709 if catch_up {
710 sync.unlock().await?;
711 }
712 let member = self.identity.read()?.ok_or(StorageFailure::MemberKeysMissing)?;
713 let state = self.database.store_state().await?;
714 match access_change(&state, member.member(), access)? {
715 Some(change) => {
716 let recording = sync.administer_at(change);
717 setup_step::single(progress, StorageSetupStep::RecordingAccess, recording).await
718 }
719 None => Ok(None),
720 }
721 }
722
723 async fn refuse_setup(&self, journal: &SetupJournal, error: SyncError) -> SyncError {
728 let outcome = match &error {
729 SyncError::Rejected(reason) => OperationOutcome::Rejected(reason.clone()),
730 SyncError::StoreOwner => OperationOutcome::Rejected(OperationRejection::StoreOwner),
731 error => OperationOutcome::Undone(UndoCause::Failed { error: error.to_string() }),
732 };
733 let undone = async {
734 if journal.creating()
735 && self.database.current_claim().await?.is_none()
736 && self.database.store_state().await?.store.is_none()
737 {
738 self.forget_keys()?;
739 }
740 self.keychain.delete_setup_credentials()?;
741 journal.end(&self.database, outcome).await?;
742 *lock_unpoisoned(&self.setup) = SetupRun::Idle;
743 Ok::<_, SyncError>(())
744 }
745 .await;
746 match undone {
747 Ok(()) => error,
748 Err(cleanup) => SyncError::cleanup(error, cleanup),
749 }
750 }
751}
752
753fn refused(error: &SyncError) -> bool {
757 match error {
758 SyncError::Rejected(_) | SyncError::StoreOwner => true,
759 SyncError::Storage(error) => matches!(
760 error.failure(),
761 StorageFailure::StoreExists | StorageFailure::OwnLocationRequired
762 ),
763 _ => false,
764 }
765}
766
767#[cfg(test)]
768#[path = "storage_setup_tests.rs"]
769mod tests;