coven_foundation/files/
creation.rs1use std::fs;
4use std::io;
5use std::path::Path;
6
7use crate::files::protection::Protection;
8use crate::files::{directory, lock, FileError, SettingsError, StoreDir, StoreSettings};
9use crate::id_source::{IdSource, StoreId};
10
11#[derive(Debug, thiserror::Error)]
13pub enum DirectoryCreationError {
14 #[error("store {0} already exists on this device")]
16 AlreadyExists(StoreId),
17 #[error("create store: {0}")]
19 File(#[from] FileError),
20 #[error("create store settings: {0}")]
22 Settings(#[from] SettingsError),
23 #[error("store {id} was published, but its directory could not be synced: {source}")]
25 Published {
26 id: StoreId,
28 #[source]
30 source: io::Error,
31 },
32 #[error("{operation}; removing unpublished directory failed: {cleanup}")]
34 Rollback {
35 #[source]
37 operation: Box<DirectoryCreationError>,
38 cleanup: io::Error,
40 },
41}
42
43impl crate::task::CarriesBug for DirectoryCreationError {
44 fn bug(&self) -> Option<&crate::task::Bug> {
45 match self {
46 Self::File(error) => error.bug(),
47 Self::Settings(error) => error.bug(),
48 Self::Rollback { operation, .. } => operation.bug(),
49 Self::AlreadyExists(_) | Self::Published { .. } => None,
50 }
51 }
52}
53
54impl DirectoryCreationError {
55 pub(crate) fn file(&self) -> Option<&FileError> {
57 match self {
58 Self::File(file) => Some(file),
59 Self::Rollback { operation, .. } => operation.file(),
60 Self::AlreadyExists(_) | Self::Settings(_) | Self::Published { .. } => None,
61 }
62 }
63}
64
65pub(crate) fn create(
66 root: &Path,
67 id: StoreId,
68 ids: &dyn IdSource,
69 unpublished: bool,
70 protection: Protection,
71 bugs: crate::task::BugSinkRef,
72) -> Result<StoreDir, DirectoryCreationError> {
73 create_directory(root)
74 .map_err(|source| FileError::at("create stores directory", root, source))?;
75 let root = fs::canonicalize(root)
76 .map_err(|source| FileError::at("resolve stores directory", root, source))?;
77 let _layout = lock::lock_layout(&root)?;
78 let destination = root.join(id.to_string());
79 for path in [destination.clone(), lock::deletion_path(&destination, id)]
82 .into_iter()
83 .chain(lock::lock_paths(&destination, id))
84 {
85 if super::atomic_file::exists("inspect store destination", &path)? {
86 return Err(DirectoryCreationError::AlreadyExists(id));
87 }
88 }
89 let stage = tempfile::Builder::new()
90 .prefix(".coven-create-")
91 .tempdir_in(&root)
92 .map_err(|source| FileError::at("create unpublished store directory", &root, source))?;
93 let settings = StoreSettings { id, device_id: ids.new_device_id() };
94 let prepare = protection
96 .begin(stage.path())
97 .map_err(DirectoryCreationError::from)
98 .and_then(|()| {
99 directory::initialize(stage.path(), &settings).map_err(DirectoryCreationError::from)
100 })
101 .and_then(|()| {
102 if unpublished {
103 super::atomic_file::replace(&super::bootstrap::marker(stage.path()), b"")?;
104 }
105 Ok(())
106 })
107 .and_then(|()| {
108 crate::files::atomic_file::rename_new(stage.path(), &destination).map_err(|source| {
109 if source.kind() == io::ErrorKind::AlreadyExists {
110 DirectoryCreationError::AlreadyExists(id)
111 } else {
112 FileError::at("publish store directory", &destination, source).into()
113 }
114 })
115 });
116 if let Err(operation) = prepare {
117 return match stage.close() {
118 Ok(()) => Err(operation),
119 Err(cleanup) => {
120 Err(DirectoryCreationError::Rollback { operation: Box::new(operation), cleanup })
121 }
122 };
123 }
124 let _unpublished_path = stage.keep();
127 #[cfg(unix)]
128 crate::files::atomic_file::sync_directory(&root).map_err(|source| {
129 if unpublished {
130 FileError::at("sync unpublished store directory", &root, source).into()
132 } else {
133 DirectoryCreationError::Published { id, source }
134 }
135 })?;
136 Ok(StoreDir::new(destination, id, protection, bugs))
137}
138
139#[cfg(windows)]
144pub(crate) fn create_directory(path: &Path) -> io::Result<()> {
145 make_directory(path)
146}
147
148#[cfg(unix)]
154pub(crate) fn create_directory(path: &Path) -> io::Result<()> {
155 create_directory_with_sync(path, &crate::files::atomic_file::sync_directory)
156}
157
158#[cfg(unix)]
159fn create_directory_with_sync(
160 path: &Path,
161 sync: &impl Fn(&Path) -> io::Result<()>,
162) -> io::Result<()> {
163 make_directory(path)?;
164 let parent = match path.parent() {
168 Some(parent) if !parent.as_os_str().is_empty() => parent,
169 _ => Path::new("."),
170 };
171 sync(parent)
172}
173
174fn make_directory(path: &Path) -> io::Result<()> {
175 match fs::create_dir(path) {
176 Ok(()) => Ok(()),
177 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
178 if fs::metadata(path)?.is_dir() {
179 Ok(())
180 } else {
181 Err(error)
182 }
183 }
184 Err(error) => Err(error),
185 }
186}
187
188#[cfg(test)]
189#[path = "creation_tests.rs"]
190mod tests;