Skip to main content

walker_common/
store.rs

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