#![warn(missing_docs)]
#![doc(html_root_url = "https://docs.rs/file-with-meta/0.2.0")]
use std::fs::{self, Metadata};
use std::io::Error as IoError;
use std::path::Path;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use serde_json::Error as SJError;
use thiserror::Error;
#[cfg(feature = "ureq")]
use ureq::Request;
#[cfg(test)]
mod tests;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("Could not examine {0}")]
Examine(String, #[source] IoError),
#[error("Unsupported format major version {0}")]
FormatVersionMajor(u32),
#[error("file-with-meta internal error: {0}")]
Internal(String),
#[error("Could not parse the metadata")]
Parse(SJError),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct MetadataFormatVersion {
major: u32,
minor: u32,
}
impl Default for MetadataFormatVersion {
fn default() -> Self {
Self { major: 0, minor: 1 }
}
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct MetadataFormat {
version: MetadataFormatVersion,
}
#[derive(Debug, Serialize, Deserialize)]
struct MetadataTopLevelFormatOnly {
format: MetadataFormat,
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct FileHttpMetadata {
pub format: MetadataFormat,
pub file_size: u64,
pub file_mtime: u64,
pub hdr_last_modified: Option<String>,
pub hdr_etag: Option<String>,
pub source_file_size: Option<u64>,
pub source_file_mtime: Option<u64>,
pub verified: bool,
}
impl FileHttpMetadata {
pub fn from_file<P>(path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
match fs::metadata(&path) {
Ok(meta) => Ok(Self {
file_size: meta.len(),
file_mtime: mtime_to_unix(&meta)?,
..Self::default()
}),
Err(err) => Err(Error::Examine(path.as_ref().display().to_string(), err)),
}
}
pub fn from_file_with_source<P1, P2>(path: P1, src: P2) -> Result<Self, Error>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let meta = Self::from_file(path)?;
match fs::metadata(&src) {
Ok(src_meta) => Ok(Self {
source_file_size: Some(src_meta.len()),
source_file_mtime: Some(mtime_to_unix(&src_meta)?),
..meta
}),
Err(err) => Err(Error::Examine(src.as_ref().display().to_string(), err)),
}
}
pub fn from_file_with_source_meta<P>(path: P, src_meta: &Self) -> Result<Self, Error>
where
P: AsRef<Path>,
{
let meta = Self::from_file(path)?;
Ok(Self {
source_file_size: Some(src_meta.file_size),
source_file_mtime: Some(src_meta.file_mtime),
..meta
})
}
pub fn parse(contents: &str) -> Result<Self, Error> {
let header =
serde_json::from_str::<MetadataTopLevelFormatOnly>(contents).map_err(Error::Parse)?;
match header.format.version.major {
0 => serde_json::from_str::<Self>(contents).map_err(Error::Parse),
_ => Err(Error::FormatVersionMajor(header.format.version.major)),
}
}
}
pub fn mtime_to_unix(metadata: &Metadata) -> Result<u64, Error> {
Ok(metadata
.modified()
.map_err(|err| {
Error::Internal(format!(
"Could not get the mtime from {:?}: {}",
metadata, err
))
})?
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|err| {
Error::Internal(format!(
"Could not get a Unix epoch timestamp from the 'modified' time in {:?}: {}",
metadata, err
))
})?
.as_secs())
}
#[allow(clippy::unnecessary_lazy_evaluations)]
pub fn match_meta<P1, P2>(dst: P1, dst_meta: P2) -> Result<Option<FileHttpMetadata>, Error>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
if let Ok(file_meta) = fs::metadata(&dst) {
if let Ok(contents) = fs::read_to_string(&dst_meta) {
if let Ok(meta) = FileHttpMetadata::parse(&contents) {
return Ok((file_meta.is_file()
&& file_meta.len() == meta.file_size
&& mtime_to_unix(&file_meta)? == meta.file_mtime)
.then(|| meta));
}
}
}
Ok(None)
}
#[allow(clippy::unnecessary_lazy_evaluations)]
pub fn match_meta_with_source<P1, P2, P3>(
dst: P1,
dst_meta: P2,
src: P3,
) -> Result<Option<FileHttpMetadata>, Error>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
P3: AsRef<Path>,
{
if let Some(meta) = match_meta(dst, dst_meta)? {
Ok(match fs::metadata(src) {
Ok(src_meta) => {
let src_len = src_meta.len();
if meta.source_file_size.unwrap_or(src_len) == src_len {
let src_mtime = mtime_to_unix(&src_meta)?;
(meta.source_file_mtime.unwrap_or(src_mtime) == src_mtime).then(|| meta)
} else {
None
}
}
Err(_) => {
(meta.source_file_size.is_none() && meta.source_file_mtime.is_none()).then(|| meta)
}
})
} else {
Ok(None)
}
}
#[cfg(feature = "ureq")]
#[allow(clippy::doc_markdown)]
pub fn build_req<P1, P2>(
orig_req: Request,
dst: P1,
dst_meta: P2,
) -> Result<(Request, Option<FileHttpMetadata>), Error>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let stored_meta = match_meta(dst, dst_meta)?;
let req = match stored_meta {
None => orig_req,
Some(ref meta) => match meta.hdr_etag {
Some(ref etag) => orig_req.set("If-None-Match", etag),
None => match meta.hdr_last_modified {
Some(ref last_modified) => orig_req.set("If-Modified-Since", last_modified),
None => orig_req,
},
},
};
Ok((req, stored_meta))
}