Skip to main content

wowlab_fs/
file.rs

1//! File reading, writing, and streaming.
2
3use std::io::{Read, Seek, Write};
4
5use crate::{
6    error::{Error, Operation, Result},
7    path::{Path, PathBuf},
8};
9
10#[cfg(unix)]
11const EXECUTABLE_MODE: u32 = 0o755;
12
13/// An open file handle without access to platform-specific filesystem APIs.
14#[derive(Debug)]
15pub struct File {
16    inner: std::fs::File,
17    path: PathBuf,
18}
19
20impl File {
21    pub(crate) fn from_open_file(inner: std::fs::File, path: PathBuf) -> Self {
22        Self { inner, path }
23    }
24
25    /// Return the path used to open this file.
26    #[must_use]
27    pub fn path(&self) -> &Path {
28        &self.path
29    }
30
31    /// Flush file contents and metadata to durable storage.
32    ///
33    /// # Errors
34    ///
35    /// Returns a contextual error when the operating system cannot synchronize the file.
36    pub fn sync(&self) -> Result<()> {
37        self.inner
38            .sync_all()
39            .map_err(|source| Error::new(Operation::Sync, &self.path, source))
40    }
41
42    #[cfg(unix)]
43    pub(crate) fn make_executable(&self) -> Result<()> {
44        use std::os::unix::fs::PermissionsExt as _;
45
46        self.inner
47            .set_permissions(std::fs::Permissions::from_mode(EXECUTABLE_MODE))
48            .map_err(|source| Error::new(Operation::SecureFile, &self.path, source))
49    }
50
51    #[cfg(all(not(unix), not(target_family = "wasm")))]
52    pub(crate) fn make_executable(&self) -> Result<()> {
53        Ok(())
54    }
55}
56
57impl Read for File {
58    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
59        self.inner.read(buf)
60    }
61}
62
63impl Write for File {
64    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
65        self.inner.write(buf)
66    }
67
68    fn flush(&mut self) -> std::io::Result<()> {
69        self.inner.flush()
70    }
71}
72
73impl Seek for File {
74    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
75        self.inner.seek(pos)
76    }
77}
78
79/// Open a file for reading.
80///
81/// # Errors
82///
83/// Returns a contextual error when the file cannot be opened.
84pub fn open(path: &Path) -> Result<File> {
85    std::fs::File::open(path)
86        .map(|inner| File::from_open_file(inner, path.to_path_buf()))
87        .map_err(|source| Error::new(Operation::OpenFile, path, source))
88}
89
90/// Create or truncate a file for streaming writes.
91///
92/// # Errors
93///
94/// Returns a contextual error when the file cannot be created.
95pub fn create(path: &Path) -> Result<File> {
96    std::fs::File::create(path)
97        .map(|inner| File::from_open_file(inner, path.to_path_buf()))
98        .map_err(|source| Error::new(Operation::CreateFile, path, source))
99}
100
101/// Create a new file without replacing an existing entry.
102///
103/// # Errors
104///
105/// Returns a contextual error when the file exists or cannot be created.
106pub fn create_new(path: &Path) -> Result<File> {
107    std::fs::OpenOptions::new()
108        .write(true)
109        .create_new(true)
110        .open(path)
111        .map(|inner| File::from_open_file(inner, path.to_path_buf()))
112        .map_err(|source| Error::new(Operation::CreateFile, path, source))
113}
114
115/// Read an entire file as bytes.
116///
117/// # Errors
118///
119/// Returns a contextual error when the file cannot be read.
120pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
121    std::fs::read(path).map_err(|source| Error::new(Operation::ReadFile, path, source))
122}
123
124/// Read an entire UTF-8 text file.
125///
126/// # Errors
127///
128/// Returns a contextual error for I/O failures or invalid UTF-8.
129pub fn read_text(path: &Path) -> Result<String> {
130    std::fs::read_to_string(path).map_err(|source| Error::new(Operation::ReadFile, path, source))
131}
132
133/// Read an entire UTF-8 text file when it exists.
134///
135/// # Errors
136///
137/// Returns a contextual error for failures other than a missing path.
138pub fn read_text_if_exists(path: &Path) -> Result<Option<String>> {
139    match read_text(path) {
140        Ok(contents) => Ok(Some(contents)),
141        Err(error) if error.is_not_found() => Ok(None),
142        Err(error) => Err(error),
143    }
144}
145
146/// Replace a file's contents directly.
147///
148/// Use [`crate::atomic::replace`] when interruption safety matters.
149///
150/// # Errors
151///
152/// Returns a contextual error when the contents cannot be written.
153pub fn write_bytes(path: &Path, contents: impl AsRef<[u8]>) -> Result<()> {
154    std::fs::write(path, contents).map_err(|source| Error::new(Operation::WriteFile, path, source))
155}
156
157/// Replace a UTF-8 text file's contents directly.
158///
159/// # Errors
160///
161/// Returns a contextual error when the contents cannot be written.
162pub fn write_text(path: &Path, contents: &str) -> Result<()> {
163    write_bytes(path, contents)
164}