Skip to main content

harmos_runtime/filesystem/publication/
mod.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12#[cfg(unix)]
13mod unix;
14
15// The Windows Publication Primitives Are A Win32 FFI Boundary; Each Call Carries Its SAFETY Argument.
16#[cfg(windows)]
17#[allow(unsafe_code)]
18mod windows;
19
20/* === Re-Exports === */
21
22/* === Imports === */
23
24// Standard Library Imports
25use std::{
26    fs::{self, File, OpenOptions},
27    io::{self, Write},
28    path::{Path, PathBuf},
29};
30// Third Party Imports
31use tempfile::NamedTempFile;
32// Local Imports
33#[cfg(unix)]
34use unix as platform;
35
36#[cfg(windows)]
37use windows as platform;
38
39/* === Constants === */
40
41/* === Macros === */
42
43/* === Type Aliases === */
44
45/* === Structs === */
46
47struct Staging(PathBuf);
48
49/* === Enums === */
50
51#[derive(Debug, thiserror::Error)]
52pub enum ConfirmError<E> {
53    #[error(transparent)]
54    Filesystem(crate::filesystem::Error),
55
56    #[error(transparent)]
57    Checkpoint(E),
58}
59
60/* === Traits === */
61
62/* === Implementations === */
63
64impl Staging {
65    fn file(directory: &Path) -> io::Result<(Self, File)> {
66        let temporary = NamedTempFile::new_in(directory)?;
67        let (file, path) = temporary.keep().map_err(|error| error.error)?;
68        Ok((Self(path), file))
69    }
70}
71
72impl Drop for Staging {
73    fn drop(&mut self) {
74        let _ = fs::remove_file(&self.0);
75    }
76}
77
78/* === Public Functions === */
79
80pub fn create_dir_all(path: &Path) -> crate::filesystem::Result<()> {
81    platform::create_dir_all(path).map_err(|error| crate::filesystem::Error::publication("create directory", path, error))
82}
83
84pub fn move_directory(source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
85    let metadata =
86        fs::symlink_metadata(source).map_err(|error| crate::filesystem::Error::publication("inspect directory", source, error))?;
87    if !metadata.is_dir() || metadata.file_type().is_symlink() {
88        return Err(crate::filesystem::Error::publication(
89            "move directory",
90            source,
91            io::Error::other("source must be a regular directory"),
92        ));
93    }
94    platform::move_directory(source, destination)
95        .map_err(|error| crate::filesystem::Error::publication("move directory", destination, error))
96}
97
98pub fn install(source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
99    sync_file(source).map_err(|error| crate::filesystem::Error::publication("flush staged file", source, error))?;
100    platform::install(source, destination).map_err(|error| crate::filesystem::Error::publication("install file", destination, error))
101}
102
103pub fn replace(source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
104    sync_file(source).map_err(|error| crate::filesystem::Error::publication("flush staged file", source, error))?;
105    platform::replace(source, destination).map_err(|error| crate::filesystem::Error::publication("replace file", destination, error))
106}
107
108pub fn remove(path: &Path) -> crate::filesystem::Result<()> {
109    match fs::symlink_metadata(parent(path)) {
110        Ok(metadata) if !metadata.is_dir() || metadata.file_type().is_symlink() => {
111            return Err(crate::filesystem::Error::publication(
112                "remove file",
113                path,
114                io::Error::new(io::ErrorKind::InvalidInput, "parent must be a regular directory"),
115            ));
116        }
117        Ok(_) => (),
118        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
119        Err(error) => {
120            return Err(crate::filesystem::Error::publication("inspect parent", path, error));
121        }
122    }
123    match fs::symlink_metadata(path) {
124        Ok(metadata) if !metadata.is_file() || metadata.file_type().is_symlink() => {
125            return Err(crate::filesystem::Error::publication(
126                "remove file",
127                path,
128                io::Error::new(io::ErrorKind::InvalidInput, "source must be a regular file"),
129            ));
130        }
131        Ok(_) => (),
132        Err(error) if error.kind() == io::ErrorKind::NotFound => (),
133        Err(error) => {
134            return Err(crate::filesystem::Error::publication("inspect file", path, error));
135        }
136    }
137    platform::remove(path).map_err(|error| crate::filesystem::Error::publication("remove file", path, error))
138}
139
140pub fn confirm(path: &Path) -> crate::filesystem::Result<()> {
141    match confirm_with_checkpoint(path, || Ok::<(), std::convert::Infallible>(())) {
142        Ok(()) => Ok(()),
143        Err(ConfirmError::Filesystem(error)) => Err(error),
144        Err(ConfirmError::Checkpoint(error)) => match error {},
145    }
146}
147
148pub fn confirm_with_checkpoint<E>(path: &Path, mut checkpoint: impl FnMut() -> Result<(), E>) -> Result<(), ConfirmError<E>> {
149    platform::confirm_with_checkpoint(path, &mut checkpoint)
150}
151
152#[tracing::instrument(level = "debug", skip_all)]
153pub fn write(path: &Path, bytes: &[u8]) -> crate::filesystem::Result<()> {
154    write_document(path, bytes).map_err(|error| crate::filesystem::Error::publication("write document", path, error))
155}
156
157/* === Private Functions === */
158
159fn confirm_error<E>(path: &Path, error: io::Error) -> ConfirmError<E> {
160    let error = crate::filesystem::Error::publication("confirm file durability", path, error);
161    ConfirmError::Filesystem(error)
162}
163
164fn write_document(path: &Path, bytes: &[u8]) -> io::Result<()> {
165    create_dir_all(parent(path))?;
166    let (staging, mut file) = Staging::file(parent(path))?;
167    file.write_all(bytes)?;
168    file.sync_all()?;
169    drop(file);
170    replace(&staging.0, path).map_err(Into::into)
171}
172
173fn sync_file(path: &Path) -> io::Result<()> {
174    // FlushFileBuffers requires GENERIC_WRITE. Read-only File::open is not a
175    // portable way to confirm file durability, even if no bytes are changed.
176    OpenOptions::new().read(true).write(true).open(path)?.sync_all()
177}
178
179fn parent(path: &Path) -> &Path {
180    path.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or(Path::new("."))
181}
182
183/* === Tests === */