coven_database/
prepared_external_blob.rs1use std::path::{Path, PathBuf};
2use std::time::SystemTime;
3
4use coven_foundation::atomic_file::FileError;
5
6use crate::DbError;
7
8pub struct PreparedExternalBlob {
12 path: PathBuf,
13 size: u64,
14 hash: String,
15 snapshot: FileSnapshot,
16}
17
18#[derive(Clone, Copy, PartialEq, Eq)]
19struct FileSnapshot {
20 size: u64,
21 modified: SystemTime,
22}
23
24impl PreparedExternalBlob {
25 pub(crate) fn validate_current(&self) -> Result<(), DbError> {
26 let metadata = std::fs::metadata(&self.path)
27 .map_err(|source| FileError::at("stat prepared external blob", &self.path, source))?;
28 let current = snapshot(&self.path, &metadata)?;
29 if current != self.snapshot {
30 return Err(changed(&self.path));
31 }
32 Ok(())
33 }
34
35 pub(crate) fn path(&self) -> &Path {
36 &self.path
37 }
38
39 pub(crate) fn size(&self) -> u64 {
40 self.size
41 }
42
43 pub(crate) fn hash(&self) -> &str {
44 &self.hash
45 }
46}
47
48pub async fn prepare_external_blob(
52 path: &Path,
53 progress: impl Fn(u64) + Send + Sync,
54) -> Result<PreparedExternalBlob, DbError> {
55 let before_metadata = tokio::fs::metadata(path)
56 .await
57 .map_err(|source| FileError::at("stat external blob before preparation", path, source))?;
58 if !before_metadata.is_file() {
59 return Err(FileError::NotFile {
60 subject: "external blob",
61 path: path.to_path_buf(),
62 }
63 .into());
64 }
65 let before = snapshot(path, &before_metadata)?;
66 let (size, digest) =
67 coven_foundation::local_file::file_facts_with_progress(path, progress).await?;
68 let after_metadata = tokio::fs::metadata(path)
69 .await
70 .map_err(|source| FileError::at("stat external blob after preparation", path, source))?;
71 let after = snapshot(path, &after_metadata)?;
72 if before != after || size != after.size {
73 return Err(changed(path));
74 }
75 Ok(PreparedExternalBlob {
76 path: path.to_path_buf(),
77 size,
78 hash: hex::encode(digest),
79 snapshot: after,
80 })
81}
82
83fn snapshot(path: &Path, metadata: &std::fs::Metadata) -> Result<FileSnapshot, DbError> {
84 let modified = metadata
85 .modified()
86 .map_err(|source| FileError::at("read external blob modification time", path, source))?;
87 Ok(FileSnapshot {
88 size: metadata.len(),
89 modified,
90 })
91}
92
93fn changed(path: &Path) -> DbError {
94 DbError::Message(format!(
95 "external blob changed while Coven prepared it: {}",
96 path.display()
97 ))
98}