dspk_view/pyramid/
storage.rs1use std::fs::{self, OpenOptions};
9use std::mem::ManuallyDrop;
10use std::path::{Path, PathBuf};
11use std::time::UNIX_EPOCH;
12
13use dspk_core::{DspError, DspResult, RecordingSource};
14use memmap2::{MmapMut, MmapRaw};
15
16const MAGIC: &[u8; 8] = b"DKMINMAX";
17const VERSION: u32 = 1;
18const COMPLETE_AT: usize = 12;
20pub const HEADER_BYTES: usize = 4096;
22const MAX_ID_BYTES: usize = 1024;
23
24#[derive(Debug, Clone, PartialEq)]
27pub struct PyramidIdentity {
28 pub source_id: String,
30 pub len: u64,
32 pub mtime_ns: u64,
34 pub channels: usize,
35 pub samples: u64,
36 pub sample_rate_hz: f64,
37}
38
39impl PyramidIdentity {
40 pub fn of(path: &Path, source_id: &str, source: &dyn RecordingSource) -> DspResult<Self> {
43 let stamp = if path.is_dir() && path.join("zarr.json").exists() { path.join("zarr.json") } else { path.to_path_buf() };
44 let meta = fs::metadata(&stamp).map_err(|e| DspError::Io(format!("{}: {e}", stamp.display())))?;
45 let mtime_ns = meta.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok()).map_or(0, |d| d.as_nanos() as u64);
46 let info = source.info();
47 Ok(Self {
48 source_id: source_id.to_string(),
49 len: if path.is_dir() { 0 } else { meta.len() },
50 mtime_ns,
51 channels: info.channel_count(),
52 samples: info.samples,
53 sample_rate_hz: info.sample_rate_hz(),
54 })
55 }
56
57 fn header(&self, base: u64) -> Vec<u8> {
59 let mut h = Vec::with_capacity(HEADER_BYTES);
60 h.extend_from_slice(MAGIC);
61 h.extend_from_slice(&VERSION.to_le_bytes());
62 h.extend_from_slice(&0u32.to_le_bytes());
63 for v in [self.channels as u64, self.samples, base, self.len, self.mtime_ns, self.sample_rate_hz.to_bits()] {
64 h.extend_from_slice(&v.to_le_bytes());
65 }
66 h.extend_from_slice(&(self.source_id.len() as u32).to_le_bytes());
67 h.extend_from_slice(self.source_id.as_bytes());
68 h.resize(HEADER_BYTES, 0);
69 h
70 }
71}
72
73pub fn pyramid_path(path: &Path, source_id: &str) -> PathBuf {
75 let dir = path.parent().unwrap_or(Path::new("."));
76 let name = path.file_name().map_or_else(|| "recording".into(), |n| n.to_string_lossy().into_owned());
77 let id: String = source_id.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect();
78 let id = if id.is_empty() { "main".to_string() } else { id };
79 dir.join("cache").join(name).join(format!("{id}.minmax"))
80}
81
82pub(crate) struct Storage {
83 map: ManuallyDrop<MmapRaw>,
84 path: Option<PathBuf>,
86}
87
88impl Storage {
89 pub fn memory(bytes: usize) -> DspResult<Self> {
91 let map = MmapMut::map_anon(bytes.max(1))?;
92 Ok(Self { map: ManuallyDrop::new(MmapRaw::from(map)), path: None })
93 }
94
95 pub fn open_file(path: &Path, identity: &PyramidIdentity, base: u64, bytes: usize) -> DspResult<Option<Self>> {
99 let Ok(file) = OpenOptions::new().read(true).write(true).open(path) else { return Ok(None) };
100 if file.metadata().map(|m| m.len()).unwrap_or(0) != bytes as u64 {
101 return Ok(None);
102 }
103 let map = MmapRaw::map_raw(&file)?;
104 let header = unsafe { std::slice::from_raw_parts(map.as_ptr(), HEADER_BYTES) };
106 let expected = identity.header(base);
107 let complete = u32::from_le_bytes(header[COMPLETE_AT..COMPLETE_AT + 4].try_into().expect("4 bytes")) == 1;
108 if header[..COMPLETE_AT] != expected[..COMPLETE_AT] || !complete || header[COMPLETE_AT + 4..] != expected[COMPLETE_AT + 4..] {
109 return Ok(None);
110 }
111 Ok(Some(Self { map: ManuallyDrop::new(map), path: Some(path.to_path_buf()) }))
112 }
113
114 pub fn create_file(path: &Path, identity: &PyramidIdentity, base: u64, bytes: usize) -> DspResult<Self> {
116 if identity.source_id.len() > MAX_ID_BYTES {
117 return Err(DspError::InvalidConfig(format!("source id longer than {MAX_ID_BYTES} bytes")));
118 }
119 if let Some(dir) = path.parent() {
120 fs::create_dir_all(dir).map_err(|e| DspError::Io(format!("{}: {e}", dir.display())))?;
121 }
122 let file = OpenOptions::new().read(true).write(true).create(true).truncate(true).open(path)
123 .map_err(|e| DspError::Io(format!("{}: {e}", path.display())))?;
124 file.set_len(bytes as u64)?;
125 let map = MmapRaw::map_raw(&file)?;
126 let header = identity.header(base);
127 unsafe { std::ptr::copy_nonoverlapping(header.as_ptr(), map.as_mut_ptr(), HEADER_BYTES) };
129 Ok(Self { map: ManuallyDrop::new(map), path: Some(path.to_path_buf()) })
130 }
131
132 pub fn path(&self) -> Option<&Path> {
133 self.path.as_deref()
134 }
135
136 pub fn ptr(&self) -> *mut u8 {
137 self.map.as_mut_ptr()
138 }
139
140 pub fn mark_complete(&self) -> DspResult<()> {
142 if self.path.is_none() {
143 return Ok(());
144 }
145 self.map.flush()?;
146 unsafe { self.map.as_mut_ptr().add(COMPLETE_AT).cast::<[u8; 4]>().write_unaligned(1u32.to_le_bytes()) };
148 self.map.flush_range(0, HEADER_BYTES)?;
149 Ok(())
150 }
151}
152
153impl Drop for Storage {
154 fn drop(&mut self) {
155 unsafe { ManuallyDrop::drop(&mut self.map) };
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn pyramid_path_sits_next_to_the_recording() {
166 assert_eq!(pyramid_path(Path::new("/data/a.ap.bin"), "ElectricalSeries/x"), PathBuf::from("/data/cache/a.ap.bin/ElectricalSeries_x.minmax"));
167 }
168}