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 with smart quantization selection.
162 ///
163 /// This is the recommended method for GUI adapters when the quantization
164 /// may be optional. It:
165 /// 1. Selects the best quantization if none specified
166 /// 2. Validates the requested quantization exists
167 /// 3. Queues the download and starts processing
168 ///
169 /// # Quantization Selection Rules
170 ///
171 /// - If a quantization is provided, validates it exists in the repository
172 /// - If none provided and 1 option exists, auto-picks it (pre-quantized model)
173 /// - If none provided and multiple exist, uses default preference order
174 /// - Returns error if requested quant not found or no suitable default
175 ///
176 /// # Arguments
177 ///
178 /// * `repo_id` - `HuggingFace` repository ID (e.g., "unsloth/Llama-3-GGUF")
179 /// * `quantization` - Optional quantization name (e.g., "`Q4_K_M`", "`Q8_0`")
180 ///
181 /// # Returns
182 ///
183 /// Returns (position, `shard_count`) on success.
184 async fn queue_smart(
185 self: Arc<Self>,
186 repo_id: String,
187 quantization: Option<String>,
188 ) -> Result<(usize, usize), DownloadError>;
189
190 /// Get a snapshot of the current queue state.
191 ///
192 /// Returns all queued, active, and recently completed/failed downloads.
193 /// This is used by UIs to display download status.
194 async fn get_queue_snapshot(&self) -> Result<QueueSnapshot, DownloadError>;
195
196 /// Cancel a download.
197 ///
198 /// If the download is queued, it's removed from the queue.
199 /// If the download is active, the underlying process is terminated.
200 /// Returns an error if the download ID is not found.
201 async fn cancel_download(&self, id: &DownloadId) -> Result<(), DownloadError>;
202
203 /// Cancel all active and queued downloads.
204 ///
205 /// This is used during application shutdown or when the user
206 /// wants to clear the queue.
207 async fn cancel_all(&self) -> Result<(), DownloadError>;
208
209 /// Get the number of active downloads.
210 async fn active_count(&self) -> Result<u32, DownloadError>;
211
212 // ─────────────────────────────────────────────────────────────────────────
213 // Queue management operations
214 // ─────────────────────────────────────────────────────────────────────────
215
216 /// Remove a pending download from the queue.
217 ///
218 /// This is for items that haven't started yet. For active downloads,
219 /// use `cancel_download` instead.
220 async fn remove_from_queue(&self, id: &DownloadId) -> Result<(), DownloadError>;
221
222 /// Reorder a download to a new position in the queue.
223 ///
224 /// The position is 1-based where 1 is next to run. Returns the actual
225 /// position assigned (may differ if requested position is out of bounds).
226 async fn reorder_queue(&self, id: &DownloadId, new_position: u32)
227 -> Result<u32, DownloadError>;
228
229 /// Cancel all downloads in a shard group.
230 ///
231 /// Used for canceling multi-file model downloads where shards are
232 /// queued together. The `group_id` matches `QueuedDownload.group_id`.
233 async fn cancel_group(&self, group_id: &str) -> Result<(), DownloadError>;
234
235 /// Clear all failed downloads from the failures list.
236 async fn clear_failed(&self) -> Result<(), DownloadError>;
237
238 /// Update the maximum queue size.
239 ///
240 /// Downloads already in queue are not affected, but new downloads
241 /// may be rejected if the queue is at capacity.
242 async fn set_max_queue_size(&self, size: u32) -> Result<(), DownloadError>;
243}