romm_api/core/download/manager/
rom.rs1use std::path::PathBuf;
2use std::sync::{Arc, Mutex};
3
4use crate::client::RommClient;
5use crate::config::RomsLayoutConfig;
6use crate::core::extras::{build_base_rom_file_targets, DownloadTarget};
7use crate::core::interrupt::is_cancelled_download;
8use crate::core::utils;
9use crate::error::DownloadError;
10use crate::types::Rom;
11
12use super::super::job::{DownloadJob, DownloadStatus};
13use super::super::paths::{resolve_console_roms_dir, resolve_download_directory};
14use super::super::transfer::{
15 download_target_with_fallback, finalize_download, prepare_download_target_destination,
16 sanitized_final_filename, FinalizeResult,
17};
18use super::DownloadManager;
19
20struct RomDownloadTask {
21 client: RommClient,
22 jobs: Arc<Mutex<Vec<DownloadJob>>>,
23 job_id: usize,
24 rom_id: u64,
25 fs_name: String,
26 final_name: String,
27 save_dir: PathBuf,
28 console_dir: PathBuf,
29 base_targets: Vec<DownloadTarget>,
30}
31
32impl DownloadManager {
33 pub fn start_download(
34 &self,
35 rom: &Rom,
36 client: RommClient,
37 layout: &RomsLayoutConfig,
38 configured_download_dir: Option<&str>,
39 ) -> Result<(), DownloadError> {
40 let platform = rom
41 .platform_display_name
42 .as_deref()
43 .or(rom.platform_custom_name.as_deref())
44 .unwrap_or("—")
45 .to_string();
46
47 let job = DownloadJob::new(rom.id, rom.name.clone(), platform);
48 let job_id = job.id;
49 let rom_id = rom.id;
50 let fs_name = rom.fs_name.clone();
51 let final_name = sanitized_final_filename(&rom.fs_name, rom.id);
52 let rom_for_targets = rom.clone();
53 let layout = layout.clone();
54 match self.jobs.lock() {
55 Ok(mut jobs) => jobs.push(job),
56 Err(err) => {
57 eprintln!("warning: download job list lock poisoned: {}", err);
58 return Err(DownloadError::JobListPoisoned(err.to_string()));
59 }
60 }
61
62 let save_dir = resolve_download_directory(configured_download_dir)?;
63 let console_dir = resolve_console_roms_dir(&layout, &save_dir, rom)?;
64 let base_targets = build_base_rom_file_targets(&rom_for_targets, &layout, &save_dir)?;
65 let task = RomDownloadTask {
66 client,
67 jobs: self.jobs.clone(),
68 job_id,
69 rom_id,
70 fs_name,
71 final_name,
72 save_dir,
73 console_dir,
74 base_targets,
75 };
76 tokio::spawn(run_rom_download_task(task));
77 Ok(())
78 }
79}
80
81async fn run_rom_download_task(task: RomDownloadTask) {
82 let temp_root = task.save_dir.join(".tmp");
83 if let Err(err) = tokio::fs::create_dir_all(&temp_root).await {
84 set_job_status(
85 &task.jobs,
86 task.job_id,
87 DownloadStatus::Error(format!(
88 "Could not create temp directory {}: {err}",
89 temp_root.display()
90 )),
91 );
92 return;
93 }
94
95 let final_path = task.console_dir.join(task.final_name.clone());
96 if let Err(err) = tokio::fs::create_dir_all(&task.console_dir).await {
97 set_job_status(
98 &task.jobs,
99 task.job_id,
100 DownloadStatus::Error(format!(
101 "Could not create console directory {}: {err}",
102 task.console_dir.display()
103 )),
104 );
105 return;
106 }
107
108 if !task.base_targets.is_empty() {
109 download_base_targets(&task.client, &task.jobs, task.job_id, &task.base_targets).await;
110 return;
111 }
112
113 download_primary_rom(
114 &task.client,
115 &task.jobs,
116 task.job_id,
117 task.rom_id,
118 &task.fs_name,
119 &temp_root,
120 &final_path,
121 )
122 .await;
123}
124
125async fn download_base_targets(
126 client: &RommClient,
127 jobs: &Arc<Mutex<Vec<DownloadJob>>>,
128 job_id: usize,
129 base_targets: &[DownloadTarget],
130) {
131 let total_targets = base_targets.len() as f64;
132 for (idx, target) in base_targets.iter().enumerate() {
133 let progress_jobs = jobs.clone();
134 let mut progress = move |received: u64, total: u64| {
135 let file_ratio = if total > 0 {
136 received as f64 / total as f64
137 } else {
138 0.0
139 };
140 let total_ratio = ((idx as f64) + file_ratio) / total_targets;
141 set_job_progress(&progress_jobs, job_id, total_ratio.min(1.0));
142 };
143
144 match prepare_download_target_destination(target).await {
145 Ok(true) => {
146 progress(
147 target.expected_size_bytes.unwrap_or(0),
148 target.expected_size_bytes.unwrap_or(0),
149 );
150 continue;
151 }
152 Ok(false) => {}
153 Err(err) => {
154 set_job_status(jobs, job_id, DownloadStatus::Error(err.to_string()));
155 return;
156 }
157 }
158
159 if let Err(final_err) =
160 download_target_with_fallback(client, target, |_, _| false, &mut progress).await
161 {
162 set_job_status(jobs, job_id, DownloadStatus::Error(final_err.to_string()));
163 return;
164 }
165 }
166 finish_job(jobs, job_id, DownloadStatus::Done);
167}
168
169async fn download_primary_rom(
170 client: &RommClient,
171 jobs: &Arc<Mutex<Vec<DownloadJob>>>,
172 job_id: usize,
173 rom_id: u64,
174 fs_name: &str,
175 temp_root: &std::path::Path,
176 final_path: &std::path::Path,
177) {
178 if final_path.exists() {
179 finish_job(jobs, job_id, DownloadStatus::SkippedAlreadyExists);
180 return;
181 }
182
183 let temp_name = format!(
184 "rom-{}-{}-{}.part",
185 rom_id,
186 utils::sanitize_filename(fs_name),
187 job_id
188 );
189 let temp_path = temp_root.join(temp_name);
190 let progress_jobs = jobs.clone();
191 let on_progress = move |received: u64, total: u64| {
192 let p = if total > 0 {
193 received as f64 / total as f64
194 } else {
195 0.0
196 };
197
198 set_job_progress(&progress_jobs, job_id, p);
199 };
200
201 let download_result = client.download_rom(rom_id, &temp_path, on_progress).await;
202 if download_result.is_err() {
203 let _ = tokio::fs::remove_file(&temp_path).await;
204 }
205 match download_result {
206 Ok(()) => match finalize_download(&temp_path, final_path).await {
207 Ok(FinalizeResult::Done) => {
208 finish_job(jobs, job_id, DownloadStatus::Done);
209 }
210 Ok(FinalizeResult::SkippedAlreadyExists) => {
211 finish_job(jobs, job_id, DownloadStatus::SkippedAlreadyExists);
212 }
213 Err(err) => {
214 let _ = tokio::fs::remove_file(&temp_path).await;
215 set_job_status(
216 jobs,
217 job_id,
218 DownloadStatus::FinalizeFailed(err.to_string()),
219 );
220 }
221 },
222 Err(e) => {
223 if is_cancelled_download(&e) {
224 set_job_status(jobs, job_id, DownloadStatus::Cancelled);
225 } else {
226 set_job_status(jobs, job_id, DownloadStatus::Error(e.to_string()));
227 }
228 }
229 }
230}
231
232fn update_download_job<F>(jobs: &Arc<Mutex<Vec<DownloadJob>>>, job_id: usize, update: F)
233where
234 F: FnOnce(&mut DownloadJob),
235{
236 if let Ok(mut list) = jobs.lock() {
237 if let Some(job) = list.iter_mut().find(|job| job.id == job_id) {
238 update(job);
239 }
240 }
241}
242
243fn set_job_progress(jobs: &Arc<Mutex<Vec<DownloadJob>>>, job_id: usize, progress: f64) {
244 update_download_job(jobs, job_id, |job| {
245 job.progress = progress;
246 });
247}
248
249fn set_job_status(jobs: &Arc<Mutex<Vec<DownloadJob>>>, job_id: usize, status: DownloadStatus) {
250 update_download_job(jobs, job_id, |job| {
251 job.status = status;
252 });
253}
254
255fn finish_job(jobs: &Arc<Mutex<Vec<DownloadJob>>>, job_id: usize, status: DownloadStatus) {
256 update_download_job(jobs, job_id, |job| {
257 job.status = status;
258 job.progress = 1.0;
259 });
260}