LCOV - code coverage report
Current view: top level - src - files.rs (source / functions) Coverage Total Hit
Test: bliki.lcov Lines: 96.5 % 57 55
Test Date: 2025-11-27 15:46:07 Functions: 100.0 % 10 10

            Line data    Source code
       1              : //! # Media files
       2              : 
       3              : use bytes::Bytes;
       4              : use rusqlite::{Connection, params};
       5              : use sha3::{Digest as _, Sha3_256};
       6              : use std::fs::write;
       7              : use std::path::Path;
       8              : 
       9              : /// Compute the message digest, using Sha256, of file
      10            2 : pub fn digest(data: &Bytes) -> Vec<u8> {
      11            2 :     let mut hasher = Sha3_256::new();
      12            2 :     hasher.update(data);
      13            2 :     let digest = hasher.finalize();
      14            2 :     digest.to_vec()
      15            2 : }
      16              : 
      17            2 : pub fn tohex(data: &Vec<u8>) -> String {
      18            2 :     let mut ret = String::new();
      19           66 :     for byte in data {
      20           64 :         ret.push_str(&format!("{:02X}", byte));
      21           64 :     }
      22            2 :     ret
      23            2 : }
      24              : 
      25              : // TODO: Consider a FileField struct we can convert from to perform validation
      26              : // and do the heavy lifting for error reporting.
      27              : 
      28              : // Should we receive the body as a stream?
      29              : // TODO: We want to avoid reading all the file into memory. We want to read the
      30              : // field by chunks instead.
      31              : // Cf. https://docs.rs/axum/latest/axum/extract/multipart/struct.Field.html
      32              : /// Store the file in SQLite
      33            2 : pub(crate) fn store_file<P: AsRef<Path>>(
      34            2 :     db: &Connection,
      35            2 :     upload_dir: P,
      36            2 :     bytes: Bytes,
      37            2 :     filename: String,
      38            2 :     content_type: String,
      39            2 : ) -> anyhow::Result<i64> {
      40            2 :     let digest = digest(&bytes);
      41            2 :     let digest_hex = tohex(&digest);
      42              :     // TODO: Infer file category from content_type
      43            2 :     let mut stmt = db.prepare(
      44            2 :         "INSERT INTO files (filename, digest, mimetype) VALUES (?, ?, ?) RETURNING file_id",
      45            0 :     )?;
      46            2 :     let file_id: i64 =
      47            2 :         stmt.query_row(params![filename, digest_hex, content_type], |row| row.get("file_id"))?;
      48            2 :     let data = bytes.to_vec();
      49            2 :     let path = upload_dir.as_ref().join(digest_hex);
      50            2 :     write(path, data)?;
      51              :     // TODO: Use a tx. Commit here. so that we only record the upload if the
      52              :     // file is saved on disk.
      53            2 :     Ok(file_id)
      54            2 : }
      55              : 
      56              : pub(crate) struct MediaFile {
      57              :     pub digest: String,
      58              :     pub content_type: String,
      59              : }
      60              : 
      61            1 : pub(crate) fn find_by_id(db: &Connection, file_id: i64) -> anyhow::Result<MediaFile> {
      62            1 :     let mut stmt = db.prepare("SELECT digest, mimetype FROM files where file_id = ?")?;
      63            1 :     let (digest, content_type): (String, String) =
      64            1 :         stmt.query_row([file_id], |row| Ok((row.get("digest")?, row.get("mimetype")?)))?;
      65              : 
      66            1 :     Ok(MediaFile { content_type, digest })
      67            1 : }
      68              : 
      69              : #[cfg(test)]
      70              : mod tests {
      71              :     use super::*;
      72              :     use crate::db::setup_db;
      73              : 
      74              :     #[test]
      75            1 :     fn test_store_file() -> anyhow::Result<()> {
      76            1 :         let db = setup_db(None);
      77            1 :         let buf = std::fs::read("public/favicon.ico")?;
      78            1 :         let file_id = store_file(
      79            1 :             &db,
      80              :             "/tmp/",
      81            1 :             buf.into(),
      82            1 :             "favicon.ico".to_string(),
      83            1 :             "image/x-icon".to_string(),
      84            0 :         )?;
      85              : 
      86            1 :         let mut stmt = db.prepare("SELECT file_id, digest FROM files WHERE file_id = ?")?;
      87            1 :         let digest: String = stmt.query_row([file_id], |row| row.get("digest"))?;
      88            1 :         assert_eq!(
      89              :             digest,
      90            1 :             "C9D25FC03F47AA1062C344781A66E680D558F0563CFB5522FF57863F027637BA".to_string()
      91              :         );
      92            1 :         let got =
      93            1 :             std::fs::read("/tmp/C9D25FC03F47AA1062C344781A66E680D558F0563CFB5522FF57863F027637BA")?;
      94            1 :         let want = std::fs::read("public/favicon.ico")?;
      95            1 :         assert_eq!(want, got);
      96            1 :         Ok(())
      97            1 :     }
      98              : }
        

Generated by: LCOV version 2.0-1