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}