Skip to main content

pulith_fs/
resource.rs

1use crate::{Error, Result};
2use std::borrow::Cow;
3use std::path::Path;
4
5#[derive(Clone, Copy, Debug, Default)]
6pub struct Options {
7    mmap_threshold: u64,
8}
9
10impl Options {
11    pub fn new() -> Self {
12        Self::default()
13    }
14
15    pub fn with_mmap_threshold(mut self, bytes: u64) -> Self {
16        self.mmap_threshold = bytes;
17        self
18    }
19
20    pub fn mmap_threshold(&self) -> u64 {
21        self.mmap_threshold
22    }
23}
24
25pub struct Resource<'a> {
26    path: Cow<'a, Path>,
27    options: Options,
28    initial_mtime: Option<std::time::SystemTime>,
29}
30
31impl<'a> Resource<'a> {
32    pub fn new(path: impl Into<Cow<'a, Path>>) -> Result<Self> {
33        let path = path.into();
34        let metadata = path
35            .metadata()
36            .map_err(|_| Error::NotFound(path.to_path_buf()))?;
37
38        Ok(Self {
39            path,
40            options: Options::default(),
41            initial_mtime: metadata.modified().ok(),
42        })
43    }
44
45    pub fn with_options(path: impl Into<Cow<'a, Path>>, options: Options) -> Result<Self> {
46        let path = path.into();
47        let metadata = path
48            .metadata()
49            .map_err(|_| Error::NotFound(path.to_path_buf()))?;
50
51        Ok(Self {
52            path,
53            options,
54            initial_mtime: metadata.modified().ok(),
55        })
56    }
57
58    pub fn path(&self) -> &Path {
59        self.path.as_ref()
60    }
61
62    pub fn ensure_integrity(&self) -> Result<()> {
63        let current_mtime = self
64            .path
65            .metadata()
66            .map_err(|e| Error::Read {
67                path: self.path.to_path_buf(),
68                source: e,
69            })?
70            .modified()
71            .ok();
72
73        if current_mtime != self.initial_mtime {
74            return Err(Error::ModifiedExternally(self.path.to_path_buf()));
75        }
76        Ok(())
77    }
78
79    pub fn metadata(&self) -> Result<std::fs::Metadata> {
80        self.path.as_ref().metadata().map_err(|e| Error::Read {
81            path: self.path.to_path_buf(),
82            source: e,
83        })
84    }
85
86    pub fn size(&self) -> Result<u64> {
87        self.metadata().map(|m| m.len())
88    }
89
90    pub fn is_dir(&self) -> bool {
91        self.path.as_ref().is_dir()
92    }
93
94    pub fn is_file(&self) -> bool {
95        self.path.as_ref().is_file()
96    }
97
98    pub fn content(&self) -> Result<Content> {
99        self.ensure_integrity()?;
100        let size = self.size()?;
101
102        if size < self.options.mmap_threshold() {
103            let data = std::fs::read(self.path.as_ref()).map_err(|e| Error::Read {
104                path: self.path.to_path_buf(),
105                source: e,
106            })?;
107            Ok(Content::Small(data))
108        } else {
109            let file = std::fs::File::open(self.path.as_ref()).map_err(|e| Error::Read {
110                path: self.path.to_path_buf(),
111                source: e,
112            })?;
113            let mmap = unsafe {
114                memmap2::MmapOptions::new()
115                    .map(&file)
116                    .map_err(|_| Error::Failed)?
117            };
118            Ok(Content::Mmap(mmap))
119        }
120    }
121
122    pub fn read_all(self) -> Result<Vec<u8>> {
123        self.content()?.to_vec()
124    }
125}
126
127pub enum Content {
128    Small(Vec<u8>),
129    Mmap(memmap2::Mmap),
130}
131
132impl Content {
133    pub fn as_slice(&self) -> &[u8] {
134        match self {
135            Content::Small(data) => data.as_slice(),
136            Content::Mmap(mmap) => mmap.as_ref(),
137        }
138    }
139
140    pub fn to_vec(self) -> Result<Vec<u8>> {
141        match self {
142            Content::Small(data) => Ok(data),
143            Content::Mmap(mmap) => Ok(mmap.to_vec()),
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use tempfile::tempdir;
152
153    #[test]
154    fn test_resource_metadata() {
155        let dir = tempdir().unwrap();
156        let path = dir.path().join("test.txt");
157        std::fs::write(&path, "hello").unwrap();
158        let resource = Resource::new(&path).unwrap();
159        assert_eq!(resource.size().unwrap(), 5);
160        assert!(resource.is_file());
161    }
162
163    #[test]
164    fn test_resource_content() {
165        let dir = tempdir().unwrap();
166        let path = dir.path().join("test.txt");
167        std::fs::write(&path, "hello world").unwrap();
168        let resource = Resource::new(&path).unwrap();
169        let content = resource.content().unwrap();
170        assert_eq!(content.as_slice(), b"hello world");
171    }
172}