aven_core/db/
installation.rs1use std::fs::{self, File, OpenOptions};
10use std::io::Write;
11use std::path::{Path, PathBuf};
12
13use anyhow::{Context, Result, ensure};
14
15pub struct InstallationGuard {
16 _lock: File,
17 marker: PathBuf,
18 identity: PathBuf,
19 exclusive: bool,
20}
21
22impl Drop for InstallationGuard {
23 fn drop(&mut self) {
24 let _ = self._lock.unlock();
27 }
28}
29
30impl InstallationGuard {
31 pub fn acquire(path: &Path) -> Result<Self> {
32 Self::acquire_mode(path, true)
33 }
34
35 pub(crate) fn acquire_plaintext(path: &Path) -> Result<Self> {
37 let guard = Self::acquire_mode(path, false)?;
38 guard.ensure_unbound()?;
39 Ok(guard)
40 }
41
42 pub(crate) fn acquire_for_backup(path: &Path) -> Result<Self> {
44 Self::acquire_mode(path, false)
45 }
46
47 fn acquire_mode(path: &Path, exclusive: bool) -> Result<Self> {
48 let parent = path
49 .parent()
50 .filter(|p| !p.as_os_str().is_empty())
51 .unwrap_or(Path::new("."));
52 let name = path
53 .file_name()
54 .context("error installation-path-invalid")?;
55 let identity = parent.canonicalize()?.join(name);
56 if let Ok(metadata) = fs::symlink_metadata(&identity) {
57 ensure!(
58 metadata.is_file() && !metadata.file_type().is_symlink(),
59 "error installation-path-alias-unsupported"
60 );
61 #[cfg(unix)]
62 {
63 use std::os::unix::fs::MetadataExt;
64 ensure!(
65 metadata.nlink() == 1,
66 "error installation-path-alias-unsupported"
67 );
68 }
69 }
70 let sidecar = |suffix: &str| {
71 let mut name = name.to_os_string();
72 name.push(suffix);
73 identity.with_file_name(name)
74 };
75 let lock_path = sidecar(".aven-installation.lock");
76 if let Ok(metadata) = fs::symlink_metadata(&lock_path) {
77 ensure!(
78 metadata.is_file() && !metadata.file_type().is_symlink(),
79 "error installation-lock-unsafe"
80 );
81 }
82 let lock = crate::private_fs::open_lock_file(&lock_path)
83 .context("error installation-lock-unsafe")?;
84 if exclusive {
85 lock.try_lock()
86 } else {
87 lock.try_lock_shared()
88 }
89 .context("error installation-busy")?;
90 Ok(Self {
91 _lock: lock,
92 marker: sidecar(".aven-e2ee-bound"),
93 identity,
94 exclusive,
95 })
96 }
97
98 pub fn identity(&self) -> &Path {
99 &self.identity
100 }
101
102 pub fn ensure_unbound(&self) -> Result<()> {
103 match fs::symlink_metadata(&self.marker) {
104 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
105 Err(error) => Err(error.into()),
106 Ok(_) => anyhow::bail!(
107 "error e2ee-installation-fenced encrypted-tail-and-replacement-unavailable"
108 ),
109 }
110 }
111
112 pub(crate) fn ensure_restore_target_unbound(&self) -> Result<()> {
113 self.ensure_replacement_target_unbound(
114 "This database takes part in sync, so restore is refused here. Restore the backup to a new database path.",
115 )
116 }
117
118 fn ensure_import_target_unbound(&self) -> Result<()> {
119 self.ensure_replacement_target_unbound(
120 "This database takes part in sync, so import is refused here. Import into a new database path.",
121 )
122 }
123
124 fn ensure_replacement_target_unbound(&self, message: &str) -> Result<()> {
125 match fs::symlink_metadata(&self.marker) {
126 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
127 Err(error) => Err(error.into()),
128 Ok(_) => anyhow::bail!(message.to_string()),
129 }
130 }
131
132 pub fn unfence(&self) -> Result<()> {
134 ensure!(self.exclusive, "error installation-exclusive-lock-required");
135 match fs::remove_file(&self.marker) {
136 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
137 result => result?,
138 }
139 File::open(self.marker.parent().context("missing marker parent")?)?.sync_all()?;
140 Ok(())
141 }
142
143 pub fn fence(&self) -> Result<()> {
145 ensure!(self.exclusive, "error installation-exclusive-lock-required");
146 if fs::symlink_metadata(&self.marker).is_ok() {
147 return Ok(());
148 }
149 let mut file = OpenOptions::new()
150 .write(true)
151 .create_new(true)
152 .open(&self.marker)?;
153 file.write_all(b"aven-e2ee-denial-only-v1\n")?;
154 file.sync_all()?;
155 File::open(self.marker.parent().context("missing marker parent")?)?.sync_all()?;
156 Ok(())
157 }
158}
159
160impl super::Database {
161 pub(crate) fn plaintext_installation_guard(&self) -> Result<Option<InstallationGuard>> {
162 self.file_identity()
163 .map(InstallationGuard::acquire_plaintext)
164 .transpose()
165 }
166
167 pub(crate) fn backup_installation_guard(&self) -> Result<Option<InstallationGuard>> {
168 self.file_identity()
169 .map(InstallationGuard::acquire_for_backup)
170 .transpose()
171 }
172
173 pub(crate) fn import_installation_guard(&self) -> Result<Option<InstallationGuard>> {
174 let guard = self
175 .file_identity()
176 .map(InstallationGuard::acquire_for_backup)
177 .transpose()?;
178 if let Some(guard) = &guard {
179 guard.ensure_import_target_unbound()?;
180 }
181 Ok(guard)
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[cfg(unix)]
190 #[test]
191 fn drop_releases_lock_while_an_inherited_descriptor_remains_open() {
192 let root = tempfile::tempdir().unwrap();
193 let path = root.path().join("db.sqlite");
194 for exclusive in [true, false] {
195 let guard = InstallationGuard::acquire_mode(&path, exclusive).unwrap();
196 let inherited = guard._lock.try_clone().unwrap();
199 assert!(InstallationGuard::acquire(&path).is_err());
200 drop(guard);
201 let next = InstallationGuard::acquire(&path).unwrap();
202 assert!(InstallationGuard::acquire(&path).is_err());
203 drop(inherited);
204 assert!(InstallationGuard::acquire(&path).is_err());
205 drop(next);
206 assert!(InstallationGuard::acquire(&path).is_ok());
207 }
208 }
209
210 #[test]
211 fn missing_target_retains_same_denial_identity_and_aliases_fail() {
212 let root = tempfile::tempdir().unwrap();
213 let path = root.path().join("db.sqlite");
214 let guard = InstallationGuard::acquire(&path).unwrap();
215 let identity = guard.identity().to_path_buf();
216 guard.fence().unwrap();
217 drop(guard);
218 fs::write(&path, b"fixture").unwrap();
219 let guard = InstallationGuard::acquire(&path).unwrap();
220 assert_eq!(guard.identity(), identity);
221 assert!(guard.ensure_unbound().is_err());
222 drop(guard);
223 #[cfg(unix)]
224 {
225 let alias = root.path().join("alias.sqlite");
226 std::os::unix::fs::symlink(&path, &alias).unwrap();
227 assert!(InstallationGuard::acquire(&alias).is_err());
228 fs::remove_file(&alias).unwrap();
229 fs::hard_link(&path, &alias).unwrap();
230 assert!(InstallationGuard::acquire(&alias).is_err());
231 assert!(InstallationGuard::acquire(&path).is_err());
232 }
233 }
234}
235
236#[cfg(test)]
237mod concurrency_tests {
238 use super::*;
239 use crate::db::Database;
240 use std::future::Future;
241 use std::task::{Context as TaskContext, Waker};
242
243 #[tokio::test]
244 async fn ordinary_operations_overlap_across_clones_and_independent_pools() {
245 let root = tempfile::tempdir().unwrap();
246 let path = root.path().join("db.sqlite");
247 let database = Database::open(&path).await.unwrap();
248 let independent = Database::open(&path).await.unwrap();
249 let clone = database.clone();
250 let source = Database::open(&root.path().join("source.sqlite"))
251 .await
252 .unwrap();
253 let capture = source
254 .capture_local_shared_state_never_dispatched()
255 .await
256 .unwrap();
257 let capture = capture.shared_state();
258 let writer = database.acquire_writer().await.unwrap();
259 let mut page = Box::pin(database.install_shared_state(capture));
260 let mut context = TaskContext::from_waker(Waker::noop());
261 assert!(page.as_mut().poll(&mut context).is_pending());
263 crate::db::backup_database(&path, &root.path().join("backup.sqlite"))
264 .await
265 .unwrap();
266 independent
267 .create_backup_archive(root.path(), &root.path().join("backup.tar.zst"))
268 .await
269 .unwrap();
270 let nested = InstallationGuard::acquire_plaintext(&path).unwrap();
271 assert!(nested.fence().is_err());
272 drop(nested);
273 let mut second = Box::pin(clone.install_shared_state(capture));
274 assert!(second.as_mut().poll(&mut context).is_pending());
275 assert!(InstallationGuard::acquire(&path).is_err());
277 drop(writer);
278 page.await.unwrap();
279 assert!(InstallationGuard::acquire(&path).is_err());
280 let _ = second.await;
282 let exclusive = InstallationGuard::acquire(&path).unwrap();
283 assert!(
284 independent
285 .install_shared_state(capture)
286 .await
287 .unwrap_err()
288 .to_string()
289 .contains("installation")
290 );
291 exclusive.fence().unwrap();
292 drop(exclusive);
293 crate::db::backup_database(&path, &root.path().join("fenced.sqlite"))
294 .await
295 .unwrap();
296 independent
297 .create_backup_archive(root.path(), &root.path().join("fenced.tar.zst"))
298 .await
299 .unwrap();
300 }
301}