use crate::retrieve::{RetrievalMetadata, RetrievedDigest};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use sha2::{Sha256, Sha512};
use std::{path::Path, time::SystemTime};
use tokio::fs;
pub const ATTR_ETAG: &str = "etag";
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("{0:#}")]
Io(anyhow::Error),
#[error("Failed to construct filename from URL: {0}")]
Filename(String),
#[error("Serialize key error: {0:#}")]
SerializeKey(anyhow::Error),
}
pub struct Document<'a> {
pub data: &'a [u8],
pub sha256: &'a Option<RetrievedDigest<Sha256>>,
pub sha512: &'a Option<RetrievedDigest<Sha512>>,
pub signature: &'a Option<String>,
pub changed: SystemTime,
pub metadata: &'a RetrievalMetadata,
pub no_timestamps: bool,
pub no_xattrs: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorData {
pub status_code: u16,
}
pub async fn store_errors(file: &Path, document: ErrorData) -> Result<(), StoreError> {
log::debug!("Writing errors for {}", file.display());
if let Some(parent) = file.parent() {
fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to create parent directory: {}", parent.display()))
.map_err(StoreError::Io)?;
}
let error_file = file.with_added_extension("errors");
fs::write(
&error_file,
serde_json::to_vec(&document)
.context("Failed to encode error information")
.map_err(StoreError::Io)?,
)
.await
.with_context(|| format!("Failed to write advisory errors: {}", error_file.display()))
.map_err(StoreError::Io)?;
Ok(())
}
pub async fn store_document(file: &Path, document: Document<'_>) -> Result<(), StoreError> {
log::debug!("Writing {}", file.display());
if let Some(parent) = file.parent() {
fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to create parent directory: {}", parent.display()))
.map_err(StoreError::Io)?;
}
fs::write(&file, document.data)
.await
.with_context(|| format!("Failed to write advisory: {}", file.display()))
.map_err(StoreError::Io)?;
if let Some(sha256) = &document.sha256 {
let file = format!("{}.sha256", file.display());
fs::write(&file, &sha256.expected)
.await
.with_context(|| format!("Failed to write checksum: {file}"))
.map_err(StoreError::Io)?;
}
if let Some(sha512) = &document.sha512 {
let file = format!("{}.sha512", file.display());
fs::write(&file, &sha512.expected)
.await
.with_context(|| format!("Failed to write checksum: {file}"))
.map_err(StoreError::Io)?;
}
if let Some(sig) = &document.signature {
let file = format!("{}.asc", file.display());
fs::write(&file, &sig)
.await
.with_context(|| format!("Failed to write signature: {file}"))
.map_err(StoreError::Io)?;
}
if !document.no_timestamps {
let mtime = document
.metadata
.last_modification
.map(SystemTime::from)
.unwrap_or_else(|| document.changed)
.into();
filetime::set_file_mtime(file, mtime)
.with_context(|| {
format!(
"Failed to set last modification timestamp: {}",
file.display()
)
})
.map_err(StoreError::Io)?;
}
if !document.no_xattrs
&& let Some(etag) = &document.metadata.etag
{
fsquirrel::set(file, ATTR_ETAG, etag.as_bytes())
.with_context(|| format!("Failed to store {}: {}", ATTR_ETAG, file.display()))
.map_err(StoreError::Io)?;
}
Ok(())
}