hermes_core/directories/
mmap.rs1use std::io;
6use std::ops::Range;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use memmap2::Mmap;
12
13use super::{
14 Directory, DirectoryWriter, FileHandle, FileStreamingWriter, OwnedBytes, StreamingWriter,
15};
16
17pub struct MmapDirectory {
31 root: PathBuf,
32 label: super::IndexLabel,
33}
34
35impl MmapDirectory {
36 pub fn new(root: impl AsRef<Path>) -> Self {
38 Self {
39 root: root.as_ref().to_path_buf(),
40 label: super::IndexLabel::default(),
41 }
42 }
43
44 pub fn root(&self) -> &Path {
46 &self.root
47 }
48
49 fn resolve(&self, path: &Path) -> PathBuf {
50 self.root.join(path)
51 }
52
53 fn mmap_file(&self, path: &Path) -> io::Result<Arc<Mmap>> {
55 let full_path = self.resolve(path);
56 let file = std::fs::File::open(&full_path)?;
57 let mmap = unsafe { Mmap::map(&file)? };
58 Ok(Arc::new(mmap))
59 }
60}
61
62impl Clone for MmapDirectory {
63 fn clone(&self) -> Self {
64 Self {
65 root: self.root.clone(),
66 label: self.label.clone(),
68 }
69 }
70}
71
72#[async_trait]
73impl Directory for MmapDirectory {
74 async fn exists(&self, path: &Path) -> io::Result<bool> {
75 let full_path = self.resolve(path);
76 tokio::fs::try_exists(&full_path).await
82 }
83
84 async fn file_size(&self, path: &Path) -> io::Result<u64> {
85 let full_path = self.resolve(path);
86 let metadata = tokio::fs::metadata(&full_path).await?;
87 Ok(metadata.len())
88 }
89
90 async fn open_read(&self, path: &Path) -> io::Result<FileHandle> {
91 let mmap = self.mmap_file(path)?;
92 Ok(FileHandle::from_bytes(OwnedBytes::from_mmap(mmap)))
94 }
95
96 async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes> {
97 let mmap = self.mmap_file(path)?;
98 let start = range.start as usize;
99 let end = range.end as usize;
100
101 if end > mmap.len() {
102 return Err(io::Error::new(
103 io::ErrorKind::InvalidInput,
104 format!("Range {}..{} exceeds file size {}", start, end, mmap.len()),
105 ));
106 }
107
108 Ok(OwnedBytes::from_mmap_range(mmap, start..end))
110 }
111
112 async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>> {
113 let full_path = self.resolve(prefix);
114 let mut entries = tokio::fs::read_dir(&full_path).await?;
115 let mut files = Vec::new();
116
117 while let Some(entry) = entries.next_entry().await? {
118 if entry.file_type().await?.is_file() {
119 files.push(entry.path().strip_prefix(&self.root).unwrap().to_path_buf());
120 }
121 }
122
123 Ok(files)
124 }
125
126 async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle> {
127 self.open_read(path).await
130 }
131
132 fn set_index_label(&self, label: &str) {
133 self.label.set(label);
134 }
135
136 fn local_path(&self, path: &Path) -> Option<PathBuf> {
137 Some(self.resolve(path))
138 }
139}
140
141#[async_trait]
142impl DirectoryWriter for MmapDirectory {
143 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
144 let full_path = self.resolve(path);
145
146 if let Some(parent) = full_path.parent() {
148 tokio::fs::create_dir_all(parent).await?;
149 }
150
151 tokio::fs::write(&full_path, data).await
152 }
153
154 async fn delete(&self, path: &Path) -> io::Result<()> {
155 let full_path = self.resolve(path);
156 tokio::fs::remove_file(&full_path).await
157 }
158
159 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
160 let from_path = self.resolve(from);
161 let to_path = self.resolve(to);
162 std::fs::rename(&from_path, &to_path)
166 }
167
168 async fn link(&self, from: &Path, to: &Path) -> io::Result<()> {
169 std::fs::hard_link(self.resolve(from), self.resolve(to))
170 }
171
172 async fn sync(&self) -> io::Result<()> {
173 let dir = std::fs::File::open(&self.root)?;
175 dir.sync_all()?;
176 Ok(())
177 }
178
179 async fn streaming_writer(&self, path: &Path) -> io::Result<Box<dyn StreamingWriter>> {
180 let full_path = self.resolve(path);
181 if let Some(parent) = full_path.parent() {
182 tokio::fs::create_dir_all(parent).await?;
183 }
184 let file = std::fs::File::create(&full_path)?;
185 Ok(Box::new(FileStreamingWriter::new(file)))
186 }
187
188 async fn streaming_writer_cold(&self, path: &Path) -> io::Result<Box<dyn StreamingWriter>> {
189 let full_path = self.resolve(path);
190 if let Some(parent) = full_path.parent() {
191 tokio::fs::create_dir_all(parent).await?;
192 }
193 let file = std::fs::File::create(&full_path)?;
194 Ok(Box::new(super::ColdStreamingWriter::new(
195 file,
196 self.label.get(),
197 )))
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use tempfile::TempDir;
205
206 #[tokio::test]
207 async fn test_mmap_directory_basic() {
208 let temp_dir = TempDir::new().unwrap();
209 let dir = MmapDirectory::new(temp_dir.path());
210
211 let test_data = b"Hello, mmap world!";
213 dir.write(Path::new("test.txt"), test_data).await.unwrap();
214
215 assert!(dir.exists(Path::new("test.txt")).await.unwrap());
217 assert!(!dir.exists(Path::new("nonexistent.txt")).await.unwrap());
218
219 assert_eq!(
221 dir.file_size(Path::new("test.txt")).await.unwrap(),
222 test_data.len() as u64
223 );
224
225 let slice = dir.open_read(Path::new("test.txt")).await.unwrap();
227 let bytes = slice.read_bytes().await.unwrap();
228 assert_eq!(bytes.as_slice(), test_data);
229
230 let range_bytes = dir.read_range(Path::new("test.txt"), 7..12).await.unwrap();
232 assert_eq!(range_bytes.as_slice(), b"mmap ");
233 }
234
235 #[cfg(unix)]
240 #[tokio::test]
241 async fn test_mmap_exists_propagates_stat_errors_instead_of_reporting_missing() {
242 use std::os::unix::fs::PermissionsExt;
243
244 let temp_dir = TempDir::new().unwrap();
245 let dir = MmapDirectory::new(temp_dir.path());
246 dir.write(Path::new("locked/seg.meta"), b"data")
247 .await
248 .unwrap();
249
250 let locked = temp_dir.path().join("locked");
253 let original = std::fs::metadata(&locked).unwrap().permissions();
254 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
255 if std::fs::metadata(locked.join("seg.meta")).is_ok() {
256 std::fs::set_permissions(&locked, original).unwrap();
259 return;
260 }
261 let result = dir.exists(Path::new("locked/seg.meta")).await;
262 std::fs::set_permissions(&locked, original).unwrap();
263
264 let error =
265 result.expect_err("stat failure must propagate as Err, not be misreported as missing");
266 assert_ne!(error.kind(), io::ErrorKind::NotFound);
267 assert!(dir.exists(Path::new("locked/seg.meta")).await.unwrap());
269 }
270
271 #[tokio::test]
272 async fn test_mmap_directory_lazy_handle() {
273 let temp_dir = TempDir::new().unwrap();
274 let dir = MmapDirectory::new(temp_dir.path());
275
276 let data: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
278 dir.write(Path::new("large.bin"), &data).await.unwrap();
279
280 let handle = dir.open_lazy(Path::new("large.bin")).await.unwrap();
282 assert_eq!(handle.len(), 1000);
283 assert!(handle.is_sync());
284
285 let range1 = handle.read_bytes_range(0..100).await.unwrap();
287 assert_eq!(range1.len(), 100);
288 assert_eq!(range1.as_slice(), &data[0..100]);
289
290 let range2 = handle.read_bytes_range_sync(500..600).unwrap();
292 assert_eq!(range2.as_slice(), &data[500..600]);
293 }
294}