Skip to main content

modde_sources/
manager.rs

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