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 pub data: &'a [u8],
23 pub sha256: &'a Option<RetrievedDigest<Sha256>>,
25 pub sha512: &'a Option<RetrievedDigest<Sha512>>,
27 pub signature: &'a Option<String>,
29
30 pub changed: SystemTime,
32
33 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
45pub 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
70pub 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 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}