treasury-store 0.2.2

Treasury storage
Documentation
use std::{
    fs::File,
    io::Write,
    mem::size_of_val,
    path::{Path, PathBuf},
    time::SystemTime,
};

use eyre::WrapErr;
use hashbrown::{hash_map::RawEntryMut, HashMap};
use url::Url;

use crate::{scheme::Scheme, temp::Temporaries};

/// Fetches and caches sources.
pub struct Sources {
    feched: HashMap<Url, (PathBuf, bool)>,
}

impl Sources {
    pub fn new() -> Self {
        Sources {
            feched: HashMap::new(),
        }
    }

    pub fn get(&self, source: &Url) -> Option<(&Path, Option<SystemTime>)> {
        let (path, local) = self.feched.get(source)?;
        if *local {
            let modified = path.metadata().ok()?.modified().ok()?;
            Some((path, Some(modified)))
        } else {
            Some((path, None))
        }
    }

    pub async fn fetch(
        &mut self,
        temporaries: &mut Temporaries<'_>,
        source: &Url,
    ) -> eyre::Result<(&Path, Option<SystemTime>)> {
        match self.feched.raw_entry_mut().from_key(&source) {
            RawEntryMut::Occupied(entry) => {
                let (path, local) = entry.into_mut();
                if *local {
                    let modified = path.metadata()?.modified()?;
                    Ok((path, Some(modified)))
                } else {
                    Ok((path, None))
                }
            }
            RawEntryMut::Vacant(entry) => match source.scheme().parse() {
                Ok(Scheme::File) => {
                    let path = source
                        .to_file_path()
                        .map_err(|()| eyre::eyre!("Invalid file: URL"))?;

                    let modified = path.metadata()?.modified()?;

                    tracing::debug!("Fetching file '{}' ('{}')", source, path.display());
                    let (_, (path, _)) = entry.insert(source.clone(), (path, true));

                    Ok((path, Some(modified)))
                }
                Ok(Scheme::Data) => {
                    let data_start = source.as_str()[size_of_val("data:")..]
                        .find(',')
                        .ok_or_else(|| eyre::eyre!("Invalid data URL"))?
                        + 1
                        + size_of_val("data:");
                    let data = &source.as_str()[data_start..];

                    let temp = temporaries.make_temporary();
                    let mut file = File::create(&temp)
                        .wrap_err("Failed to create temporary file to store data URL content")?;

                    if source.as_str()[..data_start].ends_with(";base64,") {
                        let decoded = base64::decode_config(data, base64::URL_SAFE_NO_PAD)
                            .wrap_err("Failed to decode base64 data url")?;

                        file.write_all(&decoded).wrap_err_with(|| {
                            format!(
                                "Failed to write data URL content to temporary file '{}'",
                                temp.display(),
                            )
                        })?;
                    } else {
                        file.write_all(data.as_bytes()).wrap_err_with(|| {
                            format!(
                                "Failed to write data URL content to temporary file '{}'",
                                temp.display(),
                            )
                        })?;
                    }

                    let (_, (path, _)) = entry.insert(source.clone(), (temp, false));
                    Ok((path, None))
                }
                Err(_) => Err(eyre::eyre!("Unsupported scheme '{}'", source.scheme())),
            },
        }
    }
}