1use super::{
5 atomic_file::exists, creation, device_reset, lock, protection::Protection,
6 DirectoryCreationError, FileError, StoreDir, StoreLockError,
7};
8use crate::files::StoreDirAccess;
9use crate::id_source::{IdSource, StoreId};
10use crate::locks::lock_unpoisoned;
11use std::{
12 fs, io,
13 path::{Path, PathBuf},
14 sync::Mutex,
15};
16
17#[derive(Debug, thiserror::Error)]
19pub enum BootstrapDirectoryError {
20 #[error(transparent)]
22 Create(#[from] DirectoryCreationError),
23 #[error(transparent)]
25 Lock(StoreLockError),
26 #[error(transparent)]
28 File(FileError),
29 #[error(transparent)]
32 Bug(#[from] crate::task::Bug),
33}
34
35impl From<StoreLockError> for BootstrapDirectoryError {
36 fn from(error: StoreLockError) -> Self {
37 match error {
38 StoreLockError::Bug(bug) => Self::Bug(bug),
39 error => Self::Lock(error),
40 }
41 }
42}
43
44impl From<FileError> for BootstrapDirectoryError {
45 fn from(error: FileError) -> Self {
46 match error {
47 FileError::Bug(bug) => Self::Bug(bug),
48 error => Self::File(error),
49 }
50 }
51}
52
53impl crate::task::CarriesBug for BootstrapDirectoryError {
54 fn bug(&self) -> Option<&crate::task::Bug> {
55 match self {
56 Self::Bug(bug) => Some(bug),
57 Self::File(error) => error.bug(),
58 Self::Create(creation) => creation.bug(),
59 Self::Lock(error) => error.bug(),
60 }
61 }
62}
63
64impl BootstrapDirectoryError {
65 pub fn device_locked(&self) -> Option<&std::path::Path> {
68 let file = match self {
69 Self::File(file) => file,
70 Self::Create(creation) => creation.file()?,
71 Self::Lock(_) | Self::Bug(_) => return None,
72 };
73 match file {
74 FileError::DeviceLocked { path } => Some(path),
75 _ => None,
76 }
77 }
78
79 pub fn published_unsynced(&self) -> bool {
82 matches!(self, Self::Create(DirectoryCreationError::Published { .. }))
83 }
84}
85
86pub struct BootstrapStore {
90 _closing: lock::StoreClosingGuard,
94 path: PathBuf,
95 id: StoreId,
96 lease: BootstrapLease,
97 protection: Protection,
98 bugs: crate::task::BugSinkRef,
99 resumed: bool,
102}
103
104#[derive(Debug)]
108pub(super) struct BootstrapLease {
109 held: Mutex<Option<lock::RegisteredLock>>,
110 instance: lock::LockInstance,
111}
112
113impl BootstrapLease {
114 pub(super) fn new(lock: lock::RegisteredLock) -> Self {
115 Self { instance: lock.instance(), held: Mutex::new(Some(lock)) }
116 }
117
118 pub(super) fn instance(&self) -> lock::LockInstance {
119 self.instance
120 }
121
122 pub(super) fn release(&self) {
123 lock_unpoisoned(&self.held).take();
124 }
125}
126
127impl BootstrapStore {
128 pub fn directory(&self) -> StoreDir {
131 StoreDir::bootstrapping(
132 self.path.clone(),
133 self.id,
134 self.lease.instance(),
135 self.protection,
136 self.bugs.clone(),
137 )
138 }
139
140 pub fn resumed(&self) -> bool {
143 self.resumed
144 }
145
146 pub fn publish(&self) -> Result<(), BootstrapDirectoryError> {
149 let _reader = self.directory().lock_read_only()?;
150 let published = |source: io::Error| -> BootstrapDirectoryError {
151 DirectoryCreationError::Published { id: self.id, source }.into()
152 };
153 let result = remove_marker(&self.path, published);
154 self.lease.release();
155 result
156 }
157
158 pub fn is_published(&self) -> Result<bool, FileError> {
161 Ok(!is_pending(&self.path)?)
162 }
163
164 pub fn cancel(&self) -> Result<(), BootstrapDirectoryError> {
167 let mut held = lock_unpoisoned(&self.lease.held);
168 let guard = self.lock_for_deletion(&mut held)?;
169 if exists("inspect bootstrap path", &self.path)? && !is_pending(&self.path)? {
170 return Err(DirectoryCreationError::AlreadyExists(self.id).into());
171 }
172 if let Some(guard) = guard {
173 guard.remove_directory()?;
174 }
175 held.take();
176 Ok(())
177 }
178
179 fn lock_for_deletion(
183 &self,
184 held: &mut Option<lock::RegisteredLock>,
185 ) -> Result<Option<super::StoreDeletionLock>, StoreLockError> {
186 if held.is_some() {
187 lock::for_deletion(&self.path, self.id, Some(held))
188 } else {
189 self.directory().lock_for_deletion()
190 }
191 }
192}
193
194pub(super) const MARKER: &str = ".coven-bootstrap";
196
197pub(super) fn marker(directory: &Path) -> PathBuf {
198 directory.join(MARKER)
199}
200
201pub(super) fn remove_marker(
204 directory: &Path,
205 published: impl Fn(io::Error) -> BootstrapDirectoryError,
206) -> Result<(), BootstrapDirectoryError> {
207 match super::atomic_file::remove(&marker(directory)) {
208 Ok(()) => Ok(()),
210 Err(FileError::AfterRemove { source, .. }) => Err(published(
211 *source.downcast::<io::Error>().expect("directory removal retains its IO cause"),
212 )),
213 Err(error) => Err(error.into()),
214 }
215}
216
217pub(super) fn is_pending(directory: &Path) -> Result<bool, FileError> {
218 exists("inspect bootstrap path", &marker(directory))
219}
220
221pub(super) async fn reserve_bootstrap_directory(
222 root: &Path,
223 id: StoreId,
224 ids: &dyn IdSource,
225 protection: Protection,
226 bugs: crate::task::BugSinkRef,
227) -> Result<BootstrapStore, BootstrapDirectoryError> {
228 creation::create_directory(root)
229 .map_err(|source| FileError::at("create bootstrap layout", root, source))?;
230 let root = fs::canonicalize(root)
231 .map_err(|source| FileError::at("resolve bootstrap layout", root, source))?;
232 let path = root.join(id.to_string());
233 let lock = lock::acquire_bootstrap(&path, id, &bugs).await?;
234 let closing = lock.closing_guard();
235 let lease = BootstrapLease::new(lock);
236 let resumed = exists("inspect bootstrap path", &path)?;
237 if !resumed {
238 creation::create(&root, id, ids, true, protection, bugs.clone())?;
239 }
240 let metadata = fs::symlink_metadata(&path)
241 .map_err(|source| FileError::at("inspect unpublished store", &path, source))?;
242 if !metadata.is_dir() {
243 return Err(FileError::at(
244 "inspect unpublished store",
245 &path,
246 io::Error::new(io::ErrorKind::InvalidInput, "unpublished store is not a directory"),
247 )
248 .into());
249 }
250 let _writer = lock::acquire_for_open(&path, id, &bugs).await?;
253 if let Some(record) =
255 device_reset::unfinished(&path, id).map_err(DirectoryCreationError::Settings)?
256 {
257 return Err(StoreLockError::ReplacementUnfinished { store: id, by: record.by }.into());
258 }
259 if !is_pending(&path)? {
260 return Err(DirectoryCreationError::AlreadyExists(id).into());
261 }
262 super::atomic_file::remove_temporaries(&path)?;
264 protection.apply(&path)?;
266 let store = BootstrapStore { _closing: closing, path, id, lease, protection, bugs, resumed };
267 store.directory().settings().map_err(DirectoryCreationError::Settings)?;
268 Ok(store)
269}
270
271#[cfg(test)]
272#[path = "bootstrap_tests.rs"]
273mod tests;