kget/optimization.rs
1//! Download optimization through compression and caching.
2//!
3//! This module provides the [`Optimizer`] struct for configuring and applying
4//! optimizations to downloads:
5//!
6//! - **Compression**: Automatic compression/decompression using Gzip, LZ4, or Brotli
7//! - **Caching**: Store downloaded files locally to avoid redundant downloads
8//! - **Speed limiting**: Control bandwidth usage
9//!
10//! # Example
11//!
12//! ```rust
13//! use kget::Optimizer;
14//!
15//! // Create with default settings
16//! let optimizer = Optimizer::new();
17//!
18//! // Check if compression is enabled
19//! if optimizer.is_compression_enabled() {
20//! println!("Compression active");
21//! }
22//! ```
23
24use std::error::Error;
25use std::fs::{self, File};
26use std::io::{Read, Write};
27use std::path::PathBuf;
28use flate2::write::{GzEncoder, GzDecoder};
29use lz4::block::{compress, CompressionMode};
30use crate::config::OptimizationConfig;
31
32/// Download optimizer for compression, caching, and speed limiting.
33///
34/// The `Optimizer` manages download performance features:
35///
36/// - **Compression**: Reduces storage size using configurable algorithms
37/// - **Caching**: Stores files locally to avoid re-downloading
38/// - **Speed limits**: Controls maximum download speed
39///
40/// # Compression Levels
41///
42/// | Level | Algorithm | Speed | Ratio |
43/// |-------|-----------|----------|----------|
44/// | 1-3 | Gzip | Fast | Moderate |
45/// | 4-6 | LZ4 | Balanced | Good |
46/// | 7-9 | Brotli | Slow | Best |
47///
48/// # Example
49///
50/// ```rust
51/// use kget::{Optimizer, Config};
52///
53/// // From config
54/// let config = Config::default();
55/// let optimizer = Optimizer::from_config(config.optimization);
56///
57/// // Or with defaults
58/// let optimizer = Optimizer::new();
59/// ```
60#[derive(Clone)]
61pub struct Optimizer {
62 config: OptimizationConfig,
63 /// Speed limit in bytes per second (None = unlimited)
64 pub speed_limit: Option<u64>,
65}
66
67impl Optimizer {
68 /// Create a new `Optimizer` with the provided configuration.
69 ///
70 /// # Arguments
71 ///
72 /// * `config` - Optimization configuration settings
73 ///
74 /// # Example
75 ///
76 /// ```rust
77 /// use kget::{Optimizer, Config};
78 ///
79 /// let config = Config::default();
80 /// let optimizer = Optimizer::from_config(config.optimization);
81 /// ```
82 pub fn from_config(config: OptimizationConfig) -> Self {
83 let speed_limit = config.speed_limit;
84 Self {
85 config,
86 speed_limit,
87 }
88 }
89
90 /// Compress data using the configured algorithm.
91 ///
92 /// The algorithm is selected based on `compression_level`:
93 /// - Levels 1-3: Gzip (fast)
94 /// - Levels 4-6: LZ4 (balanced)
95 /// - Levels 7-9: Brotli (high compression)
96 ///
97 /// Returns the original data unchanged if compression is disabled.
98 #[allow(dead_code)]
99 pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
100 if !self.config.compression {
101 return Ok(data.to_vec());
102 }
103 let compressed = match self.config.compression_level {
104 1..=3 => {
105 let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::fast());
106 encoder.write_all(data)?;
107 encoder.finish()?
108 }
109 4..=6 => {
110 compress(data, Some(CompressionMode::FAST(0)), true)?
111 }
112 7..=9 => {
113 let mut encoder = brotli::CompressorWriter::new(
114 Vec::new(),
115 self.config.compression_level as usize,
116 4096,
117 22,
118 );
119 encoder.write_all(data)?;
120 encoder.into_inner()
121 }
122 _ => return Ok(data.to_vec()),
123 };
124 Ok(compressed)
125 }
126
127 /// Decompress data using the appropriate algorithm based on the file header
128 ///
129 /// Supports Gzip, Brotli, and LZ4
130 pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
131 if !self.config.compression {
132 return Ok(data.to_vec());
133 }
134 let mut decompressed = Vec::new();
135 if data.starts_with(&[0x1f, 0x8b]) {
136 let mut decoder = GzDecoder::new(Vec::new());
137 decoder.write_all(data)?;
138 decompressed = decoder.finish()?;
139 } else if data.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) {
140 let mut decoder = brotli::Decompressor::new(data, 4096);
141 decoder.read_to_end(&mut decompressed)?;
142 } else {
143 let mut decoder = lz4::Decoder::new(data)?;
144 decoder.read_to_end(&mut decompressed)?;
145 }
146 Ok(decompressed)
147 }
148
149 /// Retrieve a file from the cache if it exists.
150 ///
151 /// # Arguments
152 ///
153 /// * `url` - The URL that was used to download the file
154 ///
155 /// # Returns
156 ///
157 /// - `Ok(Some(data))` if the file exists in cache
158 /// - `Ok(None)` if caching is disabled or file doesn't exist
159 /// - `Err` on I/O errors
160 #[allow(dead_code)]
161 pub fn get_cached_file(&self, url: &str) -> Result<Option<Vec<u8>>, Box<dyn Error>> {
162 if !self.config.cache_enabled {
163 return Ok(None);
164 }
165
166 let _cache_dir = self.config.cache_dir.as_str();
167 let cache_path = self.get_cache_path(url)?;
168 if cache_path.exists() {
169 let mut file = File::open(cache_path)?;
170 let mut contents = Vec::new();
171 file.read_to_end(&mut contents)?;
172 return Ok(Some(contents));
173 }
174 Ok(None)
175 }
176
177 /// Store a file in the cache
178 ///
179 /// Does nothing if caching is disabled
180 #[allow(dead_code)]
181 pub fn cache_file(&self, url: &str, data: &[u8]) -> Result<(), Box<dyn Error>> {
182 if !self.config.cache_enabled {
183 return Ok(());
184 }
185 let cache_path = self.get_cache_path(url)?;
186 if let Some(parent) = cache_path.parent() {
187 fs::create_dir_all(parent)?;
188 }
189 let mut file = File::create(cache_path)?;
190 file.write_all(data)?;
191 Ok(())
192 }
193
194 /// Generate the cache file path based on the URL
195 ///
196 /// Uses a simple hash to generate a unique filename
197 #[allow(dead_code)]
198 fn get_cache_path(&self, url: &str) -> Result<PathBuf, Box<dyn Error>> {
199 let mut cache_dir = PathBuf::from(
200 if self.config.cache_dir.is_empty() {
201 "~/.cache/kget".to_string()
202 } else {
203 self.config.cache_dir.clone()
204 }
205 );
206
207 if cache_dir.starts_with("~") {
208 if let Some(home) = dirs::home_dir() {
209 cache_dir = home.join(cache_dir.strip_prefix("~").unwrap());
210 }
211 }
212
213 // Simple hash function to generate a unique filename
214 let mut hash = 0u64;
215 for byte in url.bytes() {
216 hash = hash.wrapping_mul(31).wrapping_add(byte as u64);
217 }
218
219 cache_dir.push(format!("{:x}", hash));
220 Ok(cache_dir)
221 }
222
223 /// Get the peer connection limit for torrent downloads.
224 ///
225 /// Uses the speed limit as a proxy for connection capacity.
226 /// Returns 50 if no speed limit is set.
227 pub fn get_peer_limit(&self) -> usize {
228 self.speed_limit.unwrap_or(50) as usize
229 }
230
231 /// Check if compression is enabled.
232 pub fn is_compression_enabled(&self) -> bool {
233 self.config.compression
234 }
235
236 /// Create a new `Optimizer` with default settings.
237 ///
238 /// Equivalent to `Optimizer::default()`.
239 pub fn new() -> Self {
240 Self::default()
241 }
242
243 /// Deprecated alias for `from_config`. Use `Optimizer::from_config()` instead.
244 #[doc(hidden)]
245 pub fn with_config(config: OptimizationConfig) -> Self {
246 Self::from_config(config)
247 }
248}
249
250impl Default for Optimizer {
251 /// Create an `Optimizer` with sensible defaults:
252 /// - Compression enabled at level 6 (LZ4)
253 /// - Caching enabled in ~/.cache/kget
254 /// - No speed limit
255 /// - 4 max connections
256 fn default() -> Self {
257 Self {
258 config: OptimizationConfig {
259 compression: true,
260 compression_level: 6,
261 cache_enabled: true,
262 cache_dir: "~/.cache/kget".to_string(),
263 speed_limit: None,
264 max_connections: 4,
265 },
266 speed_limit: None,
267 }
268 }
269}