Skip to main content

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 crate::config::OptimizationConfig;
25use flate2::write::{GzDecoder, GzEncoder};
26use lz4::block::{CompressionMode, compress};
27use std::error::Error;
28use std::fs::{self, File};
29use std::io::{Read, Write};
30use std::path::PathBuf;
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 => compress(data, Some(CompressionMode::FAST(0)), true)?,
110            7..=9 => {
111                let mut encoder = brotli::CompressorWriter::new(
112                    Vec::new(),
113                    self.config.compression_level as usize,
114                    4096,
115                    22,
116                );
117                encoder.write_all(data)?;
118                encoder.into_inner()
119            }
120            _ => return Ok(data.to_vec()),
121        };
122        Ok(compressed)
123    }
124
125    /// Decompress data using the appropriate algorithm based on the file header
126    ///
127    /// Supports Gzip, Brotli, and LZ4
128    pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
129        if !self.config.compression {
130            return Ok(data.to_vec());
131        }
132        let mut decompressed = Vec::new();
133        if data.starts_with(&[0x1f, 0x8b]) {
134            let mut decoder = GzDecoder::new(Vec::new());
135            decoder.write_all(data)?;
136            decompressed = decoder.finish()?;
137        } else if data.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) {
138            let mut decoder = brotli::Decompressor::new(data, 4096);
139            decoder.read_to_end(&mut decompressed)?;
140        } else {
141            let mut decoder = lz4::Decoder::new(data)?;
142            decoder.read_to_end(&mut decompressed)?;
143        }
144        Ok(decompressed)
145    }
146
147    /// Retrieve a file from the cache if it exists.
148    ///
149    /// # Arguments
150    ///
151    /// * `url` - The URL that was used to download the file
152    ///
153    /// # Returns
154    ///
155    /// - `Ok(Some(data))` if the file exists in cache
156    /// - `Ok(None)` if caching is disabled or file doesn't exist
157    /// - `Err` on I/O errors
158    #[allow(dead_code)]
159    pub fn get_cached_file(&self, url: &str) -> Result<Option<Vec<u8>>, Box<dyn Error>> {
160        if !self.config.cache_enabled {
161            return Ok(None);
162        }
163
164        let _cache_dir = self.config.cache_dir.as_str();
165        let cache_path = self.get_cache_path(url)?;
166        if cache_path.exists() {
167            let mut file = File::open(cache_path)?;
168            let mut contents = Vec::new();
169            file.read_to_end(&mut contents)?;
170            return Ok(Some(contents));
171        }
172        Ok(None)
173    }
174
175    /// Store a file in the cache
176    ///
177    /// Does nothing if caching is disabled
178    #[allow(dead_code)]
179    pub fn cache_file(&self, url: &str, data: &[u8]) -> Result<(), Box<dyn Error>> {
180        if !self.config.cache_enabled {
181            return Ok(());
182        }
183        let cache_path = self.get_cache_path(url)?;
184        if let Some(parent) = cache_path.parent() {
185            fs::create_dir_all(parent)?;
186        }
187        let mut file = File::create(cache_path)?;
188        file.write_all(data)?;
189        Ok(())
190    }
191
192    /// Generate the cache file path based on the URL
193    ///
194    /// Uses a simple hash to generate a unique filename
195    #[allow(dead_code)]
196    fn get_cache_path(&self, url: &str) -> Result<PathBuf, Box<dyn Error>> {
197        let mut cache_dir = PathBuf::from(if self.config.cache_dir.is_empty() {
198            "~/.cache/kget".to_string()
199        } else {
200            self.config.cache_dir.clone()
201        });
202
203        if cache_dir.starts_with("~") {
204            if let Some(home) = dirs::home_dir() {
205                cache_dir = home.join(cache_dir.strip_prefix("~").unwrap());
206            }
207        }
208
209        // Simple hash function to generate a unique filename
210        let mut hash = 0u64;
211        for byte in url.bytes() {
212            hash = hash.wrapping_mul(31).wrapping_add(byte as u64);
213        }
214
215        cache_dir.push(format!("{:x}", hash));
216        Ok(cache_dir)
217    }
218
219    /// Get the peer connection limit for torrent downloads.
220    ///
221    /// Uses the speed limit as a proxy for connection capacity.
222    /// Returns 50 if no speed limit is set.
223    pub fn get_peer_limit(&self) -> usize {
224        self.speed_limit.unwrap_or(50) as usize
225    }
226
227    /// Maximum parallel HTTP connections to use for advanced downloads.
228    pub fn max_connections(&self) -> usize {
229        self.config.max_connections.clamp(1, 32)
230    }
231
232    /// Check if compression is enabled.
233    pub fn is_compression_enabled(&self) -> bool {
234        self.config.compression
235    }
236
237    /// Create a new `Optimizer` with default settings.
238    ///
239    /// Equivalent to `Optimizer::default()`.
240    pub fn new() -> Self {
241        Self::default()
242    }
243
244    /// Deprecated alias for `from_config`. Use `Optimizer::from_config()` instead.
245    #[doc(hidden)]
246    pub fn with_config(config: OptimizationConfig) -> Self {
247        Self::from_config(config)
248    }
249}
250
251impl Default for Optimizer {
252    /// Create an `Optimizer` with sensible defaults:
253    /// - Compression enabled at level 6 (LZ4)
254    /// - Caching enabled in ~/.cache/kget
255    /// - No speed limit
256    /// - 4 max connections
257    fn default() -> Self {
258        Self {
259            config: OptimizationConfig {
260                compression: true,
261                compression_level: 6,
262                cache_enabled: true,
263                cache_dir: "~/.cache/kget".to_string(),
264                speed_limit: None,
265                max_connections: 4,
266            },
267            speed_limit: None,
268        }
269    }
270}