Skip to main content

rstr/
models.rs

1use super::error::{Error, Result};
2use super::utils::collect_file_paths;
3#[cfg(feature = "progress_bar")]
4use super::utils::progress_bar;
5use chrono::{offset::Utc, DateTime};
6#[cfg(feature = "progress_bar")]
7use indicatif::ProgressIterator;
8use lazy_static::lazy_static;
9use regex::Regex;
10use sha2::{Digest, Sha256};
11use std::sync::mpsc;
12use std::thread;
13use std::{
14    fs::{self, File},
15    io,
16    path::Path,
17    path::PathBuf,
18};
19use tree_magic_mini as magic;
20
21/// Struct representing a reference to an entry in the blob store
22#[derive(Debug, Clone)]
23pub struct BlobRef {
24    /// The value of the reference, i.e. the sha256 hash of the blob
25    value: String,
26}
27
28/// Struct representing the metadata associated to a blob
29#[derive(Debug)]
30pub struct BlobMetadata {
31    /// The filename of the blob
32    pub filename: String,
33    /// The mime-type of the blob (e.g. `image/png`)
34    pub mime_type: String,
35    /// The size of the blob in bytes
36    pub size: u64,
37    /// The creation timestamp of the blob
38    pub created: DateTime<Utc>,
39}
40
41/// Returns a [`BlobRef`] instance from a hasher
42///
43/// # Examples
44///
45/// ```
46/// # use sha2::{Digest, Sha256};
47/// # use rstr::BlobRef;
48/// let mut hasher = Sha256::new();
49/// hasher.update(b"hello world");
50/// let blob_ref = BlobRef::from(hasher);
51/// assert_eq!(blob_ref.reference(), "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
52/// ```
53impl From<Sha256> for BlobRef {
54    fn from(hasher: Sha256) -> Self {
55        BlobRef::new(&format!("{:x}", hasher.finalize())[..]).unwrap()
56    }
57}
58
59impl BlobRef {
60    /// Creates a new [`BlobRef`] from a valid hex representation of the sha256 hash.
61    ///
62    /// # Errors
63    ///
64    /// The method will return a [`Error::InvalidRef`] if the input string
65    /// - has `len() != 64`
66    /// - contains any char except lowercase letters and digits
67    ///
68    /// # Examples
69    /// ```
70    /// # use rstr::BlobRef;
71    /// let blob_ref = BlobRef::new("f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de");
72    /// assert!(blob_ref.is_ok())
73    /// ```
74    /// ```
75    /// # use rstr::BlobRef;
76    /// let blob_ref = BlobRef::new("a_short_hash");
77    /// assert!(blob_ref.is_err());
78    /// let blob_ref = BlobRef::new("....aninvalidhash.29bc64a9d3732b4b9035125fdb3285f5b6455778edca7");
79    /// assert!(blob_ref.is_err());
80    /// ```
81
82    pub fn new(value: &str) -> Result<BlobRef> {
83        lazy_static! {
84            static ref VALID_HASH_REGEX: Regex = Regex::new(r"^[a-z0-9]{64}$").unwrap();
85        }
86
87        if VALID_HASH_REGEX.is_match(value) {
88            Ok(BlobRef {
89                value: String::from(value),
90            })
91        } else {
92            Err(Error::InvalidRef)
93        }
94    }
95
96    /// Converts the blob's reference into a path relative to the root of the blob store.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// # use rstr::BlobRef;
102    /// let hash = "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de";
103    /// let blob_ref = BlobRef::new(hash).unwrap();
104    /// assert_eq!(blob_ref.to_path().to_str().unwrap(), "f2/9b/c6/4a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de")
105    /// ```
106    pub fn to_path(&self) -> PathBuf {
107        PathBuf::from(&self.value[0..2])
108            .join(&self.value[2..4])
109            .join(&self.value[4..6])
110            .join(&self.value[6..])
111    }
112
113    /// Returns a string reference (hex representation of Sha256 hash) for the blob
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// # use rstr::BlobRef;
119    /// let hash = "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de";
120    /// let blob_ref = BlobRef::new(hash).unwrap();
121    ///
122    /// assert_eq!(blob_ref.reference(), hash);
123    /// ```
124    pub fn reference(&self) -> &str {
125        &self.value
126    }
127}
128
129impl std::fmt::Display for BlobRef {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(f, "BlobRef({})", &self.value[..10])
132    }
133}
134
135/// Struct for interacting with the blob store
136#[derive(Clone, Debug)]
137pub struct BlobStore {
138    root: PathBuf,
139}
140
141type BlobRefAndPath = (PathBuf, BlobRef);
142
143impl BlobStore {
144    /// Creates a new instance of the `BlobStore` struct used to interact with the blob
145    /// store. If the specified blob store root path does not exists, it tries to create
146    /// it.
147    ///
148    /// # Errors
149    ///
150    /// It errors if the specified path is not a directory or if it does not exist and
151    /// cannot be created.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use rstr::BlobStore;
157    ///
158    /// let blob_store = BlobStore::new("../tests/test_data_store");
159    /// assert!(blob_store.is_ok());
160    ///
161    /// let blob_store = BlobStore::new("../tests/test_file.txt");
162    /// assert!(blob_store.is_err());
163    /// ```
164    pub fn new<P: AsRef<Path>>(path: P) -> Result<BlobStore> {
165        let path = path.as_ref();
166        if !path.exists() {
167            fs::create_dir_all(path)?
168        } else if !path.is_dir() {
169            // TODO: return proper error
170            return Err(io::Error::from(io::ErrorKind::Other).into());
171        }
172        Ok(BlobStore { root: path.into() })
173    }
174
175    /// Returns an instance of the hasher used to compute the blob reference for a file
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use rstr::BlobStore;
181    /// use sha2::{Digest, Sha256};
182    ///
183    /// let mut hasher = BlobStore::hasher();
184    /// hasher.update(b"hello world");
185    /// let result = hasher.finalize();
186    /// assert_eq!(format!("{:x}", result), "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")
187    /// ```
188    pub fn hasher() -> Sha256 {
189        Sha256::new()
190    }
191
192    /// Given a `BlobRef` it returns it's path inside the blob store
193    fn get_blob_path(&self, blob_ref: &BlobRef) -> PathBuf {
194        self.root.join(blob_ref.to_path())
195    }
196
197    /// Given a `BlobRef` it returns it's path inside the blob store, including the filename
198    ///
199    /// # Errors
200    ///
201    /// It will error if the directory is not present/cannot be read or there is no file.
202    fn get_blob_file_path(&self, blob_ref: &BlobRef) -> Result<PathBuf> {
203        let mut entries = self.get_blob_path(blob_ref).read_dir()?;
204        if let Some(Ok(entry)) = entries.next() {
205            return Ok(entry.path());
206        };
207        Err(Error::BlobNotFound)
208    }
209
210    /// Add a file to the blob store given a path.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use rstr::{BlobStore, BlobRef};
216    /// use std::path::PathBuf;
217    /// let blob_store = BlobStore::new("../tests/test_data_store/").unwrap();
218    ///
219    /// let blob_ref: BlobRef = blob_store.add("../tests/test_file.txt").unwrap();
220    /// assert!(blob_store.exists(&blob_ref));
221    /// assert_eq!(blob_ref.reference(), "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de");
222    /// ```
223    pub fn add<P: AsRef<Path>>(&self, path: P) -> Result<BlobRef> {
224        let mut file = File::open(&path)?;
225        let mut hasher = BlobStore::hasher();
226
227        io::copy(&mut file, &mut hasher)?;
228        let blob_ref = BlobRef::from(hasher);
229
230        if !self.exists(&blob_ref) {
231            let save_path = self.get_blob_path(&blob_ref);
232            fs::create_dir_all(&save_path)?;
233
234            let filename = path.as_ref().file_name().unwrap();
235            let save_path = save_path.join(&filename);
236            fs::copy(path, save_path)?;
237        };
238
239        Ok(blob_ref)
240    }
241
242    /// Given a list of paths to files/directories it adds them to the blob store. In the case
243    /// of a directory it adds all the files in its children recursively.
244    ///
245    /// The function iterates over all paths in parallel and adds each file to the blob store.
246    ///
247    /// It returns two vectors: one containing the paths to the files that were successfully
248    /// added together with their generated `BlobRef` and the other containing the list of
249    /// paths that errored together with the error.
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// # use std::path::Path;
255    /// use rstr::{BlobStore, BlobRef};
256    ///
257    /// let blob_store = BlobStore::new("../tests/test_data_store").unwrap();
258    ///
259    /// let paths = [Path::new("../tests/test_file.txt")];
260    /// let threads: u8 = 8;
261    /// let (blob_refs_with_paths, errors) = blob_store.add_files(&paths[..], threads);
262    /// let blob_refs: Vec<BlobRef> = blob_refs_with_paths.into_iter().map(|(_, b)| b).collect();
263    ///
264    /// assert_eq!(blob_refs[0].reference(), "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de")
265    /// ```
266    pub fn add_files<P: AsRef<Path>>(
267        &self,
268        paths: &[P],
269        threads: u8,
270    ) -> (Vec<BlobRefAndPath>, Vec<(PathBuf, Error)>) {
271        let paths: Vec<PathBuf> = paths
272            .iter()
273            .flat_map(|p| collect_file_paths(p.as_ref()))
274            .collect();
275
276        let (tx, rx) = mpsc::channel();
277
278        let chunk_size = std::cmp::max(paths.len() / threads as usize, 1_usize);
279        let chunks = paths.chunks(chunk_size);
280
281        for chunk in chunks {
282            let tx = tx.clone();
283            let chunk = chunk.to_owned();
284            let blob_store = self.clone();
285            thread::spawn(move || {
286                for path in chunk {
287                    let blob_ref = blob_store.add(&path);
288                    tx.send((path, blob_ref)).expect("err")
289                }
290            });
291        }
292
293        drop(tx);
294
295        let rx_iter = rx.iter();
296
297        #[cfg(feature = "progress_bar")]
298        let rx_iter = rx_iter.progress_with(progress_bar(paths.len() as u64));
299
300        let (success, errors): (Vec<_>, Vec<_>) = rx_iter.partition(|(_, b)| b.is_ok());
301
302        let success = success.into_iter().map(|(p, b)| (p, b.unwrap())).collect();
303        let errors = errors
304            .into_iter()
305            .map(|(p, b)| (p, b.unwrap_err()))
306            .collect();
307        (success, errors)
308    }
309
310    /// Given a [`BlobRef`] it retrieves the associated file and returns it as a byte-array.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// use rstr::{BlobStore, BlobRef};
316    ///
317    /// let blob_store = BlobStore::new("../tests/test_data_store").unwrap();
318    /// let reference = "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de";
319    /// let blob_ref = BlobRef::new(reference).unwrap();
320    ///
321    /// let content = blob_store.get(&blob_ref).unwrap();
322    ///
323    /// assert_eq!(content, &[
324    ///     84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 116, 101, 115, 116, 32, 102, 105, 108,
325    ///     101, 46,
326    /// ]);
327    /// ```
328    pub fn get(&self, blob_ref: &BlobRef) -> Result<Vec<u8>> {
329        Ok(fs::read(&self.get_blob_file_path(blob_ref)?)?)
330    }
331
332    /// Returns `true` if there is a file associated with the [`BlobRef`] in the blob store
333    ///
334    /// # Examples
335    ///
336    /// ```
337    /// use rstr::{BlobStore, BlobRef};
338    ///
339    /// let blob_store = BlobStore::new("../tests/test_data_store/").unwrap();
340    /// let blob_ref = BlobRef::new("f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de").unwrap();
341    ///
342    /// assert!(blob_store.exists(&blob_ref))
343    /// ```
344    pub fn exists(&self, blob_ref: &BlobRef) -> bool {
345        let dir = self.get_blob_path(blob_ref);
346        dir.exists() && dir.read_dir().unwrap().next().is_some()
347    }
348
349    /// Given a [`BlobRef`] it deletes the corresponding blob from the blob store
350    ///
351    /// # Examples
352    ///
353    /// ```no_run
354    /// # use rstr::{BlobStore, BlobRef};
355    /// let blob_store = BlobStore::new("/path/to/blob/store").unwrap();
356    ///
357    /// let blob_ref = BlobRef::new("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9").unwrap();
358    /// assert!(blob_store.exists(&blob_ref));
359    ///
360    /// blob_store.delete(&blob_ref);
361    /// assert!(!blob_store.exists(&blob_ref));
362    /// ```
363    /// # Errors
364    ///
365    /// See [`fs::remove_dir_all`].
366    pub fn delete(&self, blob_ref: &BlobRef) -> Result<()> {
367        Ok(fs::remove_dir_all(self.get_blob_path(blob_ref))?)
368    }
369
370    /// Given a [`BlobRef`] returns the metadata relative to the referenced blob. For more
371    /// details on the metadata returned see `BlobMetadata`.
372    ///
373    /// The mime type is inferred from the file's magic number as a string.
374    /// It defaults to "application/octet-stream" if it cannot determine the type.
375    /// We use the [`tree_magic_mini`] crate to infer the mime type.
376    ///
377    /// # Errors
378    ///
379    /// Will return an error if the file cannot be found/opened or if [`std::fs::metadata`]
380    /// fails.
381    pub fn metadata(&self, blob_ref: &BlobRef) -> Result<BlobMetadata> {
382        let file_path = self.get_blob_file_path(blob_ref)?;
383
384        let mime = magic::from_filepath(&file_path).unwrap_or("application/octet-stream");
385
386        let filename = file_path.file_name().unwrap().to_str().unwrap().to_string();
387
388        let metadata = fs::metadata(file_path)?;
389        Ok(BlobMetadata {
390            mime_type: String::from(mime),
391            filename,
392            size: metadata.len(),
393            created: metadata.created()?.into(),
394        })
395    }
396}
397
398impl BlobMetadata {
399    pub fn created_str(&self) -> String {
400        self.created
401            .to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
402    }
403}