Skip to main content

coven_foundation/files/
directory.rs

1//! The paths and filesystem capabilities inside one store directory.
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6#[cfg(unix)]
7use crate::files::flush_batch::BatchFolder;
8use crate::files::protection::Protection;
9use crate::files::{
10    lock, settings, AtomicFile, CacheWriteLock, ClosedStoreLock, FileError, FileWriter, FlushBatch,
11    SettingsError, StoreDeletionLock, StoreLock, StoreLockError, StoreReadLock, StoreSettings,
12    WriterFolder,
13};
14use crate::id_source::StoreId;
15use crate::task::BugSinkRef;
16
17/// The folder an app names for SQLite's temporary tables (§21.2), outside
18/// coven's directories. It is made where the app's call names it.
19#[derive(Debug, PartialEq, Eq)]
20pub struct TemporaryFolder {
21    path: PathBuf,
22}
23
24impl TemporaryFolder {
25    /// The folder at `path`, when it names an existing folder, following
26    /// symlinks. A folder this process can't inspect counts as absent.
27    pub fn at(path: &std::path::Path) -> Option<Self> {
28        std::fs::metadata(path)
29            .is_ok_and(|metadata| metadata.is_dir())
30            .then(|| Self { path: path.to_owned() })
31    }
32
33    /// Its path.
34    pub fn path(&self) -> &std::path::Path {
35        &self.path
36    }
37}
38
39/// The store database file, also its name in a discarded device archive.
40pub(super) const DATABASE: &str = "store.db";
41
42/// One file owned by a crate, beside the database and store settings.
43/// The enum reserves the name so a caller cannot replace the database or settings.
44#[derive(Clone, Copy, Debug)]
45pub enum StoreFile {
46    /// coven-storage's provider settings, encoded and interpreted by that crate.
47    StorageSettings,
48    /// coven-crypto's passphrase-sealed store and circle keys (E1).
49    StoreKeys,
50    /// coven-crypto's passphrase-sealed member key pairs (E1).
51    MemberKeys,
52    /// Encrypted joining identity and fixed request, interpreted by coven-sync.
53    Bootstrap,
54}
55
56impl StoreFile {
57    /// The file's name in the store's directory.
58    pub(super) const fn name(self) -> &'static str {
59        match self {
60            Self::StorageSettings => "storage.json",
61            Self::StoreKeys => "store-keys.sealed",
62            Self::MemberKeys => "member-keys.sealed",
63            Self::Bootstrap => "bootstrap.sealed",
64        }
65    }
66}
67
68/// The two places coven keeps file bytes inside a store (E1).
69#[derive(Clone, Copy, Debug)]
70pub enum FileArea {
71    /// Coven's own copies of app-provided files, each staging's in a folder
72    /// of its own, and captured snapshots at the top (§17.1, Appendix C.3).
73    AppProvided,
74    /// The cache of uploaded files and chunks (§17.8).
75    Cache,
76}
77
78impl FileArea {
79    const ALL: [Self; 2] = [Self::AppProvided, Self::Cache];
80
81    /// The area's folder in the store's directory.
82    const fn name(self) -> &'static str {
83        match self {
84            Self::AppProvided => "files",
85            Self::Cache => "cache",
86        }
87    }
88}
89
90/// A portable single filename: ASCII letters, digits, `-`, `_` and interior
91/// dots, up to 255 bytes. Windows device names are refused on every platform.
92#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
93pub struct FileName(String);
94
95/// A name that could escape its directory or alias a Windows device.
96#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
97pub enum FileNameError {
98    /// The name is empty or exceeds a filesystem component's length.
99    #[error("file name must contain 1 to 255 bytes")]
100    Length,
101    /// The name contains a forbidden byte, starts with a dot or ends with one.
102    #[error("file name requires ASCII letters, digits, hyphens, underscores or interior dots")]
103    Character,
104    /// The name names a Windows device, even with an extension.
105    #[error("file name is reserved for a Windows device")]
106    Reserved,
107}
108
109impl FileName {
110    /// The validated filename, for recording an owned file in local metadata.
111    pub fn as_str(&self) -> &str {
112        &self.0
113    }
114
115    /// Validate a single filename before it is joined to an owned directory.
116    pub fn new(name: impl Into<String>) -> Result<Self, FileNameError> {
117        let name = name.into();
118        if name.is_empty() || name.len() > super::relative_path::NAME_LIMIT {
119            return Err(FileNameError::Length);
120        }
121        if name.starts_with('.')
122            || name.ends_with('.')
123            || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b"-_.".contains(&b))
124        {
125            return Err(FileNameError::Character);
126        }
127        let stem = name.split('.').next().expect("a nonempty name has a stem");
128        if super::relative_path::windows_device(stem) {
129            return Err(FileNameError::Reserved);
130        }
131        Ok(Self(name))
132    }
133}
134
135/// Where a staged copy lies among the store's app-provided files: in the
136/// folder its staging made, under its own name, recorded as
137/// `<folder>/<copy>` (Appendix C.3).
138#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
139pub struct StagedName(String);
140
141impl StagedName {
142    /// The copy `name` in the staging's folder `folder`.
143    pub fn new(folder: &FileName, name: &FileName) -> Self {
144        Self(format!("{}/{}", folder.0, name.0))
145    }
146
147    /// A recorded name read back, when it is two portable names joined by
148    /// `/`.
149    pub fn parse(recorded: &str) -> Option<Self> {
150        let (folder, name) = recorded.split_once('/')?;
151        Some(Self::new(&FileName::new(folder).ok()?, &FileName::new(name).ok()?))
152    }
153
154    /// The name as records hold it.
155    pub fn as_str(&self) -> &str {
156        &self.0
157    }
158
159    /// The staging's folder.
160    pub fn folder(&self) -> FileName {
161        FileName(self.parts().0.to_owned())
162    }
163
164    /// The copy's own name in its staging's folder.
165    pub fn copy(&self) -> FileName {
166        FileName(self.parts().1.to_owned())
167    }
168
169    /// The staging's folder and the copy's name in it.
170    fn parts(&self) -> (&str, &str) {
171        self.0.split_once('/').expect("a staged name holds its folder")
172    }
173}
174
175/// Why a staging's folder could not be made.
176#[derive(Debug, thiserror::Error)]
177pub enum StagingFolderError {
178    /// A folder of that name is already there: the id source gave out the
179    /// name before.
180    #[error("staging folder {} is already there", .0.as_str())]
181    Taken(FileName),
182    /// The store could not be held, or the folder made.
183    #[error(transparent)]
184    Writer(#[from] WriterError),
185}
186
187/// One store's directory, owning the paths to its database, app-provided files,
188/// cache, settings and sibling locks. It never exposes the directory as a raw path.
189#[derive(Clone)]
190pub struct StoreDir {
191    path: PathBuf,
192    id: StoreId,
193    /// The bootstrap lock of the bootstrap that gave the directory out, which
194    /// opens the unpublished store while that bootstrap holds it.
195    bootstrap: Option<lock::LockInstance>,
196    /// The class its layout gives the store's files (E1).
197    protection: Protection,
198    /// Where a bug in the store's work that nobody waits for goes (E15).
199    bugs: BugSinkRef,
200}
201
202impl std::fmt::Debug for StoreDir {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        f.debug_struct("StoreDir")
205            .field("path", &self.path)
206            .field("id", &self.id)
207            .field("bootstrap", &self.bootstrap)
208            .field("protection", &self.protection)
209            .finish_non_exhaustive()
210    }
211}
212
213impl StoreDir {
214    pub(crate) fn new(
215        path: PathBuf,
216        id: StoreId,
217        protection: Protection,
218        bugs: BugSinkRef,
219    ) -> Self {
220        Self { path, id, bootstrap: None, protection, bugs }
221    }
222
223    pub(super) fn bootstrapping(
224        path: PathBuf,
225        id: StoreId,
226        bootstrap: lock::LockInstance,
227        protection: Protection,
228        bugs: BugSinkRef,
229    ) -> Self {
230        Self { path, id, bootstrap: Some(bootstrap), protection, bugs }
231    }
232
233    /// Whether the store is still an unpublished installation: its marker is
234    /// there (§13.4).
235    pub fn is_unpublished(&self) -> Result<bool, FileError> {
236        super::bootstrap::is_pending(&self.path)
237    }
238
239    fn is_bootstrapping(&self) -> bool {
240        self.bootstrap
241            .is_some_and(|instance| lock::bootstrap_running(&self.path, self.id, instance))
242    }
243
244    /// An unfinished replacement's record refuses opens, from before its
245    /// fresh device registers until it is published; a marker alone belongs
246    /// to a first installation.
247    fn check_published(&self) -> Result<(), StoreLockError> {
248        if self.is_bootstrapping() {
249            return Ok(());
250        }
251        let unfinished =
252            super::device_reset::unfinished(&self.path, self.id).map_err(|error| match error {
253                SettingsError::File(error) => StoreLockError::from(error),
254                SettingsError::Bug(bug) => StoreLockError::Bug(bug),
255                error => FileError::at(
256                    "read the store's settings beside its replacement record",
257                    &self.path,
258                    std::io::Error::other(error),
259                )
260                .into(),
261            })?;
262        if let Some(record) = unfinished {
263            return Err(StoreLockError::ReplacementUnfinished { store: self.id, by: record.by });
264        }
265        if super::bootstrap::is_pending(&self.path)? {
266            return Err(StoreLockError::BootstrapPending(self.id));
267        }
268        Ok(())
269    }
270
271    /// Remove the installations resets kept in this store's directory
272    /// (§19.3). The store's own files stay. A failure midway leaves the rest
273    /// for the next call; with none left it succeeds.
274    pub fn delete_discarded_devices(&self) -> Result<(), FileError> {
275        super::device_reset::delete_archives(&self.path)
276    }
277
278    /// What this store's directory holds, in file sizes (§17.8, §19.3): its
279    /// own files, apart from the cache, and the installations resets kept.
280    pub fn local_bytes(&self) -> Result<super::DirectoryBytes, FileError> {
281        super::local_bytes::measure(&self.path, FileArea::Cache.name())
282    }
283
284    /// The store identified by this directory.
285    pub fn id(&self) -> StoreId {
286        self.id
287    }
288
289    /// The folder of the store's file `area`, for a test that looks at the
290    /// files coven keeps there.
291    #[cfg(any(test, feature = "test-utils"))]
292    pub fn area_path(&self, area: FileArea) -> std::path::PathBuf {
293        self.path.join(area.name())
294    }
295}
296
297pub(crate) fn initialize(
298    path: &std::path::Path,
299    settings: &StoreSettings,
300) -> Result<(), SettingsError> {
301    for area in FileArea::ALL {
302        let directory = path.join(area.name());
303        std::fs::create_dir(&directory).map_err(|source| {
304            crate::files::FileError::at("create store files directory", &directory, source)
305        })?;
306        #[cfg(unix)]
307        crate::files::atomic_file::sync_directory(&directory).map_err(|source| {
308            crate::files::FileError::at("sync store files directory", &directory, source)
309        })?;
310    }
311    settings::write(path, settings)
312}
313
314/// Why a store's file could not be created for writing: the store's read
315/// lock, which the writer holds, or the file itself.
316#[derive(Debug, thiserror::Error)]
317pub enum WriterError {
318    /// The store could not be held for the write.
319    #[error(transparent)]
320    Lock(#[from] StoreLockError),
321    /// Creating the file failed.
322    #[error(transparent)]
323    File(#[from] FileError),
324}
325
326/// Probes the OS writer lock without waiting, for lifetime assertions in
327/// tests. Production opens take the lock through `StoreDirAccess`.
328#[cfg(any(test, feature = "test-utils"))]
329pub trait StoreLockProbe {
330    /// Take the writer lock, refusing at once when it is held.
331    fn lock_exclusive(&self) -> Result<StoreLock, StoreLockError>;
332}
333
334#[cfg(any(test, feature = "test-utils"))]
335impl StoreLockProbe for StoreDir {
336    fn lock_exclusive(&self) -> Result<StoreLock, StoreLockError> {
337        let guard = lock::acquire(&self.path, self.id)?;
338        self.check_published()?;
339        Ok(guard)
340    }
341}
342
343/// Crate-to-crate operations for coven's owners; not re-exported by the app API.
344pub trait StoreDirAccess: Sized {
345    /// The database file's exact path for coven-database to open with SQLite.
346    /// Callers never append path components to it.
347    fn database_path(&self) -> PathBuf;
348    /// Retained remote input has its own SQLite owner: receiving bytes must not
349    /// wait for a transaction applying those bytes to the application database.
350    fn input_database_path(&self) -> PathBuf;
351    /// The record of a replacement whose swap is under way (§19.3). A record
352    /// that remains only because publication was interrupted after removing
353    /// the bootstrap marker, with settings already naming the new device, is
354    /// not returned; the next replacement removes it. Reads only.
355    fn unfinished_replacement(&self) -> Result<Option<super::ReplacementRecord>, SettingsError>;
356    /// What the store's unfinished deletion does with unsent work, when this
357    /// device recorded that it is deleting the store and leaving it (E1).
358    /// Reads only.
359    fn unfinished_deletion(&self) -> Result<Option<super::IfUnsent>, FileError>;
360    /// Record that this device is deleting the store and leaving it, with
361    /// what the deletion does with unsent work, before it journals the
362    /// device's removal. The record goes with the directory.
363    fn record_deletion(&self, unsent: super::IfUnsent) -> Result<(), FileError>;
364    /// Remove the record of a deletion that ended before it journaled the
365    /// device's removal. An absent record succeeds.
366    fn forget_deletion(&self) -> Result<(), FileError>;
367    /// The settings written when this store was created, restored or joined.
368    /// Reading takes no lock; a read-only open uses it alongside a writer.
369    fn settings(&self) -> Result<StoreSettings, SettingsError>;
370    /// Where a bug in the store's work that nobody waits for goes (E15):
371    /// the app's own reporter, which its directories were given.
372    fn bugs(&self) -> &BugSinkRef;
373    /// Take the writer lock, awaiting release by a closing instance in this
374    /// process. A live instance or another process still refuses immediately.
375    fn lock_for_open(
376        &self,
377    ) -> impl std::future::Future<Output = Result<StoreLock, StoreLockError>> + Send;
378    /// Take the writer lock to read a closed store without opening it: a live
379    /// instance or another process refuses immediately, and an instance
380    /// closing in this process is waited for. Registered as closing, so an
381    /// open in this process waits for its release (api.md E1).
382    fn lock_closed(
383        &self,
384    ) -> impl std::future::Future<Output = Result<ClosedStoreLock, StoreLockError>> + Send;
385    /// Prevent deletion while reading, alongside other readers and the writer.
386    /// The guard must outlive every open file and database connection it protects.
387    fn lock_read_only(&self) -> Result<StoreReadLock, StoreLockError>;
388    /// Hold while a cache range recorded as being written gets its bytes
389    /// with no database writer held. Shared by such writes in every process;
390    /// `exclude_cache_writes` waits for all of them (Appendix C.3).
391    fn lock_cache_write(&self) -> Result<CacheWriteLock, FileError>;
392    /// Wait until no cache range is being written in any process and keep new
393    /// writes waiting, so a range still recorded as being written has no
394    /// live writer (Appendix C.3).
395    fn exclude_cache_writes(&self) -> Result<CacheWriteLock, FileError>;
396    /// Lock an existing store or a directory left by an interrupted deletion,
397    /// holding its writer, reader and bootstrap locks so that removal takes
398    /// every lock file named for the store. An absent store with leftover
399    /// locks can finish their removal on retry. A running restore, join or
400    /// replacement refuses with `StoreLockError::AlreadyOpen`. A store whose
401    /// replacement is unfinished is locked for deletion; an unfinished first
402    /// installation is refused with `StoreLockError::BootstrapPending`.
403    /// No directory is created by this call.
404    fn lock_for_deletion(&self) -> Result<Option<StoreDeletionLock>, StoreLockError>;
405    /// Wait until no instance of this store in this process is closing, as
406    /// `lock_for_open` does, so a deletion that follows is not refused by an
407    /// instance only finishing its close. A live instance is not waited for.
408    fn closed(&self) -> impl std::future::Future<Output = Result<(), FileError>> + Send;
409    /// Give a crate the capability for its reserved file, without exposing
410    /// the store path or letting it name any other top-level file.
411    fn owned_file(&self, file: StoreFile) -> AtomicFile;
412    /// The capability for one app-provided copy or cache entry. All path
413    /// construction stays here; the name cannot traverse directories.
414    fn file(&self, area: FileArea, name: &FileName) -> AtomicFile;
415    /// Create a named file in `area` for asynchronous streaming without
416    /// replacing anything. The writer holds the store's read lock until its
417    /// handle and any cancelled I/O finish, so the store can't be deleted
418    /// under it. The caller records the name first and removes partial bytes
419    /// after a failure.
420    fn create_writer(&self, area: FileArea, name: &FileName) -> Result<FileWriter, WriterError>;
421    /// Hold `area`'s folder for a run of files made in it: the store's read
422    /// lock is taken and the folder resolved once, for every writer
423    /// [`WriterFolder::create`] makes, each as `create_writer`'s does.
424    fn writer_folder(&self, area: FileArea) -> Result<WriterFolder, WriterError>;
425    /// The capability for one staged copy, in its staging's folder.
426    fn staged_file(&self, name: &StagedName) -> AtomicFile;
427    /// Make `folder` among the app-provided files for one staging's copies,
428    /// refusing one already there, and hold it as `writer_folder` holds an
429    /// area's folder. The app-provided files' folder now holds the new
430    /// folder, so `batch`'s flush syncs it too (Appendix C.3). The caller
431    /// records the copies' names first, which name the folder.
432    fn staging_folder(
433        &self,
434        folder: &FileName,
435        batch: &mut FlushBatch,
436    ) -> Result<WriterFolder, StagingFolderError>;
437    /// Remove a staged copy, then its staging's folder once nothing is in
438    /// it, syncing the folder that held each (Appendix C.3). An absent copy
439    /// or folder is gone.
440    fn remove_staged(&self, name: &StagedName) -> Result<(), FileError>;
441    /// Reopen a named file of `area` that an earlier writer left unfinished,
442    /// for appending after its first `length` bytes, which the caller
443    /// recorded as synced; anything past them is cut off. A file with fewer
444    /// bytes is refused. The writer holds the store's read lock as
445    /// `create_writer`'s does.
446    fn resume_writer(
447        &self,
448        area: FileArea,
449        name: &FileName,
450        length: u64,
451    ) -> Result<FileWriter, WriterError>;
452}
453
454/// Check that `lock` protects exactly the store at `directory`.
455pub fn verify_lock(directory: &StoreDir, lock: &StoreLock) -> Result<(), StoreLockError> {
456    lock.verify(&directory.path, directory.id)
457}
458
459/// Remove what unfinished writes left at the top of the store `lock` holds
460/// for writing (Appendix C.3), and the joining keys a published store still
461/// holds when the join that published it stopped before deleting them
462/// (§13.4), then give every folder and file the class its
463/// layout gives the store's files, as `directory` names it, unless the
464/// store's record shows they were last given it (E1). Blocks while it walks
465/// the store.
466pub fn settle_files(directory: &StoreDir, lock: &StoreLock) -> Result<(), FileError> {
467    super::atomic_file::remove_temporaries(lock.directory())?;
468    // A store still unpublished keeps them: its join resumes from them.
469    if !super::bootstrap::is_pending(lock.directory())?
470        && super::atomic_file::exists(
471            "inspect the joining keys",
472            &lock.directory().join(StoreFile::Bootstrap.name()),
473        )?
474    {
475        directory.owned_file(StoreFile::Bootstrap).remove_with_temporaries()?;
476    }
477    directory.protection.apply(lock.directory())
478}
479
480impl StoreDirAccess for StoreDir {
481    fn database_path(&self) -> PathBuf {
482        self.path.join(DATABASE)
483    }
484    fn input_database_path(&self) -> PathBuf {
485        self.path.join("inputs.db")
486    }
487    fn unfinished_replacement(&self) -> Result<Option<super::ReplacementRecord>, SettingsError> {
488        super::device_reset::unfinished(&self.path, self.id)
489    }
490    fn unfinished_deletion(&self) -> Result<Option<super::IfUnsent>, FileError> {
491        super::deletion::read(&self.path)
492    }
493    fn record_deletion(&self, unsent: super::IfUnsent) -> Result<(), FileError> {
494        super::deletion::record(&self.path, unsent)
495    }
496    fn forget_deletion(&self) -> Result<(), FileError> {
497        super::deletion::forget(&self.path)
498    }
499    fn settings(&self) -> Result<StoreSettings, SettingsError> {
500        settings::read(&self.path, self.id)
501    }
502    fn bugs(&self) -> &BugSinkRef {
503        &self.bugs
504    }
505    async fn lock_for_open(&self) -> Result<StoreLock, StoreLockError> {
506        let guard = lock::acquire_for_open(&self.path, self.id, &self.bugs).await?;
507        self.check_published()?;
508        Ok(guard)
509    }
510    async fn lock_closed(&self) -> Result<ClosedStoreLock, StoreLockError> {
511        let guard = lock::acquire_closed(&self.path, self.id, &self.bugs).await?;
512        self.check_published()?;
513        Ok(guard)
514    }
515    fn lock_read_only(&self) -> Result<StoreReadLock, StoreLockError> {
516        let guard = lock::acquire_reader(&self.path, self.id)?;
517        self.check_published()?;
518        Ok(guard)
519    }
520    fn lock_cache_write(&self) -> Result<CacheWriteLock, FileError> {
521        lock::cache_writes(&self.path, true)
522    }
523    fn exclude_cache_writes(&self) -> Result<CacheWriteLock, FileError> {
524        lock::cache_writes(&self.path, false)
525    }
526    fn lock_for_deletion(&self) -> Result<Option<StoreDeletionLock>, StoreLockError> {
527        let bootstrapping = self.is_bootstrapping();
528        let locked = lock::for_deletion(&self.path, self.id, None);
529        // A marker beside a replacement record belongs to a swap under way,
530        // which deletion removes whole: both installations, the record and
531        // the replacement's directory. Alone, it belongs to a first
532        // installation, which only its own call resumes or cancels, whether
533        // or not that call is running and holding the bootstrap lock.
534        if !bootstrapping
535            && matches!(locked, Ok(Some(_)) | Err(StoreLockError::AlreadyOpen(_)))
536            && super::device_reset::read(&self.path)?.is_none()
537            && super::bootstrap::is_pending(&self.path)?
538        {
539            return Err(StoreLockError::BootstrapPending(self.id));
540        }
541        locked
542    }
543    async fn closed(&self) -> Result<(), FileError> {
544        lock::closed(&self.path, self.id).await
545    }
546    fn owned_file(&self, file: StoreFile) -> AtomicFile {
547        AtomicFile::new(self.path.join(file.name()))
548    }
549    fn file(&self, area: FileArea, name: &FileName) -> AtomicFile {
550        AtomicFile::new(self.path.join(area.name()).join(&name.0))
551    }
552    fn create_writer(&self, area: FileArea, name: &FileName) -> Result<FileWriter, WriterError> {
553        Ok(self.writer_folder(area)?.create(name)?)
554    }
555    fn writer_folder(&self, area: FileArea) -> Result<WriterFolder, WriterError> {
556        let lock = self.lock_read_only()?;
557        Ok(WriterFolder::open(&self.path.join(area.name()), Arc::new(lock), self.bugs.clone())?)
558    }
559    fn resume_writer(
560        &self,
561        area: FileArea,
562        name: &FileName,
563        length: u64,
564    ) -> Result<FileWriter, WriterError> {
565        let lock = self.lock_read_only()?;
566        Ok(FileWriter::resume(&self.file(area, name), lock, self.bugs.clone(), length)?)
567    }
568    fn staged_file(&self, name: &StagedName) -> AtomicFile {
569        let (folder, copy) = name.parts();
570        AtomicFile::new(self.path.join(FileArea::AppProvided.name()).join(folder).join(copy))
571    }
572    fn staging_folder(
573        &self,
574        folder: &FileName,
575        #[cfg_attr(
576            not(unix),
577            expect(unused_variables, reason = "Windows writes a folder's entries through")
578        )]
579        batch: &mut FlushBatch,
580    ) -> Result<WriterFolder, StagingFolderError> {
581        let lock = Arc::new(self.lock_read_only().map_err(WriterError::from)?);
582        let files = self.path.join(FileArea::AppProvided.name());
583        let path = files.join(&folder.0);
584        match std::fs::create_dir(&path) {
585            Ok(()) => {}
586            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
587                return Err(StagingFolderError::Taken(folder.clone()));
588            }
589            Err(source) => {
590                let error = FileError::at("create staging folder", &path, source);
591                return Err(WriterError::from(error).into());
592            }
593        }
594        #[cfg(unix)]
595        batch.add_folder(
596            BatchFolder::open(files.clone(), Some(lock.clone())).map_err(|source| {
597                WriterError::from(FileError::at("open folder", &files, source))
598            })?,
599        );
600        Ok(WriterFolder::open(&path, lock, self.bugs.clone()).map_err(WriterError::from)?)
601    }
602    fn remove_staged(&self, name: &StagedName) -> Result<(), FileError> {
603        let copy = self.staged_file(name);
604        copy.remove()?;
605        let (folder, _) = name.parts();
606        super::atomic_file::remove_empty_folder(
607            &self.path.join(FileArea::AppProvided.name()).join(folder),
608        )
609    }
610}
611
612#[cfg(test)]
613#[path = "directory_tests.rs"]
614mod tests;