reth_fs_util/
lib.rs

1//! Wrapper for `std::fs` methods
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9use serde::{de::DeserializeOwned, Serialize};
10use std::{
11    fs::{self, File, OpenOptions, ReadDir},
12    io::{self, BufWriter, Error, Write},
13    path::{Path, PathBuf},
14};
15
16/// Result alias for [`FsPathError`].
17pub type Result<T> = std::result::Result<T, FsPathError>;
18
19/// Various error variants for `std::fs` operations that serve as an addition to the `io::Error`
20/// which does not provide any information about the path.
21#[derive(Debug, thiserror::Error)]
22pub enum FsPathError {
23    /// Error variant for failed write operation with additional path context.
24    #[error("failed to write to {path:?}: {source}")]
25    Write {
26        /// The source `io::Error`.
27        source: io::Error,
28        /// The path related to the operation.
29        path: PathBuf,
30    },
31
32    /// Error variant for failed read operation with additional path context.
33    #[error("failed to read from {path:?}: {source}")]
34    Read {
35        /// The source `io::Error`.
36        source: io::Error,
37        /// The path related to the operation.
38        path: PathBuf,
39    },
40
41    /// Error variant for failed read link operation with additional path context.
42    #[error("failed to read from {path:?}: {source}")]
43    ReadLink {
44        /// The source `io::Error`.
45        source: io::Error,
46        /// The path related to the operation.
47        path: PathBuf,
48    },
49
50    /// Error variant for failed file creation operation with additional path context.
51    #[error("failed to create file {path:?}: {source}")]
52    CreateFile {
53        /// The source `io::Error`.
54        source: io::Error,
55        /// The path related to the operation.
56        path: PathBuf,
57    },
58
59    /// Error variant for failed file removal operation with additional path context.
60    #[error("failed to remove file {path:?}: {source}")]
61    RemoveFile {
62        /// The source `io::Error`.
63        source: io::Error,
64        /// The path related to the operation.
65        path: PathBuf,
66    },
67
68    /// Error variant for failed directory creation operation with additional path context.
69    #[error("failed to create dir {path:?}: {source}")]
70    CreateDir {
71        /// The source `io::Error`.
72        source: io::Error,
73        /// The path related to the operation.
74        path: PathBuf,
75    },
76
77    /// Error variant for failed directory removal operation with additional path context.
78    #[error("failed to remove dir {path:?}: {source}")]
79    RemoveDir {
80        /// The source `io::Error`.
81        source: io::Error,
82        /// The path related to the operation.
83        path: PathBuf,
84    },
85
86    /// Error variant for failed directory read operation with additional path context.
87    #[error("failed to read dir {path:?}: {source}")]
88    ReadDir {
89        /// The source `io::Error`.
90        source: io::Error,
91        /// The path related to the operation.
92        path: PathBuf,
93    },
94
95    /// Error variant for failed file renaming operation with additional path context.
96    #[error("failed to rename {from:?} to {to:?}: {source}")]
97    Rename {
98        /// The source `io::Error`.
99        source: io::Error,
100        /// The original path.
101        from: PathBuf,
102        /// The target path.
103        to: PathBuf,
104    },
105
106    /// Error variant for failed file opening operation with additional path context.
107    #[error("failed to open file {path:?}: {source}")]
108    Open {
109        /// The source `io::Error`.
110        source: io::Error,
111        /// The path related to the operation.
112        path: PathBuf,
113    },
114
115    /// Error variant for failed file read as JSON operation with additional path context.
116    #[error("failed to parse json file: {path:?}: {source}")]
117    ReadJson {
118        /// The source `serde_json::Error`.
119        source: serde_json::Error,
120        /// The path related to the operation.
121        path: PathBuf,
122    },
123
124    /// Error variant for failed JSON write to file operation with additional path context.
125    #[error("failed to write to json file: {path:?}: {source}")]
126    WriteJson {
127        /// The source `serde_json::Error`.
128        source: serde_json::Error,
129        /// The path related to the operation.
130        path: PathBuf,
131    },
132
133    /// Error variant for failed file metadata operation with additional path context.
134    #[error("failed to get metadata for {path:?}: {source}")]
135    Metadata {
136        /// The source `io::Error`.
137        source: io::Error,
138        /// The path related to the operation.
139        path: PathBuf,
140    },
141    /// Error variant for failed fsync operation with additional path context.
142    #[error("failed to sync path {path:?}: {source}")]
143    Fsync {
144        /// The source `io::Error`.
145        source: io::Error,
146        /// The path related to the operation.
147        path: PathBuf,
148    },
149}
150
151impl FsPathError {
152    /// Returns the complementary error variant for [`std::fs::write`].
153    pub fn write(source: io::Error, path: impl Into<PathBuf>) -> Self {
154        Self::Write { source, path: path.into() }
155    }
156
157    /// Returns the complementary error variant for [`std::fs::read`].
158    pub fn read(source: io::Error, path: impl Into<PathBuf>) -> Self {
159        Self::Read { source, path: path.into() }
160    }
161
162    /// Returns the complementary error variant for [`std::fs::read_link`].
163    pub fn read_link(source: io::Error, path: impl Into<PathBuf>) -> Self {
164        Self::ReadLink { source, path: path.into() }
165    }
166
167    /// Returns the complementary error variant for [`std::fs::File::create`].
168    pub fn create_file(source: io::Error, path: impl Into<PathBuf>) -> Self {
169        Self::CreateFile { source, path: path.into() }
170    }
171
172    /// Returns the complementary error variant for [`std::fs::remove_file`].
173    pub fn remove_file(source: io::Error, path: impl Into<PathBuf>) -> Self {
174        Self::RemoveFile { source, path: path.into() }
175    }
176
177    /// Returns the complementary error variant for [`std::fs::create_dir`].
178    pub fn create_dir(source: io::Error, path: impl Into<PathBuf>) -> Self {
179        Self::CreateDir { source, path: path.into() }
180    }
181
182    /// Returns the complementary error variant for [`std::fs::remove_dir`].
183    pub fn remove_dir(source: io::Error, path: impl Into<PathBuf>) -> Self {
184        Self::RemoveDir { source, path: path.into() }
185    }
186
187    /// Returns the complementary error variant for [`std::fs::read_dir`].
188    pub fn read_dir(source: io::Error, path: impl Into<PathBuf>) -> Self {
189        Self::ReadDir { source, path: path.into() }
190    }
191
192    /// Returns the complementary error variant for [`std::fs::File::open`].
193    pub fn open(source: io::Error, path: impl Into<PathBuf>) -> Self {
194        Self::Open { source, path: path.into() }
195    }
196
197    /// Returns the complementary error variant for [`std::fs::rename`].
198    pub fn rename(source: io::Error, from: impl Into<PathBuf>, to: impl Into<PathBuf>) -> Self {
199        Self::Rename { source, from: from.into(), to: to.into() }
200    }
201
202    /// Returns the complementary error variant for [`std::fs::File::metadata`].
203    pub fn metadata(source: io::Error, path: impl Into<PathBuf>) -> Self {
204        Self::Metadata { source, path: path.into() }
205    }
206
207    /// Returns the complementary error variant for `fsync`.
208    pub fn fsync(source: io::Error, path: impl Into<PathBuf>) -> Self {
209        Self::Fsync { source, path: path.into() }
210    }
211}
212
213/// Wrapper for [`File::open`].
214pub fn open(path: impl AsRef<Path>) -> Result<File> {
215    let path = path.as_ref();
216    File::open(path).map_err(|err| FsPathError::open(err, path))
217}
218
219/// Wrapper for `std::fs::read_to_string`
220pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
221    let path = path.as_ref();
222    fs::read_to_string(path).map_err(|err| FsPathError::read(err, path))
223}
224
225/// Read the entire contents of a file into a bytes vector.
226///
227/// Wrapper for `std::fs::read`
228pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
229    let path = path.as_ref();
230    fs::read(path).map_err(|err| FsPathError::read(err, path))
231}
232
233/// Wrapper for `std::fs::write`
234pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
235    let path = path.as_ref();
236    fs::write(path, contents).map_err(|err| FsPathError::write(err, path))
237}
238
239/// Wrapper for `std::fs::remove_dir_all`
240pub fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
241    let path = path.as_ref();
242    fs::remove_dir_all(path).map_err(|err| FsPathError::remove_dir(err, path))
243}
244
245/// Wrapper for `File::create`.
246pub fn create_file(path: impl AsRef<Path>) -> Result<fs::File> {
247    let path = path.as_ref();
248    File::create(path).map_err(|err| FsPathError::create_file(err, path))
249}
250
251/// Wrapper for `std::fs::remove_file`
252pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
253    let path = path.as_ref();
254    fs::remove_file(path).map_err(|err| FsPathError::remove_file(err, path))
255}
256
257/// Wrapper for `std::fs::create_dir_all`
258pub fn create_dir_all(path: impl AsRef<Path>) -> Result<()> {
259    let path = path.as_ref();
260    fs::create_dir_all(path).map_err(|err| FsPathError::create_dir(err, path))
261}
262
263/// Wrapper for `std::fs::read_dir`
264pub fn read_dir(path: impl AsRef<Path>) -> Result<ReadDir> {
265    let path = path.as_ref();
266    fs::read_dir(path).map_err(|err| FsPathError::read_dir(err, path))
267}
268
269/// Wrapper for `std::fs::rename`
270pub fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
271    let from = from.as_ref();
272    let to = to.as_ref();
273    fs::rename(from, to).map_err(|err| FsPathError::rename(err, from, to))
274}
275
276/// Wrapper for `std::fs::metadata`
277pub fn metadata(path: impl AsRef<Path>) -> Result<fs::Metadata> {
278    let path = path.as_ref();
279    fs::metadata(path).map_err(|err| FsPathError::metadata(err, path))
280}
281
282/// Reads the JSON file and deserialize it into the provided type.
283pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
284    // read the file into a byte array first
285    // https://github.com/serde-rs/json/issues/160
286    let bytes = read(path)?;
287    serde_json::from_slice(&bytes)
288        .map_err(|source| FsPathError::ReadJson { source, path: path.into() })
289}
290
291/// Writes the object as a JSON object.
292pub fn write_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
293    let file = create_file(path)?;
294    let mut writer = BufWriter::new(file);
295    serde_json::to_writer_pretty(&mut writer, obj)
296        .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
297    writer.flush().map_err(|e| FsPathError::write(e, path))
298}
299
300/// Writes atomically to file.
301///
302/// 1. Creates a temporary file with a `.tmp` extension in the same file directory.
303/// 2. Writes content with `write_fn`.
304/// 3. Fsyncs the temp file to disk.
305/// 4. Renames the temp file to the target path.
306/// 5. Fsyncs the file directory.
307///
308/// Atomic writes are hard:
309/// * <https://github.com/paradigmxyz/reth/issues/8622>
310/// * <https://users.rust-lang.org/t/how-to-write-replace-files-atomically/42821/13>
311pub fn atomic_write_file<F, E>(file_path: &Path, write_fn: F) -> Result<()>
312where
313    F: FnOnce(&mut File) -> std::result::Result<(), E>,
314    E: Into<Box<dyn core::error::Error + Send + Sync>>,
315{
316    #[cfg(windows)]
317    use std::os::windows::fs::OpenOptionsExt;
318
319    let mut tmp_path = file_path.to_path_buf();
320    tmp_path.set_extension("tmp");
321
322    // Write to the temporary file
323    let mut file =
324        File::create(&tmp_path).map_err(|err| FsPathError::create_file(err, &tmp_path))?;
325
326    write_fn(&mut file).map_err(|err| FsPathError::Write {
327        source: Error::other(err.into()),
328        path: tmp_path.clone(),
329    })?;
330
331    // fsync() file
332    file.sync_all().map_err(|err| FsPathError::fsync(err, &tmp_path))?;
333
334    // Rename file, not move
335    rename(&tmp_path, file_path)?;
336
337    // fsync() directory
338    if let Some(parent) = file_path.parent() {
339        #[cfg(windows)]
340        OpenOptions::new()
341            .read(true)
342            .write(true)
343            .custom_flags(0x02000000) // FILE_FLAG_BACKUP_SEMANTICS
344            .open(parent)
345            .map_err(|err| FsPathError::open(err, parent))?
346            .sync_all()
347            .map_err(|err| FsPathError::fsync(err, parent))?;
348
349        #[cfg(not(windows))]
350        OpenOptions::new()
351            .read(true)
352            .open(parent)
353            .map_err(|err| FsPathError::open(err, parent))?
354            .sync_all()
355            .map_err(|err| FsPathError::fsync(err, parent))?;
356    }
357
358    Ok(())
359}