Skip to main content

coven_foundation/files/
folder_copy.rs

1//! Copies coven writes into a folder the person picked, to make cloud files
2//! device-only (§17.3): the folders and files a call needs, refusing paths
3//! through a symbolic link; each file written under a temporary name in its
4//! final folder and renamed once synced, its folder synced with the call's
5//! batch (Appendix C.3); and the removal of what a call made,
6//! which removes a copy only while it is as coven left it.
7//!
8//! The folder is opened once. On Unix every folder, copy and rename below it
9//! is made relative to that open handle, one name at a time, never following
10//! a symbolic link, so a folder swapped for a link after planning can't send
11//! a write elsewhere. Windows has no such calls in the standard library, and
12//! its writes go by path, each checked for links just before it.
13
14#[cfg(windows)]
15use super::atomic_file::{create_new, rename_new};
16use super::atomic_file::{remove, sync_file};
17#[cfg(unix)]
18use super::flush_batch::BatchFolder;
19use super::observed_file::observe_metadata;
20use super::relative_path::local_name;
21use super::{FileError, FlushBatch, ObservationError, ObservedFile, RelativePath};
22use crate::task::BugSinkRef;
23use std::collections::BTreeMap;
24use std::fs;
25use std::io::{self, Write};
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28use std::time::SystemTime;
29
30/// What one call makes below the folder: the folders that don't exist yet,
31/// each before the folders inside it, and each file's path, in the order of
32/// the relative paths.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct CopyPlan {
35    /// The folders to make, outermost first.
36    pub folders: Vec<PathBuf>,
37    /// Each file's path.
38    pub files: Vec<PathBuf>,
39}
40
41/// Why a copy can't go where the app named.
42#[derive(Debug, thiserror::Error)]
43pub enum CopyPathError {
44    /// The folder isn't an absolute path to a folder, or a path below it
45    /// passes through something that isn't a folder.
46    #[error("{} is not a folder", .0.display())]
47    NotAFolder(PathBuf),
48    /// A path below the folder passes through a symbolic link.
49    #[error("{} is a symbolic link", .0.display())]
50    Link(PathBuf),
51    /// Something is at a file's path already.
52    #[error("{} is taken", .0.display())]
53    Exists(PathBuf),
54    /// Two paths of the call are one path once each name is made valid
55    /// (§17.3), as `Why?.flac` and `Why?.flac` are, or differ only in case
56    /// or Unicode form, as `Album/Track.flac` and `album/track.flac` do. Many
57    /// volumes, the default macOS and Windows ones among them, hold such
58    /// names as one, so the second copy could never be placed. Each path is
59    /// as the app named it, up to the name that clashes.
60    #[error("{path} and {other} name one path")]
61    Clash {
62        /// The later path.
63        path: RelativePath,
64        /// The earlier path it clashes with.
65        other: RelativePath,
66    },
67    /// The folder's path isn't valid Unicode, which coven records as text.
68    #[error("{} is not valid Unicode", .0.display())]
69    NotUnicode(PathBuf),
70    /// Inspecting a path failed.
71    #[error(transparent)]
72    File(FileError),
73    /// Inspecting the paths panicked on the blocking pool: a bug, which the
74    /// caller reports (E15).
75    #[error(transparent)]
76    Bug(#[from] crate::task::Bug),
77}
78
79impl From<FileError> for CopyPathError {
80    fn from(error: FileError) -> Self {
81        match error {
82            FileError::Bug(bug) => Self::Bug(bug),
83            error => Self::File(error),
84        }
85    }
86}
87
88impl crate::task::CarriesBug for CopyPathError {
89    fn bug(&self) -> Option<&crate::task::Bug> {
90        match self {
91            Self::Bug(bug) => Some(bug),
92            Self::File(error) => error.bug(),
93            _ => None,
94        }
95    }
96}
97
98/// The folder the person picked, open for the copies one call makes.
99pub struct CopyFolder {
100    path: PathBuf,
101    #[cfg(unix)]
102    handle: Arc<rustix::fd::OwnedFd>,
103    /// Where a panic in work its caller stopped waiting for goes (E15).
104    bugs: BugSinkRef,
105}
106
107impl CopyFolder {
108    /// Open `folder`, an absolute path to a folder whose path is valid
109    /// Unicode. The folder itself may be reached through a link: the person
110    /// picked it. A panic in the copies' work whose caller stopped waiting
111    /// goes to `bugs` (E15).
112    pub async fn open(folder: PathBuf, bugs: BugSinkRef) -> Result<Self, CopyPathError> {
113        let opened = bugs.clone();
114        blocking("open folder", &opened, move || {
115            if !folder.is_absolute() {
116                return Err(CopyPathError::NotAFolder(folder));
117            }
118            if folder.to_str().is_none() {
119                return Err(CopyPathError::NotUnicode(folder));
120            }
121            match fs::metadata(&folder) {
122                Ok(metadata) if metadata.is_dir() => {}
123                Ok(_) => return Err(CopyPathError::NotAFolder(folder)),
124                Err(error) if error.kind() == io::ErrorKind::NotFound => {
125                    return Err(CopyPathError::NotAFolder(folder))
126                }
127                Err(source) => return Err(FileError::at("inspect folder", &folder, source).into()),
128            }
129            #[cfg(unix)]
130            {
131                use rustix::fs::{Mode, OFlags};
132                let handle = rustix::fs::open(
133                    &folder,
134                    OFlags::DIRECTORY | OFlags::RDONLY | OFlags::CLOEXEC,
135                    Mode::empty(),
136                )
137                .map_err(|errno| FileError::at("open folder", &folder, errno.into()))?;
138                Ok(Self { path: folder, handle: Arc::new(handle), bugs })
139            }
140            #[cfg(windows)]
141            Ok(Self { path: folder, bugs })
142        })
143        .await
144    }
145
146    /// Plan the copies at `paths`, each name written as the one rule for
147    /// every platform makes it (§17.3). Only what lies below the folder is
148    /// checked for links.
149    pub async fn plan(&self, paths: Vec<RelativePath>) -> Result<CopyPlan, CopyPathError> {
150        let folder = self.path.clone();
151        blocking("plan copies", &self.bugs, move || plan(&folder, &paths)).await
152    }
153
154    /// Make the folder at `path`, a path of a plan, and sync the folder
155    /// holding it. True when this call made it; false when a folder was
156    /// there already, made by someone else, which the call never removes.
157    pub async fn create_folder(&self, path: PathBuf) -> Result<bool, FileError> {
158        let folder = self.clone_handle();
159        blocking("make folder", &self.bugs, move || folder.make_folder(&path)).await
160    }
161
162    /// Start a copy's bytes at `temporary`, a new file in its final folder.
163    pub async fn create_copy(&self, temporary: PathBuf) -> Result<CopyWriter, FileError> {
164        let folder = self.clone_handle();
165        blocking("create copy", &self.bugs, move || folder.make_copy(temporary)).await
166    }
167
168    /// Observe the copy at `path`, a path below the folder, without reading
169    /// its bytes: its size and modification time, for the original it
170    /// becomes.
171    pub async fn observe(&self, path: &Path) -> Result<ObservedFile, ObservationError> {
172        self.names(path).map_err(|source| FileError::at("observe copy", path, source))?;
173        observe_metadata(path).await
174    }
175
176    fn clone_handle(&self) -> CopyFolder {
177        Self {
178            path: self.path.clone(),
179            #[cfg(unix)]
180            handle: self.handle.clone(),
181            bugs: self.bugs.clone(),
182        }
183    }
184
185    /// The names below the folder leading to `path`.
186    fn names<'a>(&self, path: &'a Path) -> io::Result<Vec<&'a std::ffi::OsStr>> {
187        let names: Vec<_> = path
188            .strip_prefix(&self.path)
189            .map_err(|_outside| io::Error::new(io::ErrorKind::InvalidInput, "outside the folder"))?
190            .iter()
191            .collect();
192        if names.is_empty() {
193            return Err(io::Error::new(io::ErrorKind::InvalidInput, "the folder itself"));
194        }
195        Ok(names)
196    }
197
198    /// The folder holding `path`, opened one name at a time from the
199    /// folder's handle without following a link, and `path`'s own name.
200    #[cfg(unix)]
201    fn parent<'a>(
202        &self,
203        path: &'a Path,
204    ) -> io::Result<(Option<rustix::fd::OwnedFd>, &'a std::ffi::OsStr)> {
205        use rustix::fd::AsFd;
206        use rustix::fs::{Mode, OFlags};
207        let names = self.names(path)?;
208        let (last, folders) = names.split_last().expect("a path below the folder names something");
209        let mut opened: Option<rustix::fd::OwnedFd> = None;
210        for name in folders {
211            let at = opened.as_ref().map_or(self.handle.as_fd(), |fd| fd.as_fd());
212            opened = Some(rustix::fs::openat(
213                at,
214                *name,
215                OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::RDONLY | OFlags::CLOEXEC,
216                Mode::empty(),
217            )?);
218        }
219        Ok((opened, last))
220    }
221
222    #[cfg(unix)]
223    fn make_folder(&self, path: &Path) -> Result<bool, FileError> {
224        use rustix::fd::AsFd;
225        use rustix::fs::{AtFlags, FileType, Mode};
226        let at = |source: io::Error| FileError::at("create folder", path, source);
227        let (opened, name) = self.parent(path).map_err(at)?;
228        let parent = opened.as_ref().map_or(self.handle.as_fd(), |fd| fd.as_fd());
229        match rustix::fs::mkdirat(parent, name, Mode::from(0o777)) {
230            Ok(()) => {
231                rustix::fs::fsync(parent).map_err(|errno| at(errno.into()))?;
232                Ok(true)
233            }
234            Err(rustix::io::Errno::EXIST) => {
235                let stat = rustix::fs::statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
236                    .map_err(|errno| at(errno.into()))?;
237                if FileType::from_raw_mode(stat.st_mode) == FileType::Directory {
238                    Ok(false)
239                } else {
240                    Err(at(rustix::io::Errno::EXIST.into()))
241                }
242            }
243            Err(errno) => Err(at(errno.into())),
244        }
245    }
246
247    #[cfg(unix)]
248    fn make_copy(&self, temporary: PathBuf) -> Result<CopyWriter, FileError> {
249        use rustix::fd::AsFd;
250        use rustix::fs::{Mode, OFlags};
251        let at = |source: io::Error| FileError::at("create copy", &temporary, source);
252        let (opened, name) = self.parent(&temporary).map_err(at)?;
253        let parent = match opened {
254            Some(opened) => opened,
255            None => rustix::io::fcntl_dupfd_cloexec(self.handle.as_fd(), 0)
256                .map_err(|errno| at(errno.into()))?,
257        };
258        let file = rustix::fs::openat(
259            parent.as_fd(),
260            name,
261            OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
262            Mode::from(0o666),
263        )
264        .map_err(|errno| at(errno.into()))?;
265        let name = name.to_owned();
266        Ok(CopyWriter {
267            file: Arc::new(tokio::sync::Mutex::new(fs::File::from(file))),
268            path: temporary,
269            parent: Arc::new(parent),
270            name,
271            bugs: self.bugs.clone(),
272        })
273    }
274
275    /// Each folder between this one and `path` must still be a folder, and
276    /// not a link, just before a write by path.
277    #[cfg(windows)]
278    fn check_below(&self, path: &Path) -> io::Result<()> {
279        let names = self.names(path)?;
280        let mut below = self.path.clone();
281        for name in &names[..names.len() - 1] {
282            below.push(name);
283            let metadata = fs::symlink_metadata(&below)?;
284            if metadata.file_type().is_symlink() || !metadata.is_dir() {
285                return Err(io::Error::new(io::ErrorKind::InvalidInput, "not a folder"));
286            }
287        }
288        Ok(())
289    }
290
291    #[cfg(windows)]
292    fn make_folder(&self, path: &Path) -> Result<bool, FileError> {
293        let at = |source: io::Error| FileError::at("create folder", path, source);
294        self.check_below(path).map_err(at)?;
295        match fs::create_dir(path) {
296            Ok(()) => Ok(true),
297            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
298                let metadata = fs::symlink_metadata(path).map_err(at)?;
299                if metadata.is_dir() && !metadata.file_type().is_symlink() {
300                    Ok(false)
301                } else {
302                    Err(at(error))
303                }
304            }
305            Err(error) => Err(at(error)),
306        }
307    }
308
309    #[cfg(windows)]
310    fn make_copy(&self, temporary: PathBuf) -> Result<CopyWriter, FileError> {
311        let at = |source: io::Error| FileError::at("create copy", &temporary, source);
312        self.check_below(&temporary).map_err(at)?;
313        let file = create_new(&temporary).map_err(at)?;
314        Ok(CopyWriter {
315            file: Arc::new(tokio::sync::Mutex::new(file)),
316            folder: self.clone_handle(),
317            path: temporary,
318            bugs: self.bugs.clone(),
319        })
320    }
321}
322
323fn plan(folder: &Path, paths: &[RelativePath]) -> Result<CopyPlan, CopyPathError> {
324    if !folder.is_absolute() {
325        return Err(CopyPathError::NotAFolder(folder.to_owned()));
326    }
327    if folder.to_str().is_none() {
328        return Err(CopyPathError::NotUnicode(folder.to_owned()));
329    }
330    match fs::metadata(folder) {
331        Ok(metadata) if metadata.is_dir() => {}
332        Ok(_) => return Err(CopyPathError::NotAFolder(folder.to_owned())),
333        Err(error) if error.kind() == io::ErrorKind::NotFound => {
334            return Err(CopyPathError::NotAFolder(folder.to_owned()))
335        }
336        Err(source) => return Err(FileError::at("inspect folder", folder, source).into()),
337    }
338    let mut plan = CopyPlan { folders: Vec::new(), files: Vec::new() };
339    // Each path the call names, folder or file, by the names coven writes
340    // for it as a volume that ignores case and Unicode form compares them,
341    // with the path as the app named it.
342    let mut named: BTreeMap<Vec<String>, Named> = BTreeMap::new();
343    for relative in paths {
344        let names: Vec<&str> = relative.names().collect();
345        let (last, folders) = names.split_last().expect("a relative path names something");
346        let mut path = folder.to_path_buf();
347        let mut key = Vec::new();
348        for (index, name) in folders.iter().enumerate() {
349            let local = local_name(name);
350            key.push(one_name(&local));
351            path.push(local);
352            let here = relative.first(index + 1);
353            match named.get(&key) {
354                Some(Named::Folder(other)) if *other == here => continue,
355                Some(Named::Folder(other) | Named::File(other)) => {
356                    return Err(CopyPathError::Clash { path: here, other: other.clone() })
357                }
358                None => {}
359            }
360            named.insert(key.clone(), Named::Folder(here));
361            match fs::symlink_metadata(&path) {
362                Ok(metadata) if metadata.file_type().is_symlink() => {
363                    return Err(CopyPathError::Link(path))
364                }
365                Ok(metadata) if metadata.is_dir() => {}
366                Ok(_) => return Err(CopyPathError::NotAFolder(path)),
367                Err(error) if error.kind() == io::ErrorKind::NotFound => {
368                    plan.folders.push(path.clone());
369                }
370                Err(source) => return Err(FileError::at("inspect folder", &path, source).into()),
371            }
372        }
373        let local = local_name(last);
374        key.push(one_name(&local));
375        path.push(local);
376        if let Some(Named::Folder(other) | Named::File(other)) = named.get(&key) {
377            return Err(CopyPathError::Clash { path: relative.clone(), other: other.clone() });
378        }
379        named.insert(key, Named::File(relative.clone()));
380        match fs::symlink_metadata(&path) {
381            Ok(_) => return Err(CopyPathError::Exists(path)),
382            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
383            Err(source) => return Err(FileError::at("inspect file", &path, source).into()),
384        }
385        plan.files.push(path);
386    }
387    Ok(plan)
388}
389
390/// A path a plan names, as the app named it, as a folder on the way to a
391/// file or as a file.
392enum Named {
393    Folder(RelativePath),
394    File(RelativePath),
395}
396
397/// `name` as a volume that ignores case and Unicode form compares it: its
398/// canonical decomposition, case-folded, decomposed again, as Unicode's
399/// canonical caseless match does. Two names alike here are one name on such
400/// a volume. A volume folding a few letters differently still refuses the
401/// second copy's rename, which never replaces what is there.
402fn one_name(name: &str) -> String {
403    let decompose = icu_normalizer::DecomposingNormalizerBorrowed::new_nfd();
404    let folded =
405        icu_casemap::CaseMapperBorrowed::new().fold_string(&decompose.normalize(name)).into_owned();
406    decompose.normalize(&folded).into_owned()
407}
408
409/// A copy being written under its temporary name. Dropping it leaves the
410/// bytes for the caller's recorded cleanup.
411pub struct CopyWriter {
412    file: Arc<tokio::sync::Mutex<fs::File>>,
413    path: PathBuf,
414    /// The open folder holding the copy, which its rename is relative to.
415    #[cfg(unix)]
416    parent: Arc<rustix::fd::OwnedFd>,
417    #[cfg(unix)]
418    name: std::ffi::OsString,
419    #[cfg(windows)]
420    folder: CopyFolder,
421    /// Where a panic in a write its caller stopped waiting for goes (E15).
422    bugs: BugSinkRef,
423}
424
425impl CopyWriter {
426    /// Append one bounded buffer, completing its disk write before returning.
427    pub async fn append(&mut self, bytes: &[u8]) -> Result<(), FileError> {
428        let bytes = bytes.to_vec();
429        self.run("write copy", move |file| file.write_all(&bytes)).await
430    }
431
432    /// Sync the bytes, then rename them to `path`, where nothing may be, in
433    /// the same folder, so the copy is whole under its final name or not
434    /// there. The copy counts in `batch`, whose flush syncs the folder and
435    /// makes the copy durable before the commit that records it.
436    pub async fn publish(self, path: &Path, batch: &mut FlushBatch) -> Result<(), FileError> {
437        self.run("sync copy", |file| sync_file(file)).await?;
438        let to = path.to_owned();
439        #[cfg(unix)]
440        {
441            let (parent, from) = (self.parent.clone(), self.name.clone());
442            let folder = blocking("rename copy", &self.bugs, move || {
443                use rustix::fs::RenameFlags;
444                let (name, folder) = match (to.file_name(), to.parent()) {
445                    (Some(name), Some(folder)) => (name, folder),
446                    _ => {
447                        return Err(FileError::at(
448                            "rename copy",
449                            &to,
450                            io::Error::new(io::ErrorKind::InvalidInput, "file name required"),
451                        ))
452                    }
453                };
454                rustix::fs::renameat_with(&*parent, &from, &*parent, name, RenameFlags::NOREPLACE)
455                    .map_err(|errno| FileError::at("rename copy", &to, errno.into()))?;
456                BatchFolder::held(folder.to_owned(), parent, None)
457                    .map_err(|source| FileError::at("inspect copy folder", &to, source))
458            })
459            .await?;
460            batch.add(folder);
461            Ok(())
462        }
463        #[cfg(windows)]
464        {
465            let (folder, from) = (self.folder.clone_handle(), self.path.clone());
466            blocking("rename copy", &self.bugs, move || {
467                folder
468                    .check_below(&to)
469                    .map_err(|source| FileError::at("rename copy", &to, source))?;
470                rename_new(&from, &to).map_err(|source| FileError::at("rename copy", &to, source))
471            })
472            .await?;
473            batch.add();
474            Ok(())
475        }
476    }
477
478    async fn run(
479        &self,
480        operation: &'static str,
481        run: impl FnOnce(&mut fs::File) -> io::Result<()> + Send + 'static,
482    ) -> Result<(), FileError> {
483        // Acquire before spawning so later calls wait for cancelled work too.
484        let mut file = self.file.clone().lock_owned().await;
485        let path = self.path.clone();
486        blocking(operation, &self.bugs, move || {
487            run(&mut file).map_err(|source| FileError::at(operation, &path, source))
488        })
489        .await
490    }
491}
492
493/// A copy coven recorded writing in a folder the person picked, as its
494/// record reads back (§17.3): its bytes still at their temporary name, or
495/// placed at its path with the size and modification time they had there.
496/// It is made only where coven reads that record.
497#[derive(Debug)]
498pub struct RecordedCopy {
499    path: PathBuf,
500    placed: Option<(u64, SystemTime)>,
501}
502
503impl RecordedCopy {
504    /// A copy still being written, at its temporary name `temporary`.
505    pub fn writing(temporary: PathBuf) -> Self {
506        Self { path: temporary, placed: None }
507    }
508
509    /// A copy placed at `path` with `size` and `modified_at`.
510    pub fn placed(path: PathBuf, size: u64, modified_at: SystemTime) -> Self {
511        Self { path, placed: Some((size, modified_at)) }
512    }
513
514    /// Remove the copy's bytes. A placed copy goes only if it still has the
515    /// size and modification time coven recorded: one that changed since is
516    /// the person's, and stays. Absent bytes are gone already.
517    pub fn remove(&self) -> Result<(), FileError> {
518        let Some((size, modified_at)) = self.placed else {
519            return remove(&self.path);
520        };
521        let path = self.path.as_path();
522        let metadata = match fs::symlink_metadata(path) {
523            Ok(metadata) => metadata,
524            Err(error) if error.kind() == io::ErrorKind::NotFound => {
525                tracing::debug!(path = %path.display(), "copy is already absent");
526                return Ok(());
527            }
528            Err(source) => return Err(FileError::at("inspect copy", path, source)),
529        };
530        let modified = metadata
531            .modified()
532            .map_err(|source| FileError::at("read modification time", path, source))?;
533        if !metadata.is_file() || metadata.len() != size || modified != modified_at {
534            tracing::debug!(path = %path.display(), "leaving a copy the person changed");
535            return Ok(());
536        }
537        remove(path)
538    }
539}
540
541/// A folder coven made for copies in a folder the person picked, as its
542/// record reads back (§17.3). It is made only where coven reads that record.
543#[derive(Debug)]
544pub struct MadeFolder {
545    path: PathBuf,
546}
547
548impl MadeFolder {
549    /// The folder coven made at `path`.
550    pub fn at(path: PathBuf) -> Self {
551        Self { path }
552    }
553
554    /// Remove the folder if nothing is in it: a folder holding anything,
555    /// the person's files included, stays. An absent one is gone.
556    pub fn remove(&self) -> Result<(), FileError> {
557        super::atomic_file::remove_empty_folder(&self.path)
558    }
559}
560
561/// Run `run`, file work named `operation`, on the blocking pool; a panic in
562/// it is `FileError::Bug`, and the pool dropping it unstarted
563/// `FileError::Shutdown`. Once its caller stops waiting, its panic, or a bug
564/// its error carries, goes to `bugs` (E15).
565async fn blocking<T, E>(
566    operation: &'static str,
567    bugs: &BugSinkRef,
568    run: impl FnOnce() -> Result<T, E> + Send + 'static,
569) -> Result<T, E>
570where
571    T: Send + 'static,
572    E: From<FileError> + crate::task::CarriesBug + Send + 'static,
573{
574    crate::task::blocking_completing(operation, bugs.clone(), run).await.map_err(FileError::from)?
575}
576
577#[cfg(test)]
578#[path = "folder_copy_tests.rs"]
579mod tests;