gglib_core/ports/download_manager.rs
1//! Download manager port definition.
2//!
3//! This port defines the public interface for the download subsystem.
4//! It abstracts away all implementation details (Python subprocess,
5//! cancellation tokens, `HuggingFace` client) behind a clean async API.
6//!
7//! # Design
8//!
9//! - Only core download domain types in signatures
10//! - No Python types, `CancellationToken`, or HF types leak through
11//! - Consistent with other ports (`HfClientPort`, `McpServerRepository`)
12
13use async_trait::async_trait;
14use std::path::PathBuf;
15
16use crate::download::{DownloadError, DownloadId, Quantization, QueueSnapshot};
17
18/// Request to queue a new download.
19///
20/// This is a pure data structure containing all information needed
21/// to initiate a download. Infrastructure concerns (tokens, paths)
22/// are handled internally by the implementation.
23#[derive(Debug, Clone)]
24pub struct DownloadRequest {
25 /// Repository ID on `HuggingFace` (e.g., `unsloth/Llama-3-GGUF`).
26 pub repo_id: String,
27 /// The quantization to download.
28 pub quantization: Quantization,
29 /// Git revision/commit SHA (defaults to "main" if not specified).
30 pub revision: Option<String>,
31 /// Force re-download even if file exists locally.
32 pub force: bool,
33 /// Add to local model database after download.
34 pub add_to_db: bool,
35}
36
37impl DownloadRequest {
38 /// Create a new download request with required fields.
39 pub fn new(repo_id: impl Into<String>, quantization: Quantization) -> Self {
40 Self {
41 repo_id: repo_id.into(),
42 quantization,
43 revision: None,
44 force: false,
45 add_to_db: true,
46 }
47 }
48
49 /// Set the revision/commit SHA.
50 #[must_use]
51 pub fn with_revision(mut self, revision: impl Into<String>) -> Self {
52 self.revision = Some(revision.into());
53 self
54 }
55
56 /// Set whether to force re-download.
57 #[must_use]
58 pub const fn with_force(mut self, force: bool) -> Self {
59 self.force = force;
60 self
61 }
62
63 /// Set whether to add to database after download.
64 #[must_use]
65 pub const fn with_add_to_db(mut self, add_to_db: bool) -> Self {
66 self.add_to_db = add_to_db;
67 self
68 }
69}
70
71/// Configuration for creating a download manager.
72///
73/// Contains paths and limits that the download manager needs.
74/// Infrastructure-specific options are handled internally.
75#[derive(Debug, Clone)]
76pub struct DownloadManagerConfig {
77 /// Directory where models are stored.
78 pub models_directory: PathBuf,
79 /// Maximum concurrent downloads.
80 pub max_concurrent: u32,
81 /// Maximum queue size.
82 pub max_queue_size: u32,
83 /// `HuggingFace` authentication token (for private repos).
84 pub hf_token: Option<String>,
85}
86
87impl Default for DownloadManagerConfig {
88 fn default() -> Self {
89 Self {
90 models_directory: PathBuf::from("."),
91 max_concurrent: 1,
92 max_queue_size: 10,
93 hf_token: None,
94 }
95 }
96}
97
98impl DownloadManagerConfig {
99 /// Create a new config with the models directory.
100 #[must_use]
101 pub fn new(models_directory: PathBuf) -> Self {
102 Self {
103 models_directory,
104 ..Default::default()
105 }
106 }
107
108 /// Set the maximum concurrent downloads.
109 #[must_use]
110 pub const fn with_max_concurrent(mut self, max: u32) -> Self {
111 self.max_concurrent = max;
112 self
113 }
114
115 /// Set the maximum queue size.
116 #[must_use]
117 pub const fn with_max_queue_size(mut self, max: u32) -> Self {
118 self.max_queue_size = max;
119 self
120 }
121
122 /// Set the `HuggingFace` token.
123 #[must_use]
124 pub fn with_hf_token(mut self, token: Option<String>) -> Self {
125 self.hf_token = token;
126 self
127 }
128}
129
130/// Port for managing downloads.
131///
132/// This is the main interface for the download subsystem. Implementations
133/// handle all the complexity of queuing, progress tracking, cancellation,
134/// and model registration internally.
135///
136/// # Usage
137///
138/// ```ignore
139/// let manager: Arc<dyn DownloadManagerPort> = /* ... */;
140///
141/// // Queue a download
142/// let request = DownloadRequest::new("unsloth/Llama-3-GGUF", Quantization::Q4KM);
143/// let id = manager.queue_download(request).await?;
144///
145/// // Check status
146/// let snapshot = manager.get_queue_snapshot().await?;
147///
148/// // Cancel if needed
149/// manager.cancel_download(&id).await?;
150/// ```
151use std::sync::Arc;
152
153#[async_trait]
154pub trait DownloadManagerPort: Send + Sync {
155 /// Queue a new download.
156 ///
157 /// Returns the download ID which can be used to track or cancel the download.
158 /// The download will be processed according to the manager's concurrency settings.
159 async fn queue_download(&self, request: DownloadRequest) -> Result<DownloadId, DownloadError>;
160
161 /// Queue a download and ensure the queue processor is running.
162 ///
163 /// This is the recommended method for GUI adapters. It combines queuing
164 /// with worker lifecycle management, hiding the internal details of
165 /// how downloads are processed.
166 ///
167 /// The `self: Arc<Self>` receiver allows implementations to clone the
168 /// Arc and spawn worker tasks. This is object-safe and works with
169 /// `Arc<dyn DownloadManagerPort>`.
170 ///
171 /// Returns the download ID on success.
172 async fn queue_and_process(
173 self: Arc<Self>,
174 request: DownloadRequest,
175 ) -> Result<DownloadId, DownloadError>;
176
177 /// Queue a download with smart quantization selection.
178 ///
179 /// This is the recommended method for GUI adapters when the quantization
180 /// may be optional. It:
181 /// 1. Selects the best quantization if none specified
182 /// 2. Validates the requested quantization exists
183 /// 3. Queues the download and starts processing
184 ///
185 /// # Quantization Selection Rules
186 ///
187 /// - If a quantization is provided, validates it exists in the repository
188 /// - If none provided and 1 option exists, auto-picks it (pre-quantized model)
189 /// - If none provided and multiple exist, uses default preference order
190 /// - Returns error if requested quant not found or no suitable default
191 ///
192 /// # Arguments
193 ///
194 /// * `repo_id` - `HuggingFace` repository ID (e.g., "unsloth/Llama-3-GGUF")
195 /// * `quantization` - Optional quantization name (e.g., "`Q4_K_M`", "`Q8_0`")
196 ///
197 /// # Returns
198 ///
199 /// Returns (position, `shard_count`) on success.
200 async fn queue_smart(
201 self: Arc<Self>,
202 repo_id: String,
203 quantization: Option<String>,
204 ) -> Result<(usize, usize), DownloadError>;
205
206 /// Get a snapshot of the current queue state.
207 ///
208 /// Returns all queued, active, and recently completed/failed downloads.
209 /// This is used by UIs to display download status.
210 async fn get_queue_snapshot(&self) -> Result<QueueSnapshot, DownloadError>;
211
212 /// Cancel a download.
213 ///
214 /// If the download is queued, it's removed from the queue.
215 /// If the download is active, the underlying process is terminated.
216 /// Returns an error if the download ID is not found.
217 async fn cancel_download(&self, id: &DownloadId) -> Result<(), DownloadError>;
218
219 /// Cancel all active and queued downloads.
220 ///
221 /// This is used during application shutdown or when the user
222 /// wants to clear the queue.
223 async fn cancel_all(&self) -> Result<(), DownloadError>;
224
225 /// Check if a download with the given ID exists in the queue.
226 async fn has_download(&self, id: &DownloadId) -> Result<bool, DownloadError>;
227
228 /// Get the number of active downloads.
229 async fn active_count(&self) -> Result<u32, DownloadError>;
230
231 /// Get the number of pending (queued) downloads.
232 async fn pending_count(&self) -> Result<u32, DownloadError>;
233
234 // ─────────────────────────────────────────────────────────────────────────
235 // Queue management operations
236 // ─────────────────────────────────────────────────────────────────────────
237
238 /// Remove a pending download from the queue.
239 ///
240 /// This is for items that haven't started yet. For active downloads,
241 /// use `cancel_download` instead.
242 async fn remove_from_queue(&self, id: &DownloadId) -> Result<(), DownloadError>;
243
244 /// Reorder a download to a new position in the queue.
245 ///
246 /// The position is 1-based where 1 is next to run. Returns the actual
247 /// position assigned (may differ if requested position is out of bounds).
248 async fn reorder_queue(&self, id: &DownloadId, new_position: u32)
249 -> Result<u32, DownloadError>;
250
251 /// Cancel all downloads in a shard group.
252 ///
253 /// Used for canceling multi-file model downloads where shards are
254 /// queued together. The `group_id` matches `QueuedDownload.group_id`.
255 async fn cancel_group(&self, group_id: &str) -> Result<(), DownloadError>;
256
257 /// Retry a failed download.
258 ///
259 /// Moves the download from the failures list back to the queue.
260 /// Returns the position in queue where it was added.
261 async fn retry(&self, id: &DownloadId) -> Result<u32, DownloadError>;
262
263 /// Clear all failed downloads from the failures list.
264 async fn clear_failed(&self) -> Result<(), DownloadError>;
265
266 /// Update the maximum queue size.
267 ///
268 /// Downloads already in queue are not affected, but new downloads
269 /// may be rejected if the queue is at capacity.
270 async fn set_max_queue_size(&self, size: u32) -> Result<(), DownloadError>;
271
272 /// Get the maximum queue size.
273 async fn get_max_queue_size(&self) -> Result<u32, DownloadError>;
274}