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}