Skip to main content

somatize_store/
local.rs

1//! A store that is a directory. The one that works today, with no dependencies
2//! beyond hashing and a text format, because a shared folder is what there
3//! already is. [`Bucket`](crate::Bucket) is the other one and lays its bytes out
4//! exactly like this, so one can be copied onto the other.
5//!
6//! ```text
7//! <root>/blobs/ab/sha256_abc…    the bytes, named by their content
8//! <root>/names/de/sha256_def…    one JSON record per name
9//! <root>/tmp/…                   where a write lands before its rename
10//! ```
11//!
12//! The two directory characters come from the **hash** and not the front of the
13//! digest, which is the same in all of them. A record's file is named by the
14//! digest **of the name**: no filesystem takes every string a caller can invent,
15//! and the name itself is inside the record, so `grep` still finds it.
16
17use crate::store::{read_record, record};
18use crate::{Bound, Digest, Meta, Store, StoreError};
19use std::fs;
20use std::io;
21use std::path::{Path, PathBuf};
22use std::sync::atomic::{AtomicU64, Ordering};
23
24/// So no two writes of this process pick the same landing spot. A counter and
25/// not a clock: the pid separates processes, but two threads can read the same
26/// nanosecond, and then one of them lands on the other's bytes.
27static LANDINGS: AtomicU64 = AtomicU64::new(0);
28
29/// A store kept in a directory.
30pub struct Local {
31    root: PathBuf,
32}
33
34impl Local {
35    /// The store in this directory, creating it if it is not there.
36    pub fn at(root: impl Into<PathBuf>) -> Result<Self, StoreError> {
37        let root = root.into();
38        for each in ["blobs", "names", "tmp"] {
39            fs::create_dir_all(root.join(each)).map_err(io_error)?;
40        }
41        Ok(Self { root })
42    }
43
44    fn blob(&self, digest: &Digest) -> PathBuf {
45        let (head, rest) = digest.path();
46        self.root.join("blobs").join(head).join(rest)
47    }
48
49    fn record(&self, name: &str) -> PathBuf {
50        let (head, rest) = Digest::of(name.as_bytes()).path();
51        self.root.join("names").join(head).join(rest)
52    }
53
54    /// Writes it somewhere else and moves it into place, which is what makes a
55    /// half-written file impossible to read: a rename either happened or did
56    /// not.
57    fn land(&self, at: &Path, bytes: &[u8]) -> Result<(), StoreError> {
58        if let Some(directory) = at.parent() {
59            fs::create_dir_all(directory).map_err(io_error)?;
60        }
61        let landing = self.landing();
62        fs::write(&landing, bytes).map_err(io_error)?;
63        fs::rename(&landing, at).map_err(io_error)
64    }
65
66    /// A path nobody else is writing to: this process, and a number that only
67    /// goes up. Inside the store's own `tmp`, so that landing it is a move
68    /// within one filesystem and never a copy.
69    fn landing(&self) -> PathBuf {
70        self.root.join("tmp").join(format!(
71            "{}-{}",
72            std::process::id(),
73            LANDINGS.fetch_add(1, Ordering::Relaxed)
74        ))
75    }
76}
77
78impl Store for Local {
79    fn put(&self, bytes: &[u8]) -> Result<Digest, StoreError> {
80        let digest = Digest::of(bytes);
81        let at = self.blob(&digest);
82        // Already there is already done: the content is the name.
83        if !at.exists() {
84            self.land(&at, bytes)?;
85        }
86        Ok(digest)
87    }
88
89    fn get(&self, digest: &Digest) -> Result<Option<Vec<u8>>, StoreError> {
90        match fs::read(self.blob(digest)) {
91            Ok(bytes) => Ok(Some(bytes)),
92            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
93            Err(e) => Err(io_error(e)),
94        }
95    }
96
97    fn bind(&self, name: &str, digest: &Digest, meta: Meta) -> Result<(), StoreError> {
98        self.land(&self.record(name), &record(name, digest, meta)?)
99    }
100
101    fn claim(&self, name: &str, digest: &Digest, meta: Meta) -> Result<bool, StoreError> {
102        let at = self.record(name);
103        if let Some(directory) = at.parent() {
104            fs::create_dir_all(directory).map_err(io_error)?;
105        }
106        let written = record(name, digest, meta)?;
107        let landing = self.landing();
108        fs::write(&landing, &written).map_err(io_error)?;
109        // **`link` and not `rename`**, which is the whole difference: a rename
110        // replaces what is there and would hand the same work to everybody, and
111        // `link` fails when the name is taken. It is also the one that has
112        // always been trusted over NFS, where `O_EXCL` has not.
113        let taken = match fs::hard_link(&landing, &at) {
114            Ok(()) => true,
115            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => false,
116            Err(e) => {
117                let _ = fs::remove_file(&landing);
118                return Err(io_error(e));
119            }
120        };
121        // The temporary is the second name for the same bytes, and one name is
122        // enough. Failing to tidy up is not failing to claim.
123        let _ = fs::remove_file(&landing);
124        Ok(taken)
125    }
126
127    fn resolve(&self, name: &str) -> Result<Option<Bound>, StoreError> {
128        match fs::read(self.record(name)) {
129            Ok(bytes) => read_record(&bytes).map(Some),
130            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
131            Err(e) => Err(io_error(e)),
132        }
133    }
134
135    fn bound(&self) -> Result<Vec<Bound>, StoreError> {
136        let mut all = Vec::new();
137        let names = self.root.join("names");
138        for head in read_dir(&names)? {
139            for record in read_dir(&head)? {
140                all.push(read_record(&fs::read(record).map_err(io_error)?)?);
141            }
142        }
143        // By time, and by name within the same second, so two runs of this see
144        // the same thing.
145        all.sort_by(|a, b| (a.when, &a.name).cmp(&(b.when, &b.name)));
146        Ok(all)
147    }
148}
149
150/// What is inside a directory, or nothing if it is not there yet.
151fn read_dir(at: &Path) -> Result<Vec<PathBuf>, StoreError> {
152    match fs::read_dir(at) {
153        Ok(entries) => entries
154            .map(|entry| entry.map(|entry| entry.path()).map_err(io_error))
155            .collect(),
156        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
157        Err(e) => Err(io_error(e)),
158    }
159}
160
161fn io_error(e: io::Error) -> StoreError {
162    StoreError::Io(e.to_string())
163}