anvilsign in

collin/anvil

1//! Uploaded attachments: content-addressed files stored outside git.
2//!
3//! The bytes live on disk under `storage::attachment_path` (never in a git
4//! repository); this module owns the database rows that index them per repo
5//! and the content hashing used as their address. See [`crate::models::Attachment`].
6
7use sha2::{
8 Digest,
9 Sha256,
10};
11
12use crate::{
13 error::Result,
14 models::Attachment,
15};
16
17/// Lowercase hex SHA-256 of `bytes` — the content address used as both the
18/// dedup key and the on-disk filename.
19pub fn content_hash(bytes: &[u8]) -> String {
20 Sha256::digest(bytes)
21 .iter()
22 .map(|b| format!("{b:02x}"))
23 .collect()
24}
25
26/// The attachment row for `(repo_id, hash)`, if one exists.
27pub async fn find(db: &toasty::Db, repo_id: i64, hash: &str) -> Result<Option<Attachment>> {
28 let mut conn = db.clone();
29 let row = Attachment::filter(Attachment::fields().repo_id().eq(repo_id))
30 .filter(Attachment::fields().hash().eq(hash))
31 .first()
32 .exec(&mut conn)
33 .await?;
34 Ok(row)
35}
36
37/// Record an attachment for a repo, deduping on content: if `hash` is already
38/// recorded for `repo_id` the existing row is returned and no new row is made.
39/// The caller writes the bytes to [`crate::storage::attachment_path`] itself.
40pub async fn add(
41 db: &toasty::Db,
42 repo_id: i64,
43 hash: &str,
44 content_type: &str,
45 size: i64,
46 uploader_id: i64,
47) -> Result<Attachment> {
48 if let Some(existing) = find(db, repo_id, hash).await? {
49 return Ok(existing);
50 }
51 let mut conn = db.clone();
52 let row = toasty::create!(Attachment {
53 repo_id: repo_id,
54 hash: hash,
55 content_type: content_type,
56 size: size,
57 uploader_id: uploader_id,
58 created_at: crate::now(),
59 })
60 .exec(&mut conn)
61 .await?;
62 Ok(row)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn content_hash_is_stable_lowercase_hex_sha256() {
71 // Known SHA-256 of "hello".
72 assert_eq!(
73 content_hash(b"hello"),
74 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
75 );
76 assert_eq!(content_hash(b"a"), content_hash(b"a"));
77 assert_ne!(content_hash(b"a"), content_hash(b"b"));
78 }
79}