Skip to main content

coven_database/
external_blob_records.rs

1use std::path::Path;
2
3use rusqlite::{Connection, OptionalExtension};
4
5use super::{with_coven_sql_authority, DbError};
6use coven_protocol::blob::{RowBlobAuthority, RowBlobRef};
7
8/// An external user-owned file a blob id resolves to, read back from a
9/// `local_blob_refs` row. The blob's plaintext lives at `path` (an absolute file
10/// Coven references but does not own); `size` is its registered plaintext length,
11/// combined with the row's signed content hash to validate the exact file.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ExternalBlob {
14    /// Absolute path to the external file Coven reads but does not own.
15    pub path: std::path::PathBuf,
16    /// The file's plaintext length at registration. A read fails loud if the
17    /// file's current length differs.
18    pub size: u64,
19}
20
21pub(crate) struct ExternalBlobRecords<'connection> {
22    connection: &'connection Connection,
23}
24
25impl<'connection> ExternalBlobRecords<'connection> {
26    pub(crate) fn new(connection: &'connection Connection) -> Self {
27        Self { connection }
28    }
29
30    pub(crate) fn register(&self, reference: &RowBlobRef, path: &Path) -> Result<(), DbError> {
31        if reference.authority() != &RowBlobAuthority::Local || reference.stored().is_some() {
32            return Err(DbError::Message(
33                "external file requires an exact Local row blob reference".to_string(),
34            ));
35        }
36        let path = path.to_str().ok_or_else(|| {
37            DbError::Message(format!("external blob path is not UTF-8: {path:?}"))
38        })?;
39        let size = i64::try_from(reference.plaintext_size()).map_err(|_| {
40            DbError::Message("external blob plaintext size exceeds SQLite INTEGER".to_string())
41        })?;
42        with_coven_sql_authority(|| {
43            self.connection
44                .execute(
45                    "INSERT INTO local_blob_refs
46                     (table_name, row_id, column_name, row_stamp, namespace, blob_id,
47                      path, plaintext_size, plaintext_hash)
48                     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
49                     ON CONFLICT(table_name, row_id, column_name, row_stamp) DO UPDATE SET
50                       namespace = excluded.namespace,
51                       blob_id = excluded.blob_id,
52                       path = excluded.path,
53                       plaintext_size = excluded.plaintext_size,
54                       plaintext_hash = excluded.plaintext_hash",
55                    rusqlite::params![
56                        reference.table(),
57                        reference.row_id(),
58                        reference.column(),
59                        reference.row_stamp(),
60                        &reference.blob().namespace,
61                        &reference.blob().id,
62                        path,
63                        size,
64                        reference.plaintext_hash().to_string(),
65                    ],
66                )
67                .map(|_| ())
68                .map_err(DbError::from)
69        })
70    }
71
72    pub(crate) fn clear(&self, reference: &RowBlobRef) -> Result<(), DbError> {
73        with_coven_sql_authority(|| {
74            self.connection
75                .execute(
76                    "DELETE FROM local_blob_refs WHERE table_name = ?1 AND row_id = ?2
77                     AND column_name = ?3 AND row_stamp = ?4",
78                    rusqlite::params![
79                        reference.table(),
80                        reference.row_id(),
81                        reference.column(),
82                        reference.row_stamp(),
83                    ],
84                )
85                .map(|_| ())
86                .map_err(DbError::from)
87        })
88    }
89
90    pub(crate) fn load(&self, reference: &RowBlobRef) -> Result<Option<ExternalBlob>, DbError> {
91        let row = self
92            .connection
93            .query_row(
94                "SELECT path, plaintext_size, plaintext_hash, namespace, blob_id
95                 FROM local_blob_refs
96                 WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
97                   AND row_stamp = ?4",
98                rusqlite::params![
99                    reference.table(),
100                    reference.row_id(),
101                    reference.column(),
102                    reference.row_stamp()
103                ],
104                |row| {
105                    Ok((
106                        row.get::<_, String>(0)?,
107                        row.get::<_, i64>(1)?,
108                        row.get::<_, String>(2)?,
109                        row.get::<_, String>(3)?,
110                        row.get::<_, String>(4)?,
111                    ))
112                },
113            )
114            .optional()
115            .map_err(DbError::from)?;
116        let Some((path, size, hash, stored_namespace, stored_blob_id)) = row else {
117            return Ok(None);
118        };
119        let size = u64::try_from(size).map_err(|_| {
120            DbError::Message(format!(
121                "external blob {} has negative size",
122                reference.blob().id
123            ))
124        })?;
125        let hash: coven_protocol::store_commit::ObjectHash = hash.parse().map_err(|error| {
126            DbError::context(format!("external blob {} hash", reference.blob().id), error)
127        })?;
128        if size != reference.plaintext_size()
129            || hash != reference.plaintext_hash()
130            || stored_namespace != reference.blob().namespace
131            || stored_blob_id != reference.blob().id
132        {
133            return Err(DbError::Message(format!(
134                "external blob row {}/{}/{} differs from its row reference",
135                reference.table(),
136                reference.row_id(),
137                reference.column()
138            )));
139        }
140        Ok(Some(ExternalBlob {
141            path: std::path::PathBuf::from(path),
142            size,
143        }))
144    }
145}