1use crate::key::check_prefix;
29use crate::{clip, Bytes, Cond, Error, Key, Meta, Object, ObjectStore, Result};
30use graph_r::fsio;
31use std::fs::{self, File};
32use std::future::{ready, Ready};
33use std::io::{Read, Seek, SeekFrom};
34use std::ops::Range;
35use std::path::{Path, PathBuf};
36
37use std::sync::Mutex;
38use std::time::{Duration, SystemTime, UNIX_EPOCH};
39
40const LOCK_FILE: &str = ".objstore.lock";
41const PROBE_FILE: &str = ".objstore.probe";
42
43#[derive(Debug)]
45pub struct LocalDir {
46 root: PathBuf,
47 canon: PathBuf,
49 precision: u64,
51 last: Mutex<u64>,
54}
55
56impl LocalDir {
57 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
59 let root = root.as_ref().to_path_buf();
60 fs::create_dir_all(&root)?;
61 let canon = root.canonicalize()?;
62 let precision = probe_precision(&root)?;
63 Ok(Self { root, canon, precision, last: Mutex::new(0) })
64 }
65
66 #[must_use]
68 pub fn root(&self) -> &Path {
69 &self.root
70 }
71
72 #[must_use]
74 pub fn precision_ns(&self) -> u64 {
75 self.precision
76 }
77
78 fn path(&self, key: Key<'_>) -> PathBuf {
79 let mut p = self.root.clone();
80 p.extend(key.as_str().split('/'));
81 p
82 }
83
84 fn contained(&self, dir: &Path) -> Result<()> {
87 let mut probe = dir;
88 loop {
91 match probe.canonicalize() {
92 Ok(c) if c.starts_with(&self.canon) => return Ok(()),
93 Ok(_) => {
94 return Err(Error::InvalidKey { reason: "resolves outside the store root" })
95 }
96 Err(e) if absent(&e) => match probe.parent() {
97 Some(p) => probe = p,
98 None => {
99 return Err(Error::InvalidKey { reason: "resolves outside the store root" })
100 }
101 },
102 Err(e) => return Err(e.into()),
103 }
104 }
105 }
106
107 fn lock(&self) -> Result<File> {
108 Ok(fsio::lock(&self.root.join(LOCK_FILE))?)
109 }
110
111 fn next_generation(&self, current: Option<u64>) -> u64 {
112 let p = self.precision;
113 let now = SystemTime::now()
114 .duration_since(UNIX_EPOCH)
115 .map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX));
116 let mut last = self.last.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
117 let floor = current.unwrap_or(0).max(*last).saturating_add(p);
118 let g = round_up(now.max(floor), p).max(p);
119 *last = g;
120 g
121 }
122
123 fn put_sync(&self, key: &str, body: &Bytes, cond: Cond) -> Result<u64> {
124 let key = Key::parse(key)?;
125 let path = self.path(key);
126 self.contained(path.parent().unwrap_or(&self.root))?;
127 let _lock = self.lock()?;
128 let current = generation_of(&path)?;
129 match (cond, current) {
130 (Cond::CreateOnly, Some(_)) => return Err(Error::Conflict),
131 (Cond::IfGeneration(want), cur) if cur != Some(want) => {
132 return Err(Error::PreconditionFailed)
133 }
134 _ => {}
135 }
136 let dir = path.parent().unwrap_or(&self.root);
137 self.contained(dir)?;
138 fs::create_dir_all(dir).map_err(|e| match e.kind() {
139 std::io::ErrorKind::NotADirectory | std::io::ErrorKind::AlreadyExists => COLLISION,
140 _ => e.into(),
141 })?;
142 self.contained(dir)?;
143 let generation = self.next_generation(current);
144 let stamp = UNIX_EPOCH + Duration::from_nanos(generation);
147 fsio::write_atomic_with(&path, &[body], |f| f.set_modified(stamp))?;
148 Ok(generation_of(&path)?.unwrap_or(generation))
150 }
151
152 fn list_sync(&self, prefix: &str) -> Result<Vec<Meta>> {
153 check_prefix(prefix)?;
154 let (dir_part, _) = prefix.rsplit_once('/').unwrap_or(("", prefix));
156 let mut start = self.root.clone();
157 if !dir_part.is_empty() {
158 start.extend(dir_part.split('/'));
159 }
160 let mut out = Vec::new();
161 let mut stack = vec![(start, dir_part.to_owned())];
162 while let Some((dir, rel)) = stack.pop() {
163 let entries = match fs::read_dir(&dir) {
164 Ok(e) => e,
165 Err(e) if absent(&e) => continue,
166 Err(e) => return Err(e.into()),
167 };
168 for entry in entries {
169 let entry = entry?;
170 let Some(name) = entry.file_name().to_str().map(str::to_owned) else { continue };
171 let key = if rel.is_empty() { name.clone() } else { format!("{rel}/{name}") };
172 if Key::parse(&key).is_err() {
173 continue; }
175 let ft = entry.file_type()?; if ft.is_dir() {
177 if prefix.starts_with(&format!("{key}/"))
178 || key.starts_with(prefix)
179 || prefix.is_empty()
180 {
181 stack.push((entry.path(), key));
182 }
183 } else if ft.is_file() && key.starts_with(prefix) {
184 let md = entry.metadata()?;
185 out.push(Meta { key, size: md.len(), generation: mtime_generation(&md) });
186 }
187 }
188 }
189 out.sort();
190 Ok(out)
191 }
192}
193
194impl ObjectStore for LocalDir {
195 type Get<'a> = Ready<Result<Option<Object>>>;
196 type GetRange<'a> = Ready<Result<Bytes>>;
197 type Head<'a> = Ready<Result<Option<Meta>>>;
198 type Put<'a> = Ready<Result<u64>>;
199 type List<'a> = Ready<Result<Vec<Meta>>>;
200 type Delete<'a> = Ready<Result<()>>;
201
202 fn get<'a>(&'a self, key: &'a str) -> Self::Get<'a> {
203 ready((|| {
204 let path = self.path(Key::parse(key)?);
205 self.contained(path.parent().unwrap_or(&self.root))?;
206 let Some(mut f) = open_existing(&path)? else { return Ok(None) };
207 let md = f.metadata()?;
210 let mut buf = Vec::with_capacity(usize::try_from(md.len()).unwrap_or(0));
211 f.read_to_end(&mut buf)?;
212 Ok(Some(Object { bytes: Bytes::from(buf), generation: mtime_generation(&md) }))
213 })())
214 }
215
216 fn get_range<'a>(&'a self, key: &'a str, range: Range<u64>) -> Self::GetRange<'a> {
217 ready((|| {
218 let path = self.path(Key::parse(key)?);
219 self.contained(path.parent().unwrap_or(&self.root))?;
220 let Some(mut f) = open_existing(&path)? else { return Err(Error::NotFound) };
221 let r = clip(&range, f.metadata()?.len());
222 let mut buf = vec![0; r.len()];
223 f.seek(SeekFrom::Start(r.start as u64))?;
224 f.read_exact(&mut buf)?;
225 Ok(Bytes::from(buf))
226 })())
227 }
228
229 fn head<'a>(&'a self, key: &'a str) -> Self::Head<'a> {
230 ready((|| {
231 let k = Key::parse(key)?;
232 let path = self.path(k);
233 self.contained(path.parent().unwrap_or(&self.root))?;
234 match fs::symlink_metadata(&path) {
235 Ok(md) if md.is_file() => Ok(Some(Meta {
236 key: k.as_str().to_owned(),
237 size: md.len(),
238 generation: mtime_generation(&md),
239 })),
240 Ok(_) => Ok(None),
241 Err(e) if absent(&e) => Ok(None),
242 Err(e) => Err(e.into()),
243 }
244 })())
245 }
246
247 fn put<'a>(&'a self, key: &'a str, body: Bytes, cond: Cond) -> Self::Put<'a> {
248 ready(self.put_sync(key, &body, cond))
249 }
250
251 fn list<'a>(&'a self, prefix: &'a str) -> Self::List<'a> {
252 ready(self.list_sync(prefix))
253 }
254
255 fn delete<'a>(&'a self, key: &'a str) -> Self::Delete<'a> {
256 ready((|| {
257 let path = self.path(Key::parse(key)?);
258 self.contained(path.parent().unwrap_or(&self.root))?;
259 let _lock = self.lock()?;
260 match fs::symlink_metadata(&path) {
262 Ok(md) if md.is_file() => {}
263 Ok(_) => return Ok(()),
264 Err(e) if absent(&e) => return Ok(()),
265 Err(e) => return Err(e.into()),
266 }
267 match fs::remove_file(&path) {
268 Ok(()) => {}
269 Err(e) if absent(&e) => return Ok(()),
270 Err(e) => return Err(e.into()),
271 }
272 let mut dir = path.parent();
275 while let Some(d) = dir.filter(|d| *d != self.root) {
276 if fs::remove_dir(d).is_err() {
277 break; }
279 dir = d.parent();
280 }
281 fsio::sync_dir(dir.unwrap_or(&self.root));
282 Ok(())
283 })())
284 }
285}
286
287const COLLISION: Error = Error::InvalidKey {
289 reason: "an object and a prefix collide (LocalDir cannot hold both `k` and `k/...`)",
290};
291
292fn absent(e: &std::io::Error) -> bool {
294 matches!(e.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory)
295}
296
297fn open_existing(path: &Path) -> Result<Option<File>> {
298 match fs::symlink_metadata(path) {
300 Ok(md) if !md.file_type().is_file() => return Ok(None),
301 Ok(_) => {}
302 Err(e) if absent(&e) => return Ok(None),
303 Err(e) => return Err(e.into()),
304 }
305 match File::open(path) {
306 Ok(f) if f.metadata()?.is_file() => Ok(Some(f)),
307 Ok(_) => Ok(None),
308 Err(e) if absent(&e) => Ok(None),
309 Err(e) => Err(e.into()),
310 }
311}
312
313fn generation_of(path: &Path) -> Result<Option<u64>> {
314 match fs::symlink_metadata(path) {
315 Ok(md) if md.is_file() => Ok(Some(mtime_generation(&md))),
316 Ok(md) if md.is_dir() => Err(COLLISION),
317 Ok(_) => Err(Error::Protocol {
318 message: "a non-file entry exists where an object key is".into(),
319 }),
320 Err(e) if absent(&e) => Ok(None),
321 Err(e) => Err(e.into()),
322 }
323}
324
325fn mtime_generation(md: &fs::Metadata) -> u64 {
326 md.modified()
327 .ok()
328 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
329 .map_or(1, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX).max(1))
330}
331
332fn round_up(v: u64, step: u64) -> u64 {
333 v.div_ceil(step).saturating_mul(step)
334}
335
336fn probe_precision(root: &Path) -> Result<u64> {
339 const PROBE_NS: u64 = 1_700_000_000_123_456_789;
340 let path = root.join(PROBE_FILE);
341 let f = File::create(&path)?;
342 f.set_modified(UNIX_EPOCH + Duration::from_nanos(PROBE_NS))?;
343 drop(f);
344 let got = mtime_generation(&fs::metadata(&path)?);
345 let _ = fs::remove_file(&path);
346 for step in [
347 1,
348 10,
349 100,
350 1_000,
351 10_000,
352 100_000,
353 1_000_000,
354 10_000_000,
355 100_000_000,
356 1_000_000_000,
357 2_000_000_000,
358 ] {
359 if got == PROBE_NS / step * step {
360 return Ok(step);
361 }
362 }
363 Ok(2_000_000_000)
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use futures::executor::block_on;
370
371 #[test]
372 fn generations_increase_per_key_even_when_written_fast() {
373 let dir = tempfile::tempdir().unwrap();
374 let s = LocalDir::open(dir.path()).unwrap();
375 let mut last = 0;
376 for i in 0..50u8 {
377 let g = block_on(s.put("k", Bytes::from(vec![i]), Cond::None)).unwrap();
378 assert!(g > last, "generation {g} after {last}");
379 last = g;
380 }
381 assert_eq!(block_on(s.head("k")).unwrap().unwrap().generation, last);
382 }
383
384 #[test]
385 fn no_temporaries_left_and_bytes_are_exact() {
386 let dir = tempfile::tempdir().unwrap();
387 let s = LocalDir::open(dir.path()).unwrap();
388 block_on(s.put("a/b/c.bin", Bytes::from_static(b"\x00\x01payload"), Cond::CreateOnly))
389 .unwrap();
390 assert_eq!(fs::read(dir.path().join("a/b/c.bin")).unwrap(), b"\x00\x01payload");
391 let names: Vec<_> =
392 fs::read_dir(dir.path().join("a/b")).unwrap().map(|e| e.unwrap().file_name()).collect();
393 assert_eq!(names, ["c.bin"]);
394 }
395}