Skip to main content

coven_foundation/files/
creation.rs

1//! Publish a complete store directory for one new device.
2
3use 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/// A store creation failure, with publication and rollback failures explicit.
12#[derive(Debug, thiserror::Error)]
13pub enum DirectoryCreationError {
14    /// A directory or file already occupies this store's id. It is untouched.
15    #[error("store {0} already exists on this device")]
16    AlreadyExists(StoreId),
17    /// An operation failed before publication.
18    #[error("create store: {0}")]
19    File(#[from] FileError),
20    /// Writing the new device's settings failed before publication.
21    #[error("create store settings: {0}")]
22    Settings(#[from] SettingsError),
23    /// The entire store is visible, but syncing its parent failed.
24    #[error("store {id} was published, but its directory could not be synced: {source}")]
25    Published {
26        /// The store that is already visible.
27        id: StoreId,
28        /// The durability error.
29        #[source]
30        source: io::Error,
31    },
32    /// Removing the unpublished directory also failed.
33    #[error("{operation}; removing unpublished directory failed: {cleanup}")]
34    Rollback {
35        /// The original creation failure.
36        #[source]
37        operation: Box<DirectoryCreationError>,
38        /// The error removing the directory.
39        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    /// The file failure that stopped the creation, if a file operation did.
56    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    // A deletion must finish before the id can be reused, including removal
80    // of its locks. Otherwise cleanup could unlink the replacement's lock.
81    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    // The stage takes the class before anything is made in it (E1).
95    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    // The stage's old name is absent; disarm its destructor before a new
125    // temporary sibling could ever reuse that name.
126    let _unpublished_path = stage.keep();
127    #[cfg(unix)]
128    crate::files::atomic_file::sync_directory(&root).map_err(|source| {
129        if unpublished {
130            // The marker still hides this directory; it has not published a store.
131            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/// Create `path` unless it is already a directory. Its parent must exist:
140/// coven creates directories only inside the app directory, one level at a
141/// time, and never opens anything above it. Android refuses an app access to
142/// the directories above its own.
143#[cfg(windows)]
144pub(crate) fn create_directory(path: &Path) -> io::Result<()> {
145    make_directory(path)
146}
147
148/// Create `path` unless it is already a directory, then sync its parent so
149/// the entry is durable. Its parent must exist: coven creates directories
150/// only inside the app directory, one level at a time, and never opens
151/// anything above it. Android refuses an app access to the directories above
152/// its own.
153#[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    // Existence does not prove durability: an earlier attempt may have made
165    // the directory and failed before syncing its parent. Repeat the barrier
166    // on retry, including when another creator made it concurrently.
167    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;