1use std::fs;
4use std::io;
5use std::path::PathBuf;
6
7#[cfg(any(test, feature = "test-utils"))]
8use crate::files::DirectoryCreationError;
9
10use crate::files::protection::Protection;
11use crate::files::{
12 settings, BootstrapDirectoryError, FileError, SettingsError, StoreDeletionLock, StoreDir,
13 StoreDirAccess, StoreLockError,
14};
15use crate::id_source::{IdSource, StoreId};
16use crate::task::BugSinkRef;
17
18#[cfg_attr(
20 not(feature = "test-utils"),
21 doc = r#"
22Directory-only creation is a test fixture.
23```compile_fail
24use coven_foundation::{files::StoreDirectories, id_source::{StoreId, UuidIds, IdSource}};
25let layout = StoreDirectories::new("stores".into(), std::sync::Arc::new(coven_foundation::task::LoggedBugs));
26let _ = layout.create_store_dir(StoreId(UuidIds.new_id()), &UuidIds);
27```"#
28)]
29#[derive(Clone)]
30pub struct StoreDirectories {
31 app_dir: PathBuf,
32 protection: Protection,
34 bugs: BugSinkRef,
37}
38
39impl std::fmt::Debug for StoreDirectories {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("StoreDirectories")
42 .field("app_dir", &self.app_dir)
43 .field("protection", &self.protection)
44 .finish_non_exhaustive()
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50enum Installations {
51 Published,
53 UnfinishedFirst,
55}
56
57impl StoreDirectories {
58 pub fn new(app_dir: PathBuf, bugs: BugSinkRef) -> Self {
62 Self { app_dir, protection: Protection::none(), bugs }
63 }
64
65 pub fn bugs(&self) -> &BugSinkRef {
68 &self.bugs
69 }
70
71 #[cfg(target_vendor = "apple")]
77 pub fn file_protection(mut self, protection: crate::files::FileProtection) -> Self {
78 self.protection = Protection::new(protection);
79 self
80 }
81
82 fn stores(&self) -> PathBuf {
84 self.app_dir.join("stores")
85 }
86
87 pub async fn store_ids(&self) -> Result<Vec<StoreId>, FileError> {
92 self.listed(Installations::Published).await
93 }
94
95 pub async fn unfinished_first_installations(&self) -> Result<Vec<StoreId>, FileError> {
99 self.listed(Installations::UnfinishedFirst).await
100 }
101
102 pub async fn lock_unfinished_first(
111 &self,
112 id: StoreId,
113 ) -> Result<Option<StoreDeletionLock>, BootstrapDirectoryError> {
114 self.store_dir(&id).closed().await?;
115 let path = self.stores().join(id.to_string());
116 crate::task::blocking_completing(
117 "lock an unfinished installation",
118 self.bugs.clone(),
119 move || {
120 let lock = match super::lock::for_deletion(&path, id, None) {
121 Ok(Some(lock)) => lock,
122 Ok(None) | Err(StoreLockError::AlreadyOpen(_)) => return Ok(None),
124 Err(error) => return Err(BootstrapDirectoryError::from(error)),
125 };
126 let first = super::bootstrap::is_pending(&path)?
127 && super::device_reset::read(&path)?.is_none();
128 Ok(first.then_some(lock))
129 },
130 )
131 .await
132 .map_err(FileError::from)?
133 }
134
135 pub async fn recorded_deletions(&self) -> Result<Vec<StoreId>, FileError> {
141 let root = self.stores();
142 crate::task::blocking_completing("list recorded deletions", self.bugs.clone(), move || {
143 recorded_deletions(&root)
144 })
145 .await?
146 }
147
148 pub async fn deletion_recorded(&self, id: StoreId) -> Result<bool, FileError> {
151 let published = self.stores().join(id.to_string());
152 crate::task::blocking_completing(
153 "inspect a recorded deletion",
154 self.bugs.clone(),
155 move || deletion_recorded(&published, id),
156 )
157 .await?
158 }
159
160 async fn listed(&self, installations: Installations) -> Result<Vec<StoreId>, FileError> {
161 let layout = self.clone();
162 let bugs = self.bugs.clone();
163 crate::task::blocking_completing("list stores", bugs, move || {
164 layout.list_stores(installations)
165 })
166 .await?
167 }
168
169 fn list_stores(&self, installations: Installations) -> Result<Vec<StoreId>, FileError> {
170 let root = self.stores();
171 let entries = match fs::read_dir(&root) {
172 Ok(entries) => entries,
173 Err(error) if error.kind() == io::ErrorKind::NotFound => {
174 tracing::debug!(path = %root.display(), "stores directory is absent");
175 return Ok(Vec::new());
176 }
177 Err(source) => return Err(FileError::at("list store directories", &root, source)),
178 };
179 let mut stores = Vec::new();
180 for entry in entries {
181 let entry = entry
182 .map_err(|source| FileError::at("read store directory entry", &root, source))?;
183 let path = entry.path();
184 if !entry
185 .file_type()
186 .map_err(|source| FileError::at("inspect store directory", &path, source))?
187 .is_dir()
188 {
189 tracing::debug!(path = %path.display(), "entry is not a store directory");
190 continue;
191 }
192 let name = entry.file_name();
193 let id = match name.to_str().map(crate::id_source::canonical_uuid) {
194 Some(Ok(id)) => StoreId(id),
195 _ => {
196 tracing::debug!(path = %path.display(), "directory name is not a store id");
197 continue;
198 }
199 };
200 let first =
204 super::bootstrap::is_pending(&path)? && super::device_reset::read(&path)?.is_none();
205 if first != (installations == Installations::UnfinishedFirst) {
206 tracing::debug!(path = %path.display(), first, "store is not in this listing");
207 continue;
208 }
209 match settings::read(&path, id) {
210 Ok(_) => stores.push(id),
211 Err(SettingsError::File(error)) => return Err(error),
212 Err(SettingsError::Bug(bug)) => return Err(FileError::Bug(bug)),
213 Err(error) => {
214 tracing::warn!(path = %path.display(), error = %error, "directory has no valid store settings")
215 }
216 }
217 }
218 stores.sort();
219 Ok(stores)
220 }
221
222 pub fn store_dir(&self, id: &StoreId) -> StoreDir {
225 StoreDir::new(self.stores().join(id.to_string()), *id, self.protection, self.bugs.clone())
226 }
227
228 #[cfg(any(test, feature = "test-utils"))]
230 pub fn create_store_dir(
231 &self,
232 id: StoreId,
233 ids: &dyn IdSource,
234 ) -> Result<StoreDir, DirectoryCreationError> {
235 crate::files::creation::create(
236 &self.stores(),
237 id,
238 ids,
239 false,
240 self.protection,
241 self.bugs.clone(),
242 )
243 }
244}
245
246fn deletion_recorded(published: &std::path::Path, id: StoreId) -> Result<bool, FileError> {
250 use super::atomic_file::exists;
251 if exists("inspect a store", published)? {
254 return Ok(false);
255 }
256 if exists("inspect a store's deletion", &super::lock::deletion_path(published, id))? {
257 return Ok(true);
258 }
259 for path in super::lock::lock_paths(published, id) {
260 if exists("inspect a store's lock", &path)? {
261 return Ok(true);
262 }
263 }
264 Ok(false)
265}
266
267fn recorded_deletions(root: &std::path::Path) -> Result<Vec<StoreId>, FileError> {
268 let entries = match fs::read_dir(root) {
269 Ok(entries) => entries,
270 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
271 Err(source) => return Err(FileError::at("list store directories", root, source)),
272 };
273 let mut named = std::collections::BTreeSet::new();
274 for entry in entries {
275 let entry =
276 entry.map_err(|source| FileError::at("read store directory entry", root, source))?;
277 let name = entry.file_name();
278 let Some(name) = name.to_str() else {
279 tracing::debug!(name = ?name, "entry name is not UTF-8, so no store's");
280 continue;
281 };
282 let id = name
285 .strip_prefix(".coven-delete-")
286 .or_else(|| name.strip_prefix('.').and_then(|rest| rest.strip_suffix(".readers.lock")))
287 .or_else(|| name.strip_prefix('.').and_then(|rest| rest.strip_suffix(".lock")));
288 if let Some(Ok(id)) = id.map(crate::id_source::canonical_uuid) {
289 named.insert(StoreId(id));
290 }
291 }
292 let mut recorded = Vec::new();
293 for id in named {
294 if deletion_recorded(&root.join(id.to_string()), id)? {
295 recorded.push(id);
296 }
297 }
298 Ok(recorded)
299}
300
301impl StoreDirectories {
302 pub async fn begin_bootstrap(
307 &self,
308 id: StoreId,
309 ids: &dyn IdSource,
310 ) -> Result<super::BootstrapStore, super::BootstrapDirectoryError> {
311 super::bootstrap::reserve_bootstrap_directory(
312 &self.stores(),
313 id,
314 ids,
315 self.protection,
316 self.bugs.clone(),
317 )
318 .await
319 }
320
321 pub async fn begin_replacement(
329 &self,
330 id: StoreId,
331 by: super::ReplacedBy,
332 ids: &dyn IdSource,
333 ) -> Result<super::ReplacementStore, super::BootstrapDirectoryError> {
334 super::replacement::begin(&self.stores(), id, by, ids, self.protection, self.bugs.clone())
335 .await
336 }
337}
338
339#[cfg(test)]
340#[path = "layout_tests.rs"]
341mod tests;