diff --git a/src-tauri/src/instance/commands.rs b/src-tauri/src/instance/commands.rs index 0345c1532..c3041f2a8 100644 --- a/src-tauri/src/instance/commands.rs +++ b/src-tauri/src/instance/commands.rs @@ -949,8 +949,8 @@ pub async fn create_instance( .ok_or(InstanceError::ClientJsonParseError)?; task_params.push(PTaskParam::Download(DownloadParam { - src: Url::parse(&client_download_info.url.clone()) - .map_err(|_| InstanceError::ClientJsonParseError)?, + src: vec![Url::parse(&client_download_info.url.clone()) + .map_err(|_| InstanceError::ClientJsonParseError)?], dest: instance.version_path.join(format!("{}.jar", name)), filename: None, sha1: Some(client_download_info.sha1.clone()), diff --git a/src-tauri/src/instance/helpers/loader/fabric.rs b/src-tauri/src/instance/helpers/loader/fabric.rs index b7d50fffc..0e621074f 100644 --- a/src-tauri/src/instance/helpers/loader/fabric.rs +++ b/src-tauri/src/instance/helpers/loader/fabric.rs @@ -86,7 +86,7 @@ pub async fn install_fabric_loader( &priority[0], )?; task_params.push(PTaskParam::Download(DownloadParam { - src, + src: vec![src], dest: lib_dir.join(&rel), filename: None, sha1: None, diff --git a/src-tauri/src/instance/helpers/loader/forge.rs b/src-tauri/src/instance/helpers/loader/forge.rs index a0f59c8e6..e9387b6da 100644 --- a/src-tauri/src/instance/helpers/loader/forge.rs +++ b/src-tauri/src/instance/helpers/loader/forge.rs @@ -82,7 +82,7 @@ pub async fn install_forge_loader( let installer_path = lib_dir.join(&installer_rel); task_params.push(PTaskParam::Download(DownloadParam { - src: installer_url, + src: vec![installer_url], dest: installer_path.clone(), filename: None, sha1: None, @@ -218,7 +218,7 @@ pub async fn download_forge_libraries( if let Some(mojmaps) = args_map.get("{MOJMAPS}") { if let Some(client_mappings) = client_info.downloads.get("client_mappings") { task_params.push(PTaskParam::Download(DownloadParam { - src: client_mappings.url.parse()?, + src: vec![client_mappings.url.parse()?], dest: lib_dir.join(mojmaps), filename: None, sha1: Some(client_mappings.sha1.clone()), @@ -289,7 +289,7 @@ pub async fn download_forge_libraries( } task_params.push(PTaskParam::Download(DownloadParam { - src: convert_url_to_target_source( + src: vec![convert_url_to_target_source( &Url::parse(url)?, &[ ResourceType::ForgeMaven, @@ -297,7 +297,7 @@ pub async fn download_forge_libraries( ResourceType::Libraries, ], &priority[0], - )?, + )?], dest: lib_dir.join(&convert_library_name_to_path(name, None)?), filename: None, sha1: None, @@ -345,7 +345,7 @@ pub async fn download_forge_libraries( let rel = convert_library_name_to_path(&name.to_string(), None)?; task_params.push(PTaskParam::Download(DownloadParam { - src: convert_url_to_target_source( + src: vec![convert_url_to_target_source( &Url::parse(url)?, &[ ResourceType::ForgeMaven, @@ -353,7 +353,7 @@ pub async fn download_forge_libraries( ResourceType::Libraries, ], &priority[0], - )?, + )?], dest: lib_dir.join(&rel), filename: None, sha1: None, @@ -425,7 +425,7 @@ pub async fn download_forge_libraries( &priority[0], )?; task_params.push(PTaskParam::Download(DownloadParam { - src, + src: vec![src], dest: lib_dir.join(&rel), filename: None, sha1: None, diff --git a/src-tauri/src/instance/helpers/loader/neoforge.rs b/src-tauri/src/instance/helpers/loader/neoforge.rs index 21feffe4c..330c6da25 100644 --- a/src-tauri/src/instance/helpers/loader/neoforge.rs +++ b/src-tauri/src/instance/helpers/loader/neoforge.rs @@ -59,7 +59,7 @@ pub async fn install_neoforge_loader( let installer_path = lib_dir.join(&installer_rel); task_params.push(PTaskParam::Download(DownloadParam { - src: installer_url, + src: vec![installer_url], dest: installer_path.clone(), filename: None, sha1: None, @@ -198,7 +198,7 @@ pub async fn download_neoforge_libraries( if let Some(mojmaps) = args_map.get("{MOJMAPS}") { if let Some(client_mappings) = client_info.downloads.get("client_mappings") { task_params.push(PTaskParam::Download(DownloadParam { - src: client_mappings.url.parse()?, + src: vec![client_mappings.url.parse()?], dest: lib_dir.join(mojmaps), filename: None, sha1: Some(client_mappings.sha1.clone()), @@ -269,11 +269,11 @@ pub async fn download_neoforge_libraries( } task_params.push(PTaskParam::Download(DownloadParam { - src: convert_url_to_target_source( + src: vec![convert_url_to_target_source( &Url::parse(url)?, &[ResourceType::NeoforgeMaven, ResourceType::Libraries], &priority[0], - )?, + )?], dest: lib_dir.join(&convert_library_name_to_path(name, None)?), filename: None, sha1: None, @@ -317,11 +317,11 @@ pub async fn download_neoforge_libraries( let rel = convert_library_name_to_path(&name.to_string(), None)?; task_params.push(PTaskParam::Download(DownloadParam { - src: convert_url_to_target_source( + src: vec![convert_url_to_target_source( &Url::parse(url)?, &[ResourceType::NeoforgeMaven, ResourceType::Libraries], &priority[0], - )?, + )?], dest: lib_dir.join(&rel), filename: None, sha1: None, diff --git a/src-tauri/src/instance/helpers/modpack/curseforge.rs b/src-tauri/src/instance/helpers/modpack/curseforge.rs index 3388b6ca0..f28dbdc53 100644 --- a/src-tauri/src/instance/helpers/modpack/curseforge.rs +++ b/src-tauri/src/instance/helpers/modpack/curseforge.rs @@ -190,7 +190,7 @@ impl ModpackManifest for CurseForgeManifest { .map(|h| h.value.clone()); let task_param = PTaskParam::Download(DownloadParam { - src: url::Url::parse(&download_url).map_err(|_| InstanceError::InvalidSourcePath)?, + src: vec![url::Url::parse(&download_url).map_err(|_| InstanceError::InvalidSourcePath)?], sha1, dest: instance_path .join(match class_id { diff --git a/src-tauri/src/instance/helpers/modpack/modrinth.rs b/src-tauri/src/instance/helpers/modpack/modrinth.rs index d3d7874aa..0fc2df0cb 100644 --- a/src-tauri/src/instance/helpers/modpack/modrinth.rs +++ b/src-tauri/src/instance/helpers/modpack/modrinth.rs @@ -108,12 +108,16 @@ impl ModpackManifest for ModrinthManifest { .files .iter() .map(|file| { - let download_url = file + let urls = file .downloads - .first() - .ok_or(InstanceError::InvalidSourcePath)?; + .iter() + .filter_map(|u| url::Url::parse(u).ok()) + .collect::>(); + if urls.is_empty() { + return Err(InstanceError::InvalidSourcePath.into()); + } Ok(PTaskParam::Download(DownloadParam { - src: url::Url::parse(download_url).map_err(|_| InstanceError::InvalidSourcePath)?, + src: urls, sha1: Some(file.hashes.sha1.clone()), dest: instance_path.join(&file.path), filename: None, diff --git a/src-tauri/src/launch/helpers/file_validator.rs b/src-tauri/src/launch/helpers/file_validator.rs index b7743ec12..78c1cf97b 100644 --- a/src-tauri/src/launch/helpers/file_validator.rs +++ b/src-tauri/src/launch/helpers/file_validator.rs @@ -94,7 +94,7 @@ pub async fn get_invalid_library_files( &source, )?; Ok(Some(PTaskParam::Download(DownloadParam { - src, + src: vec![src], dest: file_path, filename: None, sha1: Some(artifact.sha1.clone()), @@ -339,7 +339,7 @@ pub async fn get_invalid_assets( .join(&path_in_repo) .map_err(crate::error::SJMCLError::from)?; Ok(Some(PTaskParam::Download(DownloadParam { - src, + src: vec![src], dest, filename: None, sha1: Some(item.hash.clone()), diff --git a/src-tauri/src/launcher_config/helpers/java.rs b/src-tauri/src/launcher_config/helpers/java.rs index 5fff95142..880efb35f 100644 --- a/src-tauri/src/launcher_config/helpers/java.rs +++ b/src-tauri/src/launcher_config/helpers/java.rs @@ -514,7 +514,7 @@ pub async fn build_mojang_java_download_params( let (url, sha1) = (raw["url"].as_str()?, raw["sha1"].as_str()?); Some(PTaskParam::Download(DownloadParam { - src: url.parse().ok()?, + src: vec![url.parse().ok()?], dest: runtime_dir.join(path), filename: None, sha1: Some(sha1.into()), diff --git a/src-tauri/src/launcher_config/helpers/updater.rs b/src-tauri/src/launcher_config/helpers/updater.rs index 838ca5f62..ecdcc2de2 100644 --- a/src-tauri/src/launcher_config/helpers/updater.rs +++ b/src-tauri/src/launcher_config/helpers/updater.rs @@ -144,7 +144,7 @@ pub async fn download_target_version( app.clone(), format!("launcher-update?{}", fname), vec![PTaskParam::Download(DownloadParam { - src: url::Url::parse(&url).map_err(|_| LauncherConfigError::FetchError)?, + src: vec![url::Url::parse(&url).map_err(|_| LauncherConfigError::FetchError)?], dest: download_cache_dir.join(&fname), filename: Some(fname), sha1: None, diff --git a/src-tauri/src/resource/commands.rs b/src-tauri/src/resource/commands.rs index 50b8c4d19..4f8ff2599 100644 --- a/src-tauri/src/resource/commands.rs +++ b/src-tauri/src/resource/commands.rs @@ -135,7 +135,7 @@ pub async fn download_game_server( app, format!("game-server?{}", resource_info.id), vec![PTaskParam::Download(DownloadParam { - src: url::Url::parse(&download_info.url.clone()).map_err(|_| ResourceError::ParseError)?, + src: vec![url::Url::parse(&download_info.url.clone()).map_err(|_| ResourceError::ParseError)?], dest: dest.clone().into(), filename: None, sha1: Some(download_info.sha1.clone()), @@ -184,7 +184,7 @@ pub async fn update_mods( for query in &queries { let file_path = mods_dir.join(&query.file_name); let download_param = DownloadParam { - src: url::Url::parse(&query.url).map_err(|_| ResourceError::ParseError)?, + src: vec![url::Url::parse(&query.url).map_err(|_| ResourceError::ParseError)?], dest: file_path, filename: None, sha1: Some(query.sha1.clone()), diff --git a/src-tauri/src/resource/helpers/modrinth/mod.rs b/src-tauri/src/resource/helpers/modrinth/mod.rs index 23db47245..5e0e9a3a4 100644 --- a/src-tauri/src/resource/helpers/modrinth/mod.rs +++ b/src-tauri/src/resource/helpers/modrinth/mod.rs @@ -223,7 +223,7 @@ pub async fn get_latest_fabric_api_mod_download( let dest_path = mods_dir.join(&filename); Ok(Some(DownloadParam { - src: download_url, + src: vec![download_url], dest: dest_path, filename: Some(filename), sha1: Some(latest_file.sha1.clone()), diff --git a/src-tauri/src/tasks/download.rs b/src-tauri/src/tasks/download.rs index 50f8ad7fb..754a85663 100644 --- a/src-tauri/src/tasks/download.rs +++ b/src-tauri/src/tasks/download.rs @@ -5,7 +5,6 @@ use crate::tasks::streams::reporter::Reporter; use crate::tasks::streams::ProgressStream; use crate::tasks::*; use crate::utils::fs::validate_sha1; -use crate::utils::web::with_retry; use async_speed_limit::Limiter; use futures::stream::TryStreamExt; use futures::StreamExt; @@ -25,7 +24,7 @@ use tokio_util::compat::FuturesAsyncReadCompatExt; #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct DownloadParam { - pub src: Url, + pub src: Vec, pub dest: PathBuf, pub filename: Option, pub sha1: Option, @@ -122,15 +121,15 @@ impl DownloadTask { async fn send_request( app_handle: &AppHandle, current: i64, - param: &DownloadParam, + src: Url, ) -> SJMCLResult { let state = app_handle.state::(); - let client = with_retry(state.inner().clone()); + let client = state.inner().clone(); let request = if current == 0 { - client.get(param.src.clone()) + client.get(src.clone()) } else { client - .get(param.src.clone()) + .get(src.clone()) .header(RANGE, format!("bytes={current}-")) }; @@ -149,14 +148,17 @@ impl DownloadTask { async fn create_resp_stream( app_handle: &AppHandle, current: i64, - param: &DownloadParam, + src: Url, ) -> SJMCLResult<( impl Stream> + Send, i64, )> { - let resp = Self::send_request(app_handle, current, param).await?; + let resp = Self::send_request(app_handle, current, src.clone()).await?; let total_progress = if current == 0 { - resp.content_length().unwrap() as i64 + match resp.content_length() { + Some(len) => len as i64, + None => -1, + } } else { -1 }; @@ -177,42 +179,75 @@ impl DownloadTask { impl Future> + Send, Arc>, )> { - let current = self.p_handle.desc.current; let handle = Arc::new(RwLock::new(self.p_handle)); let task_handle = handle.clone(); let param = self.param.clone(); Ok(( async move { - let (resp, total_progress) = Self::create_resp_stream(&app_handle, current, ¶m).await?; - let stream = ProgressStream::new(resp, task_handle.clone()); tokio::fs::create_dir_all(&self.dest_path.parent().unwrap()).await?; - let mut file = if current == 0 { - tokio::fs::File::create(&self.dest_path).await? - } else { - let mut f = tokio::fs::OpenOptions::new().open(&self.dest_path).await?; - f.seek(std::io::SeekFrom::Start(current as u64)).await?; - f - }; - { - let mut task_handle = task_handle.write().unwrap(); - task_handle.set_total(total_progress); - task_handle.mark_started(); - } - if let Some(lim) = limiter { - tokio::io::copy(&mut lim.limit(stream.into_async_read()).compat(), &mut file).await?; - } else { - tokio::io::copy(&mut stream.into_async_read().compat(), &mut file).await?; + let mut last_err: Option = None; + if param.src.is_empty() { + return Err(SJMCLError("No download sources provided".into())); } - drop(file); - if task_handle.read().unwrap().status().is_cancelled() { - tokio::fs::remove_file(&self.dest_path).await?; - Ok(()) - } else { - match param.sha1 { - Some(truth) => validate_sha1(param.dest, truth), - None => Ok(()), + for src in param.src.clone() { + let attempt = async { + let current = task_handle.read().unwrap().desc.current; + let (resp, total_progress) = + Self::create_resp_stream(&app_handle, current, src).await?; + let stream = ProgressStream::new(resp, task_handle.clone()); + let mut file = if current == 0 { + tokio::fs::File::create(&self.dest_path).await? + } else { + let mut f = tokio::fs::OpenOptions::new() + .write(true) + .open(&self.dest_path) + .await?; + f.seek(std::io::SeekFrom::Start(current as u64)).await?; + f + }; + { + let mut h = task_handle.write().unwrap(); + h.set_total(total_progress); + h.mark_started(); + } + if let Some(lim) = limiter.clone() { + tokio::io::copy(&mut lim.limit(stream.into_async_read()).compat(), &mut file).await?; + } else { + tokio::io::copy(&mut stream.into_async_read().compat(), &mut file).await?; + } + drop(file); + if task_handle.read().unwrap().status().is_cancelled() { + tokio::fs::remove_file(&self.dest_path).await?; + Ok(()) + } else { + match ¶m.sha1 { + Some(truth) => validate_sha1(self.dest_path.clone(), truth.clone()), + None => Ok(()), + } + } + } + .await; + + match attempt { + Ok(()) => { + if !task_handle.read().unwrap().status().is_cancelled() { + let mut h = task_handle.write().unwrap(); + h.mark_completed(); + } + return Ok(()); + } + Err(e) => { + last_err = Some(e); + let _ = tokio::fs::remove_file(&self.dest_path).await; + { + let mut h = task_handle.write().unwrap(); + h.desc.current = 0; + } + continue; + } } } + Err(last_err.unwrap_or_else(|| SJMCLError("All sources failed".into()))) }, handle, )) diff --git a/src-tauri/src/tasks/streams/mod.rs b/src-tauri/src/tasks/streams/mod.rs index a9d4cbf14..46e8b1107 100644 --- a/src-tauri/src/tasks/streams/mod.rs +++ b/src-tauri/src/tasks/streams/mod.rs @@ -71,8 +71,6 @@ where let mut h = p.handle.write().unwrap(); if let Some(item) = &opt { h.report_progress(cx, item.unit_size()); - } else { - h.mark_completed(); } opt }) diff --git a/src/components/modals/download-specific-resource-modal.tsx b/src/components/modals/download-specific-resource-modal.tsx index b75cc4578..88c540789 100644 --- a/src/components/modals/download-specific-resource-modal.tsx +++ b/src/components/modals/download-specific-resource-modal.tsx @@ -206,7 +206,7 @@ const DownloadSpecificResourceModal: React.FC< if (!savepath) return; handleScheduleProgressiveTaskGroup(resource.type, [ { - src: item.downloadUrl, + src: [item.downloadUrl], dest: savepath, sha1: item.sha1, taskType: TaskTypeEnums.Download, diff --git a/src/models/task.ts b/src/models/task.ts index 7ad2b0fb5..209991825 100644 --- a/src/models/task.ts +++ b/src/models/task.ts @@ -6,7 +6,7 @@ export type TaskType = `${TaskTypeEnums}`; export interface DownloadTaskParam { taskType: TaskTypeEnums.Download; - src: string; + src: string[]; dest: string; // destination path filename?: string; // destination filename sha1?: string; @@ -16,7 +16,7 @@ export type TaskParam = DownloadTaskParam; export interface DownloadTaskPayload { taskType: TaskTypeEnums.Download; - src: string; + src: string[]; dest: string; // destination path filename: string; // destination filename sha1: string; diff --git a/src/pages/settings/dev-test.tsx b/src/pages/settings/dev-test.tsx index acaa0eb32..856f95aa2 100644 --- a/src/pages/settings/dev-test.tsx +++ b/src/pages/settings/dev-test.tsx @@ -63,7 +63,9 @@ const DevTestPage = () => { console.log("Download button clicked"); let dl: DownloadTaskParam[] = [ { - src: "https://edge.forgecdn.net/files/3045/381/%5B___MixinCompat-0.8___%5D.jar", + src: [ + "https://edge.forgecdn.net/files/3045/381/%5B___MixinCompat-0.8___%5D.jar", + ], dest: "D:\\mods\\[___MixinCompat-0.8___].jar", taskType: TaskTypeEnums.Download, },