walker_common/
store.rs

1use crate::retrieve::{RetrievalMetadata, RetrievedDigest};
2use anyhow::Context;
3use sha2::{Sha256, Sha512};
4use std::{path::Path, time::SystemTime};
5use tokio::fs;
6
7pub const ATTR_ETAG: &str = "etag";
8
9#[derive(Debug, thiserror::Error)]
10pub enum StoreError {
11    #[error("{0:#}")]
12    Io(anyhow::Error),
13    #[error("Failed to construct filename from URL: {0}")]
14    Filename(String),
15    #[error("Serialize key error: {0:#}")]
16    SerializeKey(anyhow::Error),
17}
18
19pub struct Document<'a> {
20    /// The data to store
21    pub data: &'a [u8],
22    /// An optional SHA256 digest
23    pub sha256: &'a Option<RetrievedDigest<Sha256>>,
24    /// An optional SHA512 digest
25    pub sha512: &'a Option<RetrievedDigest<Sha512>>,
26    /// An optional signature
27    pub signature: &'a Option<String>,
28
29    /// Last change date
30    pub changed: SystemTime,
31
32    /// Metadata from the retrieval process
33    pub metadata: &'a RetrievalMetadata,
34
35    pub no_timestamps: bool,
36    pub no_xattrs: bool,
37}
38
39pub async fn store_document(file: &Path, document: Document<'_>) -> Result<(), StoreError> {
40    log::debug!("Writing {}", file.display());
41
42    if let Some(parent) = file.parent() {
43        fs::create_dir_all(parent)
44            .await
45            .with_context(|| format!("Failed to create parent directory: {}", parent.display()))
46            .map_err(StoreError::Io)?;
47    }
48
49    fs::write(&file, document.data)
50        .await
51        .with_context(|| format!("Failed to write advisory: {}", file.display()))
52        .map_err(StoreError::Io)?;
53
54    if let Some(sha256) = &document.sha256 {
55        let file = format!("{}.sha256", file.display());
56        fs::write(&file, &sha256.expected)
57            .await
58            .with_context(|| format!("Failed to write checksum: {file}"))
59            .map_err(StoreError::Io)?;
60    }
61    if let Some(sha512) = &document.sha512 {
62        let file = format!("{}.sha512", file.display());
63        fs::write(&file, &sha512.expected)
64            .await
65            .with_context(|| format!("Failed to write checksum: {file}"))
66            .map_err(StoreError::Io)?;
67    }
68    if let Some(sig) = &document.signature {
69        let file = format!("{}.asc", file.display());
70        fs::write(&file, &sig)
71            .await
72            .with_context(|| format!("Failed to write signature: {file}"))
73            .map_err(StoreError::Io)?;
74    }
75
76    if !document.no_timestamps {
77        // We use the retrieval metadata timestamp as file timestamp. If that's not available, then
78        // we use the change entry timestamp.
79        let mtime = document
80            .metadata
81            .last_modification
82            .map(SystemTime::from)
83            .unwrap_or_else(|| document.changed)
84            .into();
85        filetime::set_file_mtime(file, mtime)
86            .with_context(|| {
87                format!(
88                    "Failed to set last modification timestamp: {}",
89                    file.display()
90                )
91            })
92            .map_err(StoreError::Io)?;
93    }
94
95    if !document.no_xattrs {
96        if let Some(etag) = &document.metadata.etag {
97            fsquirrel::set(file, ATTR_ETAG, etag.as_bytes())
98                .with_context(|| format!("Failed to store {}: {}", ATTR_ETAG, file.display()))
99                .map_err(StoreError::Io)?;
100        }
101    }
102
103    Ok(())
104}