1use crate::encoding::{self, ContentEncoding, MAX_INFLATED_BYTES};
40use crate::key::check_prefix;
41use crate::{clip, Bytes, Cond, Error, Key, Meta, Object, ObjectStore, Result, Stat};
42use graph_r::fsio;
43use std::fs::{self, File};
44use std::future::{ready, Ready};
45use std::io::{Read, Seek, SeekFrom};
46use std::ops::Range;
47use std::path::{Path, PathBuf};
48
49use std::sync::Mutex;
50use std::time::{Duration, SystemTime, UNIX_EPOCH};
51
52const LOCK_FILE: &str = ".objstore.lock";
53const PROBE_FILE: &str = ".objstore.probe";
54const ENC_DIR: &str = ".objstore-encoding";
56const MAX_SIDECAR: u64 = 256;
58const READ_ATTEMPTS: u32 = 3;
61
62enum Seen {
64 Plain,
65 Encoded(ContentEncoding),
66 Moved,
68}
69
70#[derive(Debug)]
72pub struct LocalDir {
73 root: PathBuf,
74 canon: PathBuf,
76 precision: u64,
78 last: Mutex<u64>,
81 max_inflated: u64,
83}
84
85impl LocalDir {
86 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
88 let root = root.as_ref().to_path_buf();
89 fs::create_dir_all(&root)?;
90 let canon = root.canonicalize()?;
91 let precision = probe_precision(&root)?;
92 Ok(Self { root, canon, precision, last: Mutex::new(0), max_inflated: MAX_INFLATED_BYTES })
93 }
94
95 #[must_use]
98 pub fn with_max_inflated(mut self, limit: u64) -> Self {
99 self.max_inflated = limit;
100 self
101 }
102
103 #[must_use]
105 pub fn root(&self) -> &Path {
106 &self.root
107 }
108
109 #[must_use]
111 pub fn precision_ns(&self) -> u64 {
112 self.precision
113 }
114
115 fn path(&self, key: Key<'_>) -> PathBuf {
116 let mut p = self.root.clone();
117 p.extend(key.as_str().split('/'));
118 p
119 }
120
121 fn contained(&self, dir: &Path) -> Result<()> {
124 let mut probe = dir;
125 loop {
128 match probe.canonicalize() {
129 Ok(c) if c.starts_with(&self.canon) => return Ok(()),
130 Ok(_) => {
131 return Err(Error::InvalidKey { reason: "resolves outside the store root" })
132 }
133 Err(e) if absent(&e) => match probe.parent() {
134 Some(p) => probe = p,
135 None => {
136 return Err(Error::InvalidKey { reason: "resolves outside the store root" })
137 }
138 },
139 Err(e) => return Err(e.into()),
140 }
141 }
142 }
143
144 fn sidecar(&self, key: Key<'_>) -> PathBuf {
145 let mut p = self.root.join(ENC_DIR);
146 p.extend(key.as_str().split('/'));
147 p
148 }
149
150 fn seen(&self, key: Key<'_>, path: &Path, generation: u64) -> Result<Seen> {
152 match fs::symlink_metadata(self.root.join(ENC_DIR)) {
155 Err(e) if absent(&e) => return Ok(Seen::Plain),
156 Err(e) => return Err(e.into()),
157 Ok(_) => {}
158 }
159 let side = self.sidecar(key);
160 self.contained(side.parent().unwrap_or(&self.root))?;
161 if let Some(token) = sidecar_entry(&side, generation)? {
162 return match ContentEncoding::parse(&token)? {
163 Some(e) => Ok(Seen::Encoded(e)),
164 None => Err(Error::corrupt("encoding sidecar names no encoding")),
165 };
166 }
167 Ok(match fs::symlink_metadata(path) {
170 Ok(md) if md.is_file() && mtime_generation(&md) == generation => Seen::Plain,
171 Ok(_) => Seen::Moved,
172 Err(e) if absent(&e) => Seen::Moved,
173 Err(e) => return Err(e.into()),
174 })
175 }
176
177 fn read_whole(&self, key: Key<'_>) -> Result<Option<(Bytes, u64, Option<ContentEncoding>)>> {
180 let path = self.path(key);
181 self.contained(path.parent().unwrap_or(&self.root))?;
182 for attempt in 1..=READ_ATTEMPTS {
183 let _quiesce = if attempt == READ_ATTEMPTS { Some(self.lock()?) } else { None };
184 let Some(mut f) = open_existing(&path)? else { return Ok(None) };
185 let md = f.metadata()?;
188 let generation = mtime_generation(&md);
189 let mut buf = Vec::with_capacity(usize::try_from(md.len()).unwrap_or(0));
190 f.read_to_end(&mut buf)?;
191 match self.seen(key, &path, generation)? {
192 Seen::Plain => return Ok(Some((Bytes::from(buf), generation, None))),
193 Seen::Encoded(e) => return Ok(Some((Bytes::from(buf), generation, Some(e)))),
194 Seen::Moved => {}
195 }
196 }
197 Err(Error::protocol("an object changed while the store lock was held"))
198 }
199
200 fn stat_sync(&self, key: Key<'_>) -> Result<Option<Stat>> {
201 let path = self.path(key);
202 self.contained(path.parent().unwrap_or(&self.root))?;
203 for attempt in 1..=READ_ATTEMPTS {
204 let _quiesce = if attempt == READ_ATTEMPTS { Some(self.lock()?) } else { None };
205 let md = match fs::symlink_metadata(&path) {
206 Ok(md) if md.is_file() => md,
207 Ok(_) => return Ok(None),
208 Err(e) if absent(&e) => return Ok(None),
209 Err(e) => return Err(e.into()),
210 };
211 let meta = Meta {
212 key: key.as_str().to_owned(),
213 size: md.len(),
214 generation: mtime_generation(&md),
215 };
216 let content_encoding = match self.seen(key, &path, meta.generation)? {
217 Seen::Plain => None,
218 Seen::Encoded(e) => Some(e),
219 Seen::Moved => continue,
220 };
221 return Ok(Some(Stat { meta, content_encoding }));
222 }
223 Err(Error::protocol("an object changed while the store lock was held"))
224 }
225
226 fn list_stat_sync(&self, prefix: &str) -> Result<Vec<Stat>> {
227 let metas = self.list_sync(prefix)?;
228 let mut out = Vec::with_capacity(metas.len());
229 for meta in metas {
230 let key = Key::parse(&meta.key)?;
231 let content_encoding = match self.seen(key, &self.path(key), meta.generation)? {
232 Seen::Plain => None,
233 Seen::Encoded(e) => Some(e),
234 Seen::Moved => {
236 out.extend(self.stat_sync(key)?);
237 continue;
238 }
239 };
240 out.push(Stat { meta, content_encoding });
241 }
242 Ok(out)
243 }
244
245 fn drop_sidecar(&self, key: Key<'_>) -> Result<()> {
247 let enc_root = self.root.join(ENC_DIR);
248 if !enc_root.is_dir() {
249 return Ok(());
250 }
251 let side = self.sidecar(key);
252 self.contained(side.parent().unwrap_or(&self.root))?;
253 match fs::remove_file(&side) {
254 Ok(()) => {}
255 Err(e) if absent(&e) => return Ok(()),
256 Err(e) => return Err(e.into()),
257 }
258 let mut dir = side.parent();
259 while let Some(d) = dir.filter(|d| *d != enc_root) {
260 if fs::remove_dir(d).is_err() {
261 break;
262 }
263 dir = d.parent();
264 }
265 Ok(())
266 }
267
268 fn lock(&self) -> Result<File> {
269 Ok(fsio::lock(&self.root.join(LOCK_FILE))?)
270 }
271
272 fn next_generation(&self, current: Option<u64>) -> u64 {
273 let p = self.precision;
274 let now = SystemTime::now()
275 .duration_since(UNIX_EPOCH)
276 .map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX));
277 let mut last = self.last.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
278 let floor = current.unwrap_or(0).max(*last).saturating_add(p);
279 let g = round_up(now.max(floor), p).max(p);
280 *last = g;
281 g
282 }
283
284 fn put_sync(
285 &self,
286 key: &str,
287 body: &Bytes,
288 cond: Cond,
289 encoding: Option<ContentEncoding>,
290 ) -> Result<u64> {
291 let key = Key::parse(key)?;
292 let path = self.path(key);
293 self.contained(path.parent().unwrap_or(&self.root))?;
294 let _lock = self.lock()?;
295 let current = generation_of(&path)?;
296 match (cond, current) {
297 (Cond::CreateOnly, Some(_)) => return Err(Error::Conflict),
298 (Cond::IfGeneration(want), cur) if cur != Some(want) => {
299 return Err(Error::PreconditionFailed)
300 }
301 _ => {}
302 }
303 let dir = path.parent().unwrap_or(&self.root);
304 self.contained(dir)?;
305 fs::create_dir_all(dir).map_err(|e| match e.kind() {
306 std::io::ErrorKind::NotADirectory | std::io::ErrorKind::AlreadyExists => COLLISION,
307 _ => e.into(),
308 })?;
309 self.contained(dir)?;
310 let side = match encoding {
313 None => None,
314 Some(e) => {
315 let side = self.sidecar(key);
316 let sdir = side.parent().unwrap_or(&self.root);
317 self.contained(sdir)?;
318 fs::create_dir_all(sdir).map_err(|e| match e.kind() {
319 std::io::ErrorKind::NotADirectory | std::io::ErrorKind::AlreadyExists => {
320 COLLISION
321 }
322 _ => e.into(),
323 })?;
324 self.contained(sdir)?;
325 let kept = match current {
326 Some(c) => sidecar_entry(&side, c)?.map(|t| format!("{c} {t}\n")),
327 None => None,
328 };
329 Some((side, e, kept.unwrap_or_default()))
330 }
331 };
332 let generation = self.next_generation(current);
333 let stamp = UNIX_EPOCH + Duration::from_nanos(generation);
336 let mut recorded = generation;
337 fsio::write_atomic_with(&path, &[body], |f| {
338 f.set_modified(stamp)?;
339 if let Some((side, e, kept)) = &side {
340 recorded = mtime_generation(&f.metadata()?);
341 fsio::write_atomic(side, &[format!("{recorded} {e}\n{kept}").as_bytes()])?;
342 }
343 Ok(())
344 })?;
345 let visible = generation_of(&path)?.unwrap_or(generation);
347 if let Some((side, e, kept)) = &side {
348 if visible != recorded {
349 fsio::write_atomic(side, &[format!("{visible} {e}\n{kept}").as_bytes()])?;
351 }
352 }
353 Ok(visible)
354 }
355
356 fn list_sync(&self, prefix: &str) -> Result<Vec<Meta>> {
357 check_prefix(prefix)?;
358 let (dir_part, _) = prefix.rsplit_once('/').unwrap_or(("", prefix));
360 let mut start = self.root.clone();
361 if !dir_part.is_empty() {
362 start.extend(dir_part.split('/'));
363 }
364 let mut out = Vec::new();
365 let mut stack = vec![(start, dir_part.to_owned())];
366 while let Some((dir, rel)) = stack.pop() {
367 let entries = match fs::read_dir(&dir) {
368 Ok(e) => e,
369 Err(e) if absent(&e) => continue,
370 Err(e) => return Err(e.into()),
371 };
372 for entry in entries {
373 let entry = entry?;
374 let Some(name) = entry.file_name().to_str().map(str::to_owned) else { continue };
375 let key = if rel.is_empty() { name.clone() } else { format!("{rel}/{name}") };
376 if Key::parse(&key).is_err() {
377 continue; }
379 let ft = entry.file_type()?; if ft.is_dir() {
381 if prefix.starts_with(&format!("{key}/"))
382 || key.starts_with(prefix)
383 || prefix.is_empty()
384 {
385 stack.push((entry.path(), key));
386 }
387 } else if ft.is_file() && key.starts_with(prefix) {
388 let md = entry.metadata()?;
389 out.push(Meta { key, size: md.len(), generation: mtime_generation(&md) });
390 }
391 }
392 }
393 out.sort();
394 Ok(out)
395 }
396}
397
398impl ObjectStore for LocalDir {
399 type Get<'a> = Ready<Result<Option<Object>>>;
400 type GetRange<'a> = Ready<Result<Bytes>>;
401 type Head<'a> = Ready<Result<Option<Meta>>>;
402 type Put<'a> = Ready<Result<u64>>;
403 type List<'a> = Ready<Result<Vec<Meta>>>;
404 type Delete<'a> = Ready<Result<()>>;
405
406 fn get<'a>(&'a self, key: &'a str) -> Self::Get<'a> {
407 ready((|| {
408 let Some((stored, generation, encoding)) = self.read_whole(Key::parse(key)?)? else {
409 return Ok(None);
410 };
411 let bytes = match encoding {
412 None => stored,
413 Some(e) => encoding::decode(e, &stored, self.max_inflated)?,
414 };
415 Ok(Some(Object { bytes, generation }))
416 })())
417 }
418
419 fn get_range<'a>(&'a self, key: &'a str, range: Range<u64>) -> Self::GetRange<'a> {
420 ready((|| {
421 let k = Key::parse(key)?;
422 let path = self.path(k);
423 self.contained(path.parent().unwrap_or(&self.root))?;
424 for attempt in 1..=READ_ATTEMPTS {
425 let _quiesce = if attempt == READ_ATTEMPTS { Some(self.lock()?) } else { None };
426 let Some(mut f) = open_existing(&path)? else { return Err(Error::NotFound) };
427 let md = f.metadata()?;
428 match self.seen(k, &path, mtime_generation(&md))? {
429 Seen::Encoded(e) => {
430 return Err(Error::EncodedRange { encoding: e.as_str().into() })
431 }
432 Seen::Moved => continue,
433 Seen::Plain => {}
434 }
435 let r = clip(&range, md.len());
436 let mut buf = vec![0; r.len()];
437 f.seek(SeekFrom::Start(r.start as u64))?;
438 f.read_exact(&mut buf)?;
439 return Ok(Bytes::from(buf));
440 }
441 Err(Error::protocol("an object changed while the store lock was held"))
442 })())
443 }
444
445 fn head<'a>(&'a self, key: &'a str) -> Self::Head<'a> {
446 ready((|| {
447 let k = Key::parse(key)?;
448 let path = self.path(k);
449 self.contained(path.parent().unwrap_or(&self.root))?;
450 match fs::symlink_metadata(&path) {
451 Ok(md) if md.is_file() => Ok(Some(Meta {
452 key: k.as_str().to_owned(),
453 size: md.len(),
454 generation: mtime_generation(&md),
455 })),
456 Ok(_) => Ok(None),
457 Err(e) if absent(&e) => Ok(None),
458 Err(e) => Err(e.into()),
459 }
460 })())
461 }
462
463 fn put<'a>(&'a self, key: &'a str, body: Bytes, cond: Cond) -> Self::Put<'a> {
464 ready(self.put_sync(key, &body, cond, None))
465 }
466
467 fn list<'a>(&'a self, prefix: &'a str) -> Self::List<'a> {
468 ready(self.list_sync(prefix))
469 }
470
471 fn delete<'a>(&'a self, key: &'a str) -> Self::Delete<'a> {
472 ready((|| {
473 let path = self.path(Key::parse(key)?);
474 self.contained(path.parent().unwrap_or(&self.root))?;
475 let _lock = self.lock()?;
476 match fs::symlink_metadata(&path) {
478 Ok(md) if md.is_file() => {}
479 Ok(_) => return Ok(()),
480 Err(e) if absent(&e) => return Ok(()),
481 Err(e) => return Err(e.into()),
482 }
483 match fs::remove_file(&path) {
484 Ok(()) => {}
485 Err(e) if absent(&e) => return Ok(()),
486 Err(e) => return Err(e.into()),
487 }
488 self.drop_sidecar(Key::parse(key)?)?;
490 let mut dir = path.parent();
493 while let Some(d) = dir.filter(|d| *d != self.root) {
494 if fs::remove_dir(d).is_err() {
495 break; }
497 dir = d.parent();
498 }
499 fsio::sync_dir(dir.unwrap_or(&self.root));
500 Ok(())
501 })())
502 }
503
504 fn put_encoded<'a>(
505 &'a self,
506 key: &'a str,
507 body: Bytes,
508 cond: Cond,
509 encoding: ContentEncoding,
510 ) -> impl std::future::Future<Output = Result<u64>> + 'a {
511 ready(Key::parse(key).and_then(|_| {
513 let stored = encoding::encode(encoding, &body)?;
514 self.put_sync(key, &stored, cond, Some(encoding))
515 }))
516 }
517
518 fn stat<'a>(
519 &'a self,
520 key: &'a str,
521 ) -> impl std::future::Future<Output = Result<Option<Stat>>> + 'a {
522 ready(Key::parse(key).and_then(|k| self.stat_sync(k)))
523 }
524
525 fn list_stat<'a>(
526 &'a self,
527 prefix: &'a str,
528 ) -> impl std::future::Future<Output = Result<Vec<Stat>>> + 'a {
529 ready(self.list_stat_sync(prefix))
530 }
531}
532
533fn sidecar_entry(side: &Path, generation: u64) -> Result<Option<String>> {
536 let Some(f) = open_existing(side)? else { return Ok(None) };
539 let mut text = Vec::with_capacity(64);
540 f.take(MAX_SIDECAR + 1).read_to_end(&mut text)?;
541 if text.len() as u64 > MAX_SIDECAR {
542 return Err(Error::corrupt("encoding sidecar too long"));
543 }
544 let text =
545 std::str::from_utf8(&text).map_err(|_| Error::corrupt("encoding sidecar is not text"))?;
546 for line in text.lines() {
547 let (g, token) =
548 line.split_once(' ').ok_or_else(|| Error::corrupt("malformed encoding sidecar"))?;
549 let g: u64 = g.parse().map_err(|_| Error::corrupt("malformed encoding sidecar"))?;
550 if g == generation {
551 return Ok(Some(token.to_owned()));
552 }
553 }
554 Ok(None)
555}
556
557const COLLISION: Error = Error::InvalidKey {
559 reason: "an object and a prefix collide (LocalDir cannot hold both `k` and `k/...`)",
560};
561
562fn absent(e: &std::io::Error) -> bool {
564 matches!(e.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory)
565}
566
567fn open_existing(path: &Path) -> Result<Option<File>> {
568 match fs::symlink_metadata(path) {
570 Ok(md) if !md.file_type().is_file() => return Ok(None),
571 Ok(_) => {}
572 Err(e) if absent(&e) => return Ok(None),
573 Err(e) => return Err(e.into()),
574 }
575 match File::open(path) {
576 Ok(f) if f.metadata()?.is_file() => Ok(Some(f)),
577 Ok(_) => Ok(None),
578 Err(e) if absent(&e) => Ok(None),
579 Err(e) => Err(e.into()),
580 }
581}
582
583fn generation_of(path: &Path) -> Result<Option<u64>> {
584 match fs::symlink_metadata(path) {
585 Ok(md) if md.is_file() => Ok(Some(mtime_generation(&md))),
586 Ok(md) if md.is_dir() => Err(COLLISION),
587 Ok(_) => Err(Error::Protocol {
588 message: "a non-file entry exists where an object key is".into(),
589 }),
590 Err(e) if absent(&e) => Ok(None),
591 Err(e) => Err(e.into()),
592 }
593}
594
595fn mtime_generation(md: &fs::Metadata) -> u64 {
596 md.modified()
597 .ok()
598 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
599 .map_or(1, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX).max(1))
600}
601
602fn round_up(v: u64, step: u64) -> u64 {
603 v.div_ceil(step).saturating_mul(step)
604}
605
606fn probe_precision(root: &Path) -> Result<u64> {
609 const PROBE_NS: u64 = 1_700_000_000_123_456_789;
610 let path = root.join(PROBE_FILE);
611 let f = File::create(&path)?;
612 f.set_modified(UNIX_EPOCH + Duration::from_nanos(PROBE_NS))?;
613 drop(f);
614 let got = mtime_generation(&fs::metadata(&path)?);
615 let _ = fs::remove_file(&path);
616 for step in [
617 1,
618 10,
619 100,
620 1_000,
621 10_000,
622 100_000,
623 1_000_000,
624 10_000_000,
625 100_000_000,
626 1_000_000_000,
627 2_000_000_000,
628 ] {
629 if got == PROBE_NS / step * step {
630 return Ok(step);
631 }
632 }
633 Ok(2_000_000_000)
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639 use futures::executor::block_on;
640
641 #[test]
642 fn generations_increase_per_key_even_when_written_fast() {
643 let dir = tempfile::tempdir().unwrap();
644 let s = LocalDir::open(dir.path()).unwrap();
645 let mut last = 0;
646 for i in 0..50u8 {
647 let g = block_on(s.put("k", Bytes::from(vec![i]), Cond::None)).unwrap();
648 assert!(g > last, "generation {g} after {last}");
649 last = g;
650 }
651 assert_eq!(block_on(s.head("k")).unwrap().unwrap().generation, last);
652 }
653
654 #[test]
655 fn no_temporaries_left_and_bytes_are_exact() {
656 let dir = tempfile::tempdir().unwrap();
657 let s = LocalDir::open(dir.path()).unwrap();
658 block_on(s.put("a/b/c.bin", Bytes::from_static(b"\x00\x01payload"), Cond::CreateOnly))
659 .unwrap();
660 assert_eq!(fs::read(dir.path().join("a/b/c.bin")).unwrap(), b"\x00\x01payload");
661 let names: Vec<_> =
662 fs::read_dir(dir.path().join("a/b")).unwrap().map(|e| e.unwrap().file_name()).collect();
663 assert_eq!(names, ["c.bin"]);
664 }
665}