Skip to main content

gdown_core/
cache.rs

1//! Cached download with hash verification
2
3use crate::error::Result;
4use md5::{Digest as Md5Digest, Md5};
5use sha1::Sha1;
6use sha2::{Sha256, Sha512};
7use std::path::{Path, PathBuf};
8
9/// Hash algorithm for verification
10#[derive(Debug, Clone, Copy)]
11pub enum HashAlgorithm {
12    Md5,
13    Sha1,
14    Sha256,
15    Sha512,
16}
17
18/// Hash specification
19#[derive(Debug, Clone)]
20pub struct HashSpec {
21    pub algo: HashAlgorithm,
22    pub value: String,
23}
24
25impl HashSpec {
26    pub fn md5(value: &str) -> Self {
27        Self {
28            algo: HashAlgorithm::Md5,
29            value: value.to_lowercase(),
30        }
31    }
32
33    pub fn sha1(value: &str) -> Self {
34        Self {
35            algo: HashAlgorithm::Sha1,
36            value: value.to_lowercase(),
37        }
38    }
39
40    pub fn sha256(value: &str) -> Self {
41        Self {
42            algo: HashAlgorithm::Sha256,
43            value: value.to_lowercase(),
44        }
45    }
46
47    pub fn sha512(value: &str) -> Self {
48        Self {
49            algo: HashAlgorithm::Sha512,
50            value: value.to_lowercase(),
51        }
52    }
53}
54
55/// Cache manager for gdown
56pub struct Cache {
57    root: PathBuf,
58}
59
60impl Cache {
61    /// Create a new cache manager
62    pub fn new() -> Result<Self> {
63        let root = dirs::cache_dir()
64            .unwrap_or_else(|| PathBuf::from("~/.cache/gdown"))
65            .join("gdown");
66        Ok(Self { root })
67    }
68
69    /// Create cache with custom root path
70    pub fn with_root(root: PathBuf) -> Self {
71        Self { root }
72    }
73
74    /// Get cache path for a URL
75    fn cache_path(&self, url: &str) -> PathBuf {
76        let hash = Sha256::digest(url.as_bytes());
77        let hash_str = format!("{:x}", hash);
78        self.root.join(&hash_str)
79    }
80
81    /// Check if URL is cached
82    pub fn is_cached(&self, url: &str) -> bool {
83        let path = self.cache_path(url);
84        path.exists()
85    }
86
87    /// Compute file hash (sync version)
88    pub fn compute_hash_sync(&self, path: &Path, algo: HashAlgorithm) -> Result<String> {
89        use std::io::Read;
90
91        let mut file = std::fs::File::open(path)?;
92        let mut buffer = vec![0u8; 8192];
93
94        match algo {
95            HashAlgorithm::Md5 => {
96                let mut hasher = Md5::new();
97                loop {
98                    let bytes_read = file.read(&mut buffer)?;
99                    if bytes_read == 0 {
100                        break;
101                    }
102                    hasher.update(&buffer[..bytes_read]);
103                }
104                Ok(format!("{:x}", hasher.finalize()))
105            }
106            HashAlgorithm::Sha1 => {
107                let mut hasher = Sha1::new();
108                loop {
109                    let bytes_read = file.read(&mut buffer)?;
110                    if bytes_read == 0 {
111                        break;
112                    }
113                    hasher.update(&buffer[..bytes_read]);
114                }
115                Ok(format!("{:x}", hasher.finalize()))
116            }
117            HashAlgorithm::Sha256 => {
118                let mut hasher = Sha256::new();
119                loop {
120                    let bytes_read = file.read(&mut buffer)?;
121                    if bytes_read == 0 {
122                        break;
123                    }
124                    hasher.update(&buffer[..bytes_read]);
125                }
126                Ok(format!("{:x}", hasher.finalize()))
127            }
128            HashAlgorithm::Sha512 => {
129                let mut hasher = Sha512::new();
130                loop {
131                    let bytes_read = file.read(&mut buffer)?;
132                    if bytes_read == 0 {
133                        break;
134                    }
135                    hasher.update(&buffer[..bytes_read]);
136                }
137                Ok(format!("{:x}", hasher.finalize()))
138            }
139        }
140    }
141
142    /// Verify file hash matches spec
143    pub fn verify_hash(&self, path: &Path, spec: &HashSpec) -> Result<bool> {
144        let computed = self.compute_hash_sync(path, spec.algo)?;
145        Ok(computed.to_lowercase() == spec.value.to_lowercase())
146    }
147}
148
149impl Default for Cache {
150    fn default() -> Self {
151        Self::new().expect("Failed to create cache directory")
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_hash_spec_md5() {
161        let spec = HashSpec::md5("abc123");
162        assert!(matches!(spec.algo, HashAlgorithm::Md5));
163        assert_eq!(spec.value, "abc123");
164    }
165
166    #[test]
167    fn test_hash_spec_sha256() {
168        let spec = HashSpec::sha256("def456");
169        assert!(matches!(spec.algo, HashAlgorithm::Sha256));
170        assert_eq!(spec.value, "def456");
171    }
172}