aven_core/sync/client/engine/
reset.rs1use anyhow::{Result, bail};
6
7use super::{LocalPhase, local_phase, unix_now};
8use crate::db::Database;
9use crate::db::installation::InstallationGuard;
10use crate::sync::client::coordination;
11use crate::sync::client::host::{ClientHost, key_store};
12
13const SETUP_IN_PROGRESS: &str = "error sync-reset-setup-in-progress hint=\"finish setup by rerunning `aven sync setup`; if the original setup can no longer be resumed, run `aven sync reset --force`\"";
14const JOIN_IN_PROGRESS: &str = "error sync-reset-join-in-progress hint=\"finish joining by rerunning `aven sync join` before resetting\"";
15const INVITATION_OPEN: &str = "error sync-reset-invitation-open hint=\"wait for the other device to join, or run `aven sync invite --cancel`, before resetting\"";
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Reset {
20 Reset,
22 NotSetUp,
25}
26
27pub async fn reset(
34 database: &Database,
35 host: &dyn ClientHost,
36 force_incomplete_setup: bool,
37) -> Result<Reset> {
38 let _guard = coordination::acquire(database).await?;
39 let phase = local_phase(database).await?;
40 match phase {
41 LocalPhase::SetupIncomplete if !force_incomplete_setup => bail!(SETUP_IN_PROGRESS),
42 LocalPhase::SetupIncomplete => {}
43 LocalPhase::JoinIncomplete => bail!(JOIN_IN_PROGRESS),
44 LocalPhase::SetUp if invitation_open(database, host).await => bail!(INVITATION_OPEN),
45 LocalPhase::SetUp | LocalPhase::NotSetUp => {}
46 }
47 let store = key_store(host, database).await?;
48 let installation = database
49 .file_identity()
50 .map(InstallationGuard::acquire)
51 .transpose()?;
52 database.clear_sync_state().await?;
55 store.erase()?;
56 if let Some(installation) = installation {
57 installation.unfence()?;
58 }
59 Ok(match phase {
60 LocalPhase::SetupIncomplete | LocalPhase::SetUp => Reset::Reset,
61 LocalPhase::JoinIncomplete | LocalPhase::NotSetUp => Reset::NotSetUp,
62 })
63}
64
65async fn invitation_open(database: &Database, host: &dyn ClientHost) -> bool {
68 let open = async {
69 let store = key_store(host, database).await?;
70 let Some((_, server)) = store.association(database).await? else {
71 return anyhow::Ok(false);
72 };
73 let inputs = store.active_inputs(database, &server).await?;
74 let now = unix_now()?;
75 Ok(store
76 .open_invitation(database, &inputs)
77 .await?
78 .is_some_and(|invitation| invitation.expires_at > now))
79 };
80 open.await.unwrap_or(false)
81}
82
83#[cfg(test)]
84mod tests {
85 use std::path::{Path, PathBuf};
86 use std::sync::Arc;
87
88 use super::*;
89 use crate::api::{CreateTask, Store};
90 use crate::choices::{TaskPriority, TaskStatus};
91 use crate::sync::client::keys::test_support::isolated_store;
92 use crate::sync::client::keys::{FileProtectedStorage, ProtectedStorage, StoreResult};
93
94 struct Host(PathBuf);
95
96 impl ClientHost for Host {
97 fn ensure_sync_allowed(&self) -> Result<()> {
98 Ok(())
99 }
100 fn protected_storage(&self) -> StoreResult<Arc<dyn ProtectedStorage>> {
101 Ok(Arc::new(FileProtectedStorage::new(self.0.clone())))
102 }
103 fn blob_dir(&self, database: &Database) -> Result<PathBuf> {
104 Ok(database.path().with_extension("blobs"))
105 }
106 fn attachment_lifecycle_policy(&self) -> crate::attachments::LifecyclePolicy {
107 crate::attachments::LifecyclePolicy::default()
108 }
109 fn device_label(&self) -> Option<String> {
110 None
111 }
112 }
113
114 async fn database_with_task(path: &Path) -> Database {
115 let store = Store::open(path).await.unwrap();
116 let workspace = store.resolve_workspace("default").await.unwrap();
117 store
118 .create_task(
119 &workspace.id,
120 CreateTask {
121 title: "Kept through reset".to_string(),
122 description: String::new(),
123 project: "app".to_string(),
124 status: TaskStatus::Todo,
125 priority: TaskPriority::None,
126 metadata: Vec::new(),
127 available_at: None,
128 due_on: None,
129 },
130 )
131 .await
132 .unwrap();
133 drop(store);
134 Database::open(path).await.unwrap()
135 }
136
137 async fn count(database: &Database, sql: &str) -> i64 {
138 sqlx::query_scalar(sqlx::AssertSqlSafe(sql))
139 .fetch_one(database.pool())
140 .await
141 .unwrap()
142 }
143
144 #[tokio::test]
145 async fn reset_keeps_data_and_leaves_a_database_that_can_set_up_again() {
146 let directory = tempfile::tempdir().unwrap();
147 let keys = directory.path().join("keys");
148 let path = directory.path().join("aven.sqlite");
149 let database = database_with_task(&path).await;
150 let host = Host(keys.clone());
151
152 isolated_store(&database, &keys)
154 .await
155 .load_or_create()
156 .unwrap();
157 InstallationGuard::acquire(&path).unwrap().fence().unwrap();
158 sqlx::query(
159 "INSERT OR REPLACE INTO meta(key, value) VALUES
160 ('e2ee_association', 'old'), ('sync_cursor', '42'), ('sync_generation', '3')",
161 )
162 .execute(database.pool())
163 .await
164 .unwrap();
165 let key_files = || {
166 std::fs::read_dir(&keys)
167 .unwrap()
168 .filter_map(|entry| entry.unwrap().file_name().into_string().ok())
169 .filter(|name| !name.ends_with(".lock"))
170 .count()
171 };
172 assert!(key_files() > 0);
173
174 assert_eq!(
175 reset(&database, &host, false).await.unwrap(),
176 Reset::NotSetUp
177 );
178
179 assert_eq!(count(&database, "SELECT COUNT(*) FROM tasks").await, 1);
180 assert!(count(&database, "SELECT COUNT(*) FROM changes").await > 0);
181 assert_eq!(key_files(), 0);
182 assert!(
183 InstallationGuard::acquire(&path)
184 .unwrap()
185 .ensure_unbound()
186 .is_ok()
187 );
188 assert_eq!(database.meta("e2ee_association").await.unwrap(), None);
189 assert_eq!(
190 database.meta("sync_cursor").await.unwrap().as_deref(),
191 Some("0")
192 );
193 assert_eq!(local_phase(&database).await.unwrap(), LocalPhase::NotSetUp);
194 super::super::ensure_setup_available(&database, &host)
195 .await
196 .unwrap();
197 isolated_store(&database, &keys)
199 .await
200 .load_or_create()
201 .unwrap();
202
203 assert_eq!(
205 reset(&database, &host, false).await.unwrap(),
206 Reset::NotSetUp
207 );
208 assert_eq!(count(&database, "SELECT COUNT(*) FROM tasks").await, 1);
209 }
210}