diff options
| author | 2026-01-13 15:56:20 +0800 | |
|---|---|---|
| committer | 2026-01-13 15:56:20 +0800 | |
| commit | 66f7825ed9638606665b9e61c6f8132de013da14 (patch) | |
| tree | 764c30309a1c5a77f3b0d92d5131b1b50ae50402 /src-tauri | |
| parent | 6fbb5f19cab02f3a0f18cdeda3da02e717b69cd6 (diff) | |
| download | DropOut-66f7825ed9638606665b9e61c6f8132de013da14.tar.gz DropOut-66f7825ed9638606665b9e61c6f8132de013da14.zip | |
feat: implement download functionality with progress monitoring and version management
Diffstat (limited to 'src-tauri')
| -rw-r--r-- | src-tauri/Cargo.toml | 3 | ||||
| -rw-r--r-- | src-tauri/src/core/downloader.rs | 133 | ||||
| -rw-r--r-- | src-tauri/src/core/game_version.rs | 66 | ||||
| -rw-r--r-- | src-tauri/src/core/mod.rs | 1 | ||||
| -rw-r--r-- | src-tauri/src/main.rs | 64 |
5 files changed, 221 insertions, 46 deletions
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0091ed4..1c81ade 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -14,6 +14,9 @@ serde_json = "1.0.149" tauri = { version = "2.0.0", features = [] } tauri-plugin-shell = "2.0.0" uuid = { version = "1.10.0", features = ["v3", "v4", "serde"] } +futures = "0.3" +sha1 = "0.10" +hex = "0.4" [build-dependencies] tauri-build = { version = "2.0.0", features = [] } diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs index 8d717be..0ba9aec 100644 --- a/src-tauri/src/core/downloader.rs +++ b/src-tauri/src/core/downloader.rs @@ -1,8 +1,12 @@ use std::path::PathBuf; -use tokio::sync::mpsc; use serde::{Serialize, Deserialize}; +use tauri::{Emitter, Window}; +use futures::StreamExt; +use tokio::io::AsyncWriteExt; +use std::sync::Arc; +use tokio::sync::Semaphore; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct DownloadTask { pub url: String, pub path: PathBuf, @@ -10,42 +14,115 @@ pub struct DownloadTask { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DownloadProgress { - Started(String), - Progress { file: String, downloaded: u64, total: u64 }, - Finished(String), - Error(String, String), -} - -pub struct Downloader { - sender: mpsc::Sender<DownloadProgress>, +pub struct ProgressEvent { + pub file: String, + pub downloaded: u64, + pub total: u64, + pub status: String, // "Downloading", "Verifying", "Finished", "Error" } -impl Downloader { - pub fn new(sender: mpsc::Sender<DownloadProgress>) -> Self { - Self { sender } - } +pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<(), String> { + let client = reqwest::Client::new(); + let semaphore = Arc::new(Semaphore::new(10)); // Max 10 concurrent downloads + + // Notify start (total files) + let _ = window.emit("download-start", tasks.len()); - pub async fn download(&self, tasks: Vec<DownloadTask>) { - // TODO: Implement parallel download with limits - // Use futures::stream::StreamExt::buffer_unordered + let tasks_stream = futures::stream::iter(tasks).map(|task| { + let client = client.clone(); + let window = window.clone(); + let semaphore = semaphore.clone(); - for task in tasks { - if let Err(_) = self.sender.send(DownloadProgress::Started(task.url.clone())).await { - break; + async move { + let _permit = semaphore.acquire().await.unwrap(); + let file_name = task.path.file_name().unwrap().to_string_lossy().to_string(); + + // 1. Check if file exists and verify SHA1 + if task.path.exists() { + let _ = window.emit("download-progress", ProgressEvent { + file: file_name.clone(), + downloaded: 0, + total: 0, + status: "Verifying".into(), + }); + + if let Some(expected_sha1) = &task.sha1 { + if let Ok(data) = tokio::fs::read(&task.path).await { + let mut hasher = sha1::Sha1::new(); + use sha1::Digest; + hasher.update(&data); + let result = hex::encode(hasher.finalize()); + if &result == expected_sha1 { + // Already valid + let _ = window.emit("download-progress", ProgressEvent { + file: file_name.clone(), + downloaded: 0, + total: 0, + status: "Skipped".into(), + }); + return Ok(()); + } + } + } } - // Simulate download for now or implement basic - // Ensure directory exists + // 2. Download if let Some(parent) = task.path.parent() { let _ = tokio::fs::create_dir_all(parent).await; } - // Real implementation would use reqwest here - - if let Err(_) = self.sender.send(DownloadProgress::Finished(task.url)).await { - break; + match client.get(&task.url).send().await { + Ok(resp) => { + let total_size = resp.content_length().unwrap_or(0); + let mut file = match tokio::fs::File::create(&task.path).await { + Ok(f) => f, + Err(e) => return Err(format!("Create file error: {}", e)), + }; + + // reqwest::Response::bytes_stream() is only available if the 'stream' feature is enabled + // But we used 'blocking' and 'json'. We should add 'stream' feature to Cargo.toml? + // Or just use chunk(). + // Actually, let's just create a loop if stream feature is missing or use chunk() manually if blocking is used? + // Wait, we are in async context. 'reqwest' dependency in Cargo.toml has 'json', 'blocking' + // We need 'stream' feature for .bytes_stream() + + // Let's use loop with chunk() + loop { + match resp.chunk().await { + Ok(Some(chunk)) => { + if let Err(e) = file.write_all(&chunk).await { + return Err(format!("Write error: {}", e)); + } + downloaded += chunk.len() as u64; + let _ = window.emit("download-progress", ProgressEvent { + file: file_name.clone(), + downloaded, + total: total_size, + status: "Downloading".into(), + }); + } + Ok(None) => break, + Err(e) => return Err(format!("Download error: {}", e)), + } + } + }, + Err(e) => return Err(format!("Request error: {}", e)), } + + let _ = window.emit("download-progress", ProgressEvent { + file: file_name.clone(), + downloaded: 0, + total: 0, + status: "Finished".into(), + }); + + Ok(()) } - } + }); + + // Buffer unordered to run concurrently + tasks_stream.buffer_unordered(10).collect::<Vec<Result<(), String>>>().await; + + let _ = window.emit("download-complete", ()); + Ok(()) } diff --git a/src-tauri/src/core/game_version.rs b/src-tauri/src/core/game_version.rs new file mode 100644 index 0000000..c33f99c --- /dev/null +++ b/src-tauri/src/core/game_version.rs @@ -0,0 +1,66 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct GameVersion { + pub id: String, + pub downloads: Downloads, + #[serde(rename = "assetIndex")] + pub asset_index: AssetIndex, + pub libraries: Vec<Library>, + #[serde(rename = "mainClass")] + pub main_class: String, + #[serde(rename = "minecraftArguments")] + pub minecraft_arguments: Option<String>, + pub arguments: Option<Arguments>, + #[serde(rename = "javaVersion")] + pub java_version: Option<JavaVersion>, +} + +#[derive(Debug, Deserialize)] +pub struct Downloads { + pub client: DownloadArtifact, + pub server: Option<DownloadArtifact>, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct DownloadArtifact { + pub sha1: String, + pub size: u64, + pub url: String, + pub path: Option<String>, +} + +#[derive(Debug, Deserialize)] +pub struct AssetIndex { + pub id: String, + pub sha1: String, + pub size: u64, + pub url: String, + #[serde(rename = "totalSize")] + pub total_size: u64, +} + +#[derive(Debug, Deserialize)] +pub struct Library { + pub downloads: Option<LibraryDownloads>, + pub name: String, +} + +#[derive(Debug, Deserialize)] +pub struct LibraryDownloads { + pub artifact: Option<DownloadArtifact>, + pub classifiers: Option<serde_json::Value>, // Complex, simplifying for now +} + +#[derive(Debug, Deserialize)] +pub struct Arguments { + pub game: Option<serde_json::Value>, + pub jvm: Option<serde_json::Value>, +} + +#[derive(Debug, Deserialize)] +pub struct JavaVersion { + pub component: String, + #[serde(rename = "majorVersion")] + pub major_version: u64, +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 320ab82..aaa14a6 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,4 @@ pub mod manifest; pub mod auth; pub mod downloader; +pub mod game_version; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 402f58f..6623802 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,28 +1,50 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use tauri::State; +use tauri::{State, Window}; // Added Window mod core; mod launcher; #[tauri::command] -async fn start_game() -> Result<String, String> { - println!("Backend received StartGame"); - match core::manifest::fetch_version_manifest().await { - Ok(manifest) => { - let msg = format!( - "Fetched manifest.\nLatest release: {}\nLatest snapshot: {}", - manifest.latest.release, manifest.latest.snapshot - ); - println!("{}", msg); - Ok(msg) - } - Err(e) => { - eprintln!("Error fetching manifest: {}", e); - Err(e.to_string()) - } - } +async fn start_game( + window: Window, + version_id: String +) -> Result<String, String> { + println!("Backend received StartGame for {}", version_id); + + // 1. Fetch manifest to find the version URL + let manifest = core::manifest::fetch_version_manifest().await.map_err(|e| e.to_string())?; + + // Find the version info + let version_info = manifest.versions.iter().find(|v| v.id == version_id) + .ok_or_else(|| format!("Version {} not found in manifest", version_id))?; + + // 2. Fetch specific version JSON (client.jar info) + let version_url = &version_info.url; + let version_details: core::game_version::GameVersion = reqwest::get(version_url) + .await.map_err(|e| e.to_string())? + .json().await.map_err(|e| e.to_string())?; + + // 3. Prepare download task for Client Jar + let client_jar = version_details.downloads.client; + // Where to save? Let's use ./versions/{version_id}/{version_id}.jar + let mut path = std::path::PathBuf::from("versions"); + path.push(&version_id); + path.push(format!("{}.jar", version_id)); + + let task = core::downloader::DownloadTask { + url: client_jar.url, + path, + sha1: Some(client_jar.sha1), + }; + + println!("Starting download of client jar..."); + + // 4. Start Download + core::downloader::download_files(window, vec![task]).await.map_err(|e| e.to_string())?; + + Ok(format!("Download complete for {}", version_id)) } #[tauri::command] @@ -55,11 +77,17 @@ async fn get_active_account( Ok(state.active_account.lock().unwrap().clone()) } +#[tauri::command] +async fn logout(state: State<'_, core::auth::AccountState>) -> Result<(), String> { + *state.active_account.lock().unwrap() = None; + Ok(()) +} + fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .manage(core::auth::AccountState::new()) - .invoke_handler(tauri::generate_handler![start_game, get_versions, login_offline, get_active_account]) + .invoke_handler(tauri::generate_handler![start_game, get_versions, login_offline, get_active_account, logout]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } |