harmos_runtime/filesystem/publication/
mod.rs1#[cfg(unix)]
13mod unix;
14
15#[cfg(windows)]
17#[allow(unsafe_code)]
18mod windows;
19
20use std::{
26 fs::{self, File, OpenOptions},
27 io::{self, Write},
28 path::{Path, PathBuf},
29};
30use tempfile::NamedTempFile;
32#[cfg(unix)]
34use unix as platform;
35
36#[cfg(windows)]
37use windows as platform;
38
39struct Staging(PathBuf);
48
49#[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
60impl 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
78pub 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 install_opened(file: &File, source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
106 file.sync_all().map_err(|error| crate::filesystem::Error::publication("flush staged file", source, error))?;
107 platform::install_opened(file, source, destination)
108 .map_err(|error| crate::filesystem::Error::publication("install file", destination, error))
109}
110
111pub fn replace(source: &Path, destination: &Path) -> crate::filesystem::Result<()> {
112 sync_file(source).map_err(|error| crate::filesystem::Error::publication("flush staged file", source, error))?;
113 platform::replace(source, destination).map_err(|error| crate::filesystem::Error::publication("replace file", destination, error))
114}
115
116pub fn remove(path: &Path) -> crate::filesystem::Result<()> {
117 match fs::symlink_metadata(parent(path)) {
118 Ok(metadata) if !metadata.is_dir() || metadata.file_type().is_symlink() => {
119 return Err(crate::filesystem::Error::publication(
120 "remove file",
121 path,
122 io::Error::new(io::ErrorKind::InvalidInput, "parent must be a regular directory"),
123 ));
124 }
125 Ok(_) => (),
126 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
127 Err(error) => {
128 return Err(crate::filesystem::Error::publication("inspect parent", path, error));
129 }
130 }
131 match fs::symlink_metadata(path) {
132 Ok(metadata) if !metadata.is_file() || metadata.file_type().is_symlink() => {
133 return Err(crate::filesystem::Error::publication(
134 "remove file",
135 path,
136 io::Error::new(io::ErrorKind::InvalidInput, "source must be a regular file"),
137 ));
138 }
139 Ok(_) => (),
140 Err(error) if error.kind() == io::ErrorKind::NotFound => (),
141 Err(error) => {
142 return Err(crate::filesystem::Error::publication("inspect file", path, error));
143 }
144 }
145 platform::remove(path).map_err(|error| crate::filesystem::Error::publication("remove file", path, error))
146}
147
148pub fn confirm(path: &Path) -> crate::filesystem::Result<()> {
149 match confirm_with_checkpoint(path, || Ok::<(), std::convert::Infallible>(())) {
150 Ok(()) => Ok(()),
151 Err(ConfirmError::Filesystem(error)) => Err(error),
152 Err(ConfirmError::Checkpoint(error)) => match error {},
153 }
154}
155
156pub fn confirm_with_checkpoint<E>(path: &Path, mut checkpoint: impl FnMut() -> Result<(), E>) -> Result<(), ConfirmError<E>> {
157 platform::confirm_with_checkpoint(path, &mut checkpoint)
158}
159
160#[tracing::instrument(level = "debug", skip_all)]
161pub fn write(path: &Path, bytes: &[u8]) -> crate::filesystem::Result<()> {
162 write_document(path, bytes).map_err(|error| crate::filesystem::Error::publication("write document", path, error))
163}
164
165fn confirm_error<E>(path: &Path, error: io::Error) -> ConfirmError<E> {
168 let error = crate::filesystem::Error::publication("confirm file durability", path, error);
169 ConfirmError::Filesystem(error)
170}
171
172fn write_document(path: &Path, bytes: &[u8]) -> io::Result<()> {
173 create_dir_all(parent(path))?;
174 let (staging, mut file) = Staging::file(parent(path))?;
175 file.write_all(bytes)?;
176 file.sync_all()?;
177 drop(file);
178 replace(&staging.0, path).map_err(Into::into)
179}
180
181fn sync_file(path: &Path) -> io::Result<()> {
182 OpenOptions::new().read(true).write(true).open(path)?.sync_all()
185}
186
187fn parent(path: &Path) -> &Path {
188 path.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or(Path::new("."))
189}
190
191#[cfg(test)]
194mod tests {
195 use super::*;
196 use std::io::{Read, Seek, SeekFrom};
197
198 #[test]
199 fn opened_install_preserves_identity_and_refuses_replacement() {
200 let root = tempfile::tempdir().unwrap();
201 let source = root.path().join("source");
202 let destination = root.path().join("destination");
203 let mut file = OpenOptions::new().create_new(true).read(true).write(true).open(&source).unwrap();
204 file.write_all(b"evidence").unwrap();
205 install_opened(&file, &source, &destination).unwrap();
206 file.seek(SeekFrom::Start(0)).unwrap();
207 let mut bytes = Vec::new();
208 file.read_to_end(&mut bytes).unwrap();
209 assert_eq!(bytes, b"evidence");
210 assert_eq!(fs::read(&destination).unwrap(), bytes);
211 let other = root.path().join("other");
212 let mut replacement = File::create(&other).unwrap();
213 replacement.write_all(b"different").unwrap();
214 assert!(install_opened(&replacement, &other, &destination).is_err());
215 assert_eq!(fs::read(&destination).unwrap(), b"evidence");
216 assert_eq!(fs::read(&other).unwrap(), b"different");
217 }
218}