Skip to main content

modde_sources/
manager.rs

1//! Synchronous download queue bookkeeping: track download state, enforce a
2//! concurrency budget, and surface which queued downloads may start next.
3
4use std::path::PathBuf;
5
6/// Tracks an active or completed download.
7#[derive(Debug, Clone)]
8pub struct ManagedDownload {
9    pub id: usize,
10    pub url: String,
11    pub dest: PathBuf,
12    pub mod_name: Option<String>,
13    pub state: DownloadState,
14}
15
16/// State of a managed download.
17#[derive(Debug, Clone)]
18pub enum DownloadState {
19    Queued,
20    Active { progress: f64 },
21    Paused { bytes: u64 },
22    Complete { path: PathBuf },
23    Failed { error: String },
24}
25
26/// Manages a queue of downloads with progress tracking.
27///
28/// This is a synchronous data structure -- callers drive the actual async
29/// downloads and update state through the provided methods.
30pub struct DownloadManager {
31    downloads: Vec<ManagedDownload>,
32    max_concurrent: usize,
33    next_id: usize,
34}
35
36impl DownloadManager {
37    /// Create an empty manager allowing up to `max_concurrent` active downloads.
38    #[must_use]
39    pub fn new(max_concurrent: usize) -> Self {
40        Self {
41            downloads: Vec::new(),
42            max_concurrent,
43            next_id: 0,
44        }
45    }
46
47    /// Add a download to the queue. Returns the assigned download ID.
48    pub fn enqueue(&mut self, url: String, dest: PathBuf, mod_name: Option<String>) -> usize {
49        let id = self.next_id;
50        self.next_id += 1;
51        self.downloads.push(ManagedDownload {
52            id,
53            url,
54            dest,
55            mod_name,
56            state: DownloadState::Queued,
57        });
58        id
59    }
60
61    /// Pause an active download, recording how many bytes were fetched so far.
62    pub fn pause(&mut self, id: usize, bytes_so_far: u64) {
63        if let Some(dl) = self.get_mut(id)
64            && matches!(dl.state, DownloadState::Active { .. })
65        {
66            dl.state = DownloadState::Paused {
67                bytes: bytes_so_far,
68            };
69        }
70    }
71
72    /// Move a paused download back to the queue.
73    pub fn resume(&mut self, id: usize) {
74        if let Some(dl) = self.get_mut(id)
75            && matches!(dl.state, DownloadState::Paused { .. })
76        {
77            dl.state = DownloadState::Queued;
78        }
79    }
80
81    /// Remove a download from the manager entirely.
82    pub fn cancel(&mut self, id: usize) {
83        self.downloads.retain(|dl| dl.id != id);
84    }
85
86    /// Number of currently active downloads.
87    #[must_use]
88    pub fn active_count(&self) -> usize {
89        self.downloads
90            .iter()
91            .filter(|dl| matches!(dl.state, DownloadState::Active { .. }))
92            .count()
93    }
94
95    /// Maximum number of concurrent downloads allowed.
96    #[must_use]
97    pub fn max_concurrent(&self) -> usize {
98        self.max_concurrent
99    }
100
101    /// View all tracked downloads.
102    #[must_use]
103    pub fn all(&self) -> &[ManagedDownload] {
104        &self.downloads
105    }
106
107    /// Get a mutable reference to a download by ID.
108    pub fn get_mut(&mut self, id: usize) -> Option<&mut ManagedDownload> {
109        self.downloads.iter_mut().find(|dl| dl.id == id)
110    }
111
112    /// Get an immutable reference to a download by ID.
113    #[must_use]
114    pub fn get(&self, id: usize) -> Option<&ManagedDownload> {
115        self.downloads.iter().find(|dl| dl.id == id)
116    }
117
118    /// Returns true if there is room to start another download.
119    #[must_use]
120    pub fn can_start_more(&self) -> bool {
121        self.active_count() < self.max_concurrent
122    }
123
124    /// Return IDs of queued downloads that could be activated, up to the
125    /// remaining concurrency budget.
126    #[must_use]
127    pub fn next_queued(&self) -> Vec<usize> {
128        let budget = self.max_concurrent.saturating_sub(self.active_count());
129        self.downloads
130            .iter()
131            .filter(|dl| matches!(dl.state, DownloadState::Queued))
132            .take(budget)
133            .map(|dl| dl.id)
134            .collect()
135    }
136}