use crate::error::Result;
use md5::{Digest as Md5Digest, Md5};
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy)]
pub enum HashAlgorithm {
Md5,
Sha1,
Sha256,
Sha512,
}
#[derive(Debug, Clone)]
pub struct HashSpec {
pub algo: HashAlgorithm,
pub value: String,
}
impl HashSpec {
pub fn md5(value: &str) -> Self {
Self {
algo: HashAlgorithm::Md5,
value: value.to_lowercase(),
}
}
pub fn sha1(value: &str) -> Self {
Self {
algo: HashAlgorithm::Sha1,
value: value.to_lowercase(),
}
}
pub fn sha256(value: &str) -> Self {
Self {
algo: HashAlgorithm::Sha256,
value: value.to_lowercase(),
}
}
pub fn sha512(value: &str) -> Self {
Self {
algo: HashAlgorithm::Sha512,
value: value.to_lowercase(),
}
}
}
pub struct Cache {
root: PathBuf,
}
impl Cache {
pub fn new() -> Result<Self> {
let root = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("~/.cache/gdown"))
.join("gdown");
Ok(Self { root })
}
pub fn with_root(root: PathBuf) -> Self {
Self { root }
}
fn cache_path(&self, url: &str) -> PathBuf {
let hash = Sha256::digest(url.as_bytes());
let hash_str = format!("{:x}", hash);
self.root.join(&hash_str)
}
pub fn is_cached(&self, url: &str) -> bool {
let path = self.cache_path(url);
path.exists()
}
pub fn compute_hash_sync(&self, path: &Path, algo: HashAlgorithm) -> Result<String> {
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut buffer = vec![0u8; 8192];
match algo {
HashAlgorithm::Md5 => {
let mut hasher = Md5::new();
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
HashAlgorithm::Sha1 => {
let mut hasher = Sha1::new();
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
HashAlgorithm::Sha256 => {
let mut hasher = Sha256::new();
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
HashAlgorithm::Sha512 => {
let mut hasher = Sha512::new();
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
}
}
pub fn verify_hash(&self, path: &Path, spec: &HashSpec) -> Result<bool> {
let computed = self.compute_hash_sync(path, spec.algo)?;
Ok(computed.to_lowercase() == spec.value.to_lowercase())
}
}
impl Default for Cache {
fn default() -> Self {
Self::new().expect("Failed to create cache directory")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_spec_md5() {
let spec = HashSpec::md5("abc123");
assert!(matches!(spec.algo, HashAlgorithm::Md5));
assert_eq!(spec.value, "abc123");
}
#[test]
fn test_hash_spec_sha256() {
let spec = HashSpec::sha256("def456");
assert!(matches!(spec.algo, HashAlgorithm::Sha256));
assert_eq!(spec.value, "def456");
}
}