gdown-core 0.1.0

Core download logic for Google Drive
Documentation
//! Cached download with hash verification

use crate::error::Result;
use md5::{Digest as Md5Digest, Md5};
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use std::path::{Path, PathBuf};

/// Hash algorithm for verification
#[derive(Debug, Clone, Copy)]
pub enum HashAlgorithm {
    Md5,
    Sha1,
    Sha256,
    Sha512,
}

/// Hash specification
#[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(),
        }
    }
}

/// Cache manager for gdown
pub struct Cache {
    root: PathBuf,
}

impl Cache {
    /// Create a new cache manager
    pub fn new() -> Result<Self> {
        let root = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from("~/.cache/gdown"))
            .join("gdown");
        Ok(Self { root })
    }

    /// Create cache with custom root path
    pub fn with_root(root: PathBuf) -> Self {
        Self { root }
    }

    /// Get cache path for a URL
    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)
    }

    /// Check if URL is cached
    pub fn is_cached(&self, url: &str) -> bool {
        let path = self.cache_path(url);
        path.exists()
    }

    /// Compute file hash (sync version)
    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()))
            }
        }
    }

    /// Verify file hash matches spec
    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");
    }
}