Skip to main content

romm_api/core/download/manager/
rom.rs

1use 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}