Skip to main content

coven_database/
prepared_external_blob.rs

1use std::path::{Path, PathBuf};
2use std::time::SystemTime;
3
4use coven_foundation::atomic_file::FileError;
5
6use crate::DbError;
7
8/// A user-owned file whose plaintext size and SHA-256 digest Coven read in one
9/// pass. Its content facts stay private and can only be consumed by external
10/// blob registration.
11pub 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
48/// Stream a user-owned file once and prepare its opaque content identity for
49/// registration. `progress` receives the cumulative bytes consumed after each
50/// read.
51pub 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}