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}