diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bc3d89a..5cac561 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2943,7 +2943,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.7.1", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -3064,6 +3064,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.32.0" @@ -3620,6 +3629,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -3745,15 +3765,18 @@ dependencies = [ "futures", "lazy_static", "pinyin", + "quick-xml 0.31.0", "reqwest", "serde", "serde_json", + "sha1", "tauri", "tauri-build", "tauri-plugin-clipboard-manager", "tauri-plugin-opener", "tokio", "tokio-macros", + "tokio-util", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 07dab98..b183780 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,10 +23,13 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12.12", features = ["json"] } -tokio = { version = "1.43.0", features = ["full"] } +tokio = { version = "1.43.0", features = ["full", "sync"] } tokio-macros = { version = "2.5.0" } +tokio-util = { version = "0.7.10", features = ["io"] } lazy_static = "1.5.0" futures = "0.3.31" pinyin = "0.10.0" tauri-plugin-clipboard-manager = "2.2.0" dirs = "6.0.0" +quick-xml = "0.31.0" +sha1 = "0.10.6" diff --git a/src-tauri/src/handlers/download.rs b/src-tauri/src/handlers/download.rs index e3fa047..1f4ce73 100644 --- a/src-tauri/src/handlers/download.rs +++ b/src-tauri/src/handlers/download.rs @@ -1,153 +1,28 @@ -use crate::models::download::{DownloadStatus, DownloadTask}; use tauri::State; -use std::sync::Mutex; -use std::collections::HashMap; -use crate::handlers::server::get_json_server_url; -use crate::utils::{format_icon_url, UA}; -pub type DownloadQueue = Mutex>; +use crate::{models::download::DownloadTask, utils::download_manager::DownloadManager}; #[tauri::command] -pub async fn get_downloads(queue: State<'_, DownloadQueue>) -> Result, String> { - let downloads = queue.lock().map_err(|e| e.to_string())?; - Ok(downloads.values().cloned().collect()) -} - -// 检查是否有正在下载的任务 -fn has_downloading_task(downloads: &HashMap) -> bool { - downloads.values().any(|task| matches!(task.status, DownloadStatus::Downloading)) -} - -// 获取下一个等待下载的任务 -fn get_next_queued_task(downloads: &mut HashMap) -> Option { - downloads - .iter() - .find(|(_, task)| matches!(task.status, DownloadStatus::Queued)) - .map(|(task_id, _)| task_id.clone()) -} - -// 开始下载下一个任务 -fn start_next_download(downloads: &mut HashMap) { - if let Some(task_id) = get_next_queued_task(downloads) { - if let Some(task) = downloads.get_mut(&task_id) { - task.status = DownloadStatus::Downloading; - } - } +pub async fn get_downloads(manager: State<'_, DownloadManager>) -> Result, String> { + manager.get_downloads() } #[tauri::command] -pub async fn add_download(category: String, pkgname: String, filename:String, name:String, queue: State<'_, DownloadQueue>) -> Result<(), String> { - let task_id = format!("{}/{}", category, pkgname); - - // 检查任务是否已存在 - { - let downloads = queue.lock().map_err(|e| e.to_string())?; - if downloads.contains_key(&task_id) { - return Ok(()); - } - } - - // 获取metalink文件URL - let json_server_url = get_json_server_url(); - let metalink_url = format!("{}{}/{}/{}.metalink", json_server_url, category, pkgname, filename); - // 发送请求获取metalink文件 - let client = reqwest::Client::new(); - let response = client - .get(&metalink_url) - .header("User-Agent", UA) - .send() - .await - .map_err(|e| format!("获取metalink文件失败: {}", e))?; - - // 检查响应状态 - if !response.status().is_success() { - return Err(format!("获取metalink文件失败: HTTP {}", response.status())); - } - - let mut downloads = queue.lock().map_err(|e| e.to_string())?; - let initial_status = if has_downloading_task(&downloads) { - DownloadStatus::Queued - } else { - DownloadStatus::Downloading - }; - - // 创建下载任务 - let task = DownloadTask { - category: category.clone(), - pkgname: pkgname.clone(), - filename, - name, - icon: format_icon_url(&category, &pkgname), - status: initial_status, - progress: 0.0, - speed: None, - size: None, - }; - - // 添加任务到队列 - downloads.insert(task_id, task); - - Ok(()) +pub async fn add_download(category: String, pkgname: String, filename: String, name: String, manager: State<'_, DownloadManager>) -> Result<(), String> { + manager.add_download(category, pkgname, filename, name).await } #[tauri::command] -pub async fn pause_download(category: String, pkgname: String, queue: State<'_, DownloadQueue>) -> Result<(), String> { - let mut downloads = queue.lock().map_err(|e| e.to_string())?; - let task_id = format!("{}/{}", category, pkgname); - - if let Some(task) = downloads.get_mut(&task_id) { - let old_status = task.status.clone(); - task.status = DownloadStatus::Paused; - if matches!(old_status, DownloadStatus::Downloading) { - start_next_download(&mut downloads); - } - } - Ok(()) +pub async fn pause_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { + manager.pause_download(category, pkgname) } #[tauri::command] -pub async fn resume_download(category: String, pkgname: String, queue: State<'_, DownloadQueue>) -> Result<(), String> { - let mut downloads = queue.lock().map_err(|e| e.to_string())?; - let task_id = format!("{}/{}", category, pkgname); - - // 先检查任务是否存在且处于暂停状态 - let should_resume = downloads - .get(&task_id) - .map(|task| matches!(task.status, DownloadStatus::Paused)) - .unwrap_or(false); - - if should_resume { - // 检查是否有其他正在下载的任务 - let has_downloading = has_downloading_task(&downloads); - - // 更新任务状态 - if let Some(task) = downloads.get_mut(&task_id) { - task.status = if has_downloading { - DownloadStatus::Queued - } else { - DownloadStatus::Downloading - }; - } - } - - Ok(()) +pub async fn resume_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { + manager.resume_download(category, pkgname) } #[tauri::command] -pub async fn cancel_download(category: String, pkgname: String, queue: State<'_, DownloadQueue>) -> Result<(), String> { - let mut downloads = queue.lock().map_err(|e| e.to_string())?; - let task_id = format!("{}/{}", category, pkgname); - - let was_downloading = downloads - .get(&task_id) - .map(|task| matches!(task.status, DownloadStatus::Downloading)) - .unwrap_or(false); - - downloads.remove(&task_id); - - if was_downloading { - start_next_download(&mut downloads); - } - - Ok(()) +pub async fn cancel_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { + manager.cancel_download(category, pkgname) } \ No newline at end of file diff --git a/src-tauri/src/handlers/mod.rs b/src-tauri/src/handlers/mod.rs index 8287a8e..261a1d4 100644 --- a/src-tauri/src/handlers/mod.rs +++ b/src-tauri/src/handlers/mod.rs @@ -3,4 +3,4 @@ pub mod server; pub mod app; pub mod home; pub mod file; -pub mod download; \ No newline at end of file +pub mod download; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dee3c24..9ad8e82 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,7 +7,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) - .manage(handlers::download::DownloadQueue::default()) + .manage(utils::download_manager::DownloadManager::new()) .invoke_handler(tauri::generate_handler![ handlers::category::get_all_categories, handlers::category::get_category_apps, diff --git a/src-tauri/src/models/download.rs b/src-tauri/src/models/download.rs index 9bfbdee..2bff942 100644 --- a/src-tauri/src/models/download.rs +++ b/src-tauri/src/models/download.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::utils::metalink::MetalinkFile; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DownloadTask { pub category: String, @@ -11,6 +13,7 @@ pub struct DownloadTask { pub name: String, pub speed: Option, pub size: Option, + pub metalink: MetalinkFile, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src-tauri/src/utils/download_manager.rs b/src-tauri/src/utils/download_manager.rs new file mode 100644 index 0000000..089d366 --- /dev/null +++ b/src-tauri/src/utils/download_manager.rs @@ -0,0 +1,178 @@ +use std::collections::HashMap; +use std::sync::Mutex; +use crate::models::download::{DownloadStatus, DownloadTask}; +use crate::utils::metalink::parse_metalink; +use crate::handlers::server::get_json_server_url; +use crate::utils::{format_icon_url, UA}; + +pub struct DownloadManager { + queue: Mutex>, +} + +impl DownloadManager { + pub fn new() -> Self { + DownloadManager { + queue: Mutex::new(HashMap::new()), + } + } + + // 获取所有下载任务 + pub fn get_downloads(&self) -> Result, String> { + let downloads = self.queue.lock().map_err(|e| e.to_string())?; + Ok(downloads.values().cloned().collect()) + } + + // 检查是否有正在下载的任务 + fn has_downloading_task(&self) -> Result { + let downloads = self.queue.lock().map_err(|e| e.to_string())?; + Ok(downloads.values().any(|task| matches!(task.status, DownloadStatus::Downloading))) + } + + // 获取下一个等待下载的任务 + fn get_next_queued_task(&self) -> Result, String> { + let downloads = self.queue.lock().map_err(|e| e.to_string())?; + Ok(downloads + .iter() + .find(|(_, task)| matches!(task.status, DownloadStatus::Queued)) + .map(|(task_id, _)| task_id.clone())) + } + + // 开始下载下一个任务 + fn start_next_download(&self) -> Result<(), String> { + let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + if let Some(task_id) = self.get_next_queued_task()? { + if let Some(task) = downloads.get_mut(&task_id) { + task.status = DownloadStatus::Downloading; + } + } + Ok(()) + } + + // 添加下载任务 + pub async fn add_download(&self, category: String, pkgname: String, filename: String, name: String) -> Result<(), String> { + let task_id = format!("{}/{}", category, pkgname); + + // 获取metalink文件URL和内容(在获取锁之前完成) + let json_server_url = get_json_server_url(); + let metalink_url = format!("{}{}/{}/{}.metalink", json_server_url, category, pkgname, filename); + + // 发送请求获取metalink文件 + let client = reqwest::Client::new(); + let response = client + .get(&metalink_url) + .header("User-Agent", UA) + .send() + .await + .map_err(|e| format!("获取metalink文件失败: {}", e))?; + + // 检查响应状态 + if !response.status().is_success() { + return Err(format!("获取metalink文件失败: HTTP {}", response.status())); + } + + // 解析 metalink 内容 + let metalink_content = response.text().await + .map_err(|e| format!("读取metalink内容失败: {}", e))?; + + let metalink_data = parse_metalink(&metalink_content)?; + + println!("{:?}", metalink_data); + + // 获取一次锁,完成所有状态更新操作 + let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + + // 检查任务是否已存在 + if downloads.contains_key(&task_id) { + return Ok(()); + } + + // 检查是否有正在下载的任务 + let has_downloading = downloads.values().any(|task| matches!(task.status, DownloadStatus::Downloading)); + let initial_status = if has_downloading { + DownloadStatus::Queued + } else { + DownloadStatus::Downloading + }; + + // 创建下载任务 + let task = DownloadTask { + category: category.clone(), + pkgname: pkgname.clone(), + filename, + name, + icon: format_icon_url(&category, &pkgname), + status: initial_status, + progress: 0.0, + speed: None, + size: None, + metalink: metalink_data, + }; + + // 添加任务到队列 + downloads.insert(task_id, task); + + Ok(()) + } + + // 暂停下载任务 + pub fn pause_download(&self, category: String, pkgname: String) -> Result<(), String> { + let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + let task_id = format!("{}/{}", category, pkgname); + + if let Some(task) = downloads.get_mut(&task_id) { + let old_status = task.status.clone(); + task.status = DownloadStatus::Paused; + if matches!(old_status, DownloadStatus::Downloading) { + self.start_next_download()?; + } + } + Ok(()) + } + + // 恢复下载任务 + pub fn resume_download(&self, category: String, pkgname: String) -> Result<(), String> { + let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + let task_id = format!("{}/{}", category, pkgname); + + // 先检查任务是否存在且处于暂停状态 + let should_resume = downloads + .get(&task_id) + .map(|task| matches!(task.status, DownloadStatus::Paused)) + .unwrap_or(false); + + if should_resume { + // 检查是否有其他正在下载的任务 + let has_downloading = self.has_downloading_task()?; + + // 更新任务状态 + if let Some(task) = downloads.get_mut(&task_id) { + task.status = if has_downloading { + DownloadStatus::Queued + } else { + DownloadStatus::Downloading + }; + } + } + + Ok(()) + } + + // 取消下载任务 + pub fn cancel_download(&self, category: String, pkgname: String) -> Result<(), String> { + let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + let task_id = format!("{}/{}", category, pkgname); + + let was_downloading = downloads + .get(&task_id) + .map(|task| matches!(task.status, DownloadStatus::Downloading)) + .unwrap_or(false); + + downloads.remove(&task_id); + + if was_downloading { + self.start_next_download()?; + } + + Ok(()) + } +} \ No newline at end of file diff --git a/src-tauri/src/utils/metalink.rs b/src-tauri/src/utils/metalink.rs new file mode 100644 index 0000000..5fdc79b --- /dev/null +++ b/src-tauri/src/utils/metalink.rs @@ -0,0 +1,116 @@ +use quick_xml::events::Event; +use quick_xml::reader::Reader; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetalinkFile { + pub name: String, + pub hashes: HashMap, + pub urls: Vec, +} + +pub fn parse_metalink(content: &str) -> Result { + let mut reader = Reader::from_str(content); + reader.trim_text(true); + + let mut buf = Vec::new(); + let mut file = MetalinkFile { + name: String::new(), + hashes: HashMap::new(), + urls: Vec::new(), + }; + + let mut in_file = false; + let mut in_verification = false; + let mut current_hash_type = String::new(); + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) => { + match e.name().as_ref() { + b"file" => { + in_file = true; + if let Ok(attr) = e.attributes().next().unwrap() { + if attr.key.as_ref() == b"name" { + file.name = String::from_utf8_lossy(&attr.value).to_string(); + } + } + } + b"verification" => in_verification = true, + b"hash" => { + if in_verification { + if let Ok(attr) = e.attributes().next().unwrap() { + if attr.key.as_ref() == b"type" { + current_hash_type = String::from_utf8_lossy(&attr.value).to_string(); + } + } + } + } + b"url" => { + if in_file { + // 暂时不处理 url 的属性,直接获取内容 + } + } + _ => (), + } + } + Ok(Event::Text(e)) => { + if !current_hash_type.is_empty() { + file.hashes.insert(current_hash_type.clone(), e.unescape().unwrap().to_string()); + current_hash_type.clear(); + } else if in_file { + let text = e.unescape().unwrap(); + if text.trim().starts_with("http") { + file.urls.push(text.to_string()); + } + } + } + Ok(Event::End(ref e)) => { + match e.name().as_ref() { + b"file" => in_file = false, + b"verification" => in_verification = false, + _ => (), + } + } + Ok(Event::Eof) => break, + Err(e) => return Err(format!("解析错误: {}", e)), + _ => (), + } + } + + if file.name.is_empty() { + return Err("未找到文件名".to_string()); + } + + Ok(file) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_metalink() { + let content = r#" + + + + + a6af060b8a27417ed972b8b004815725 + 45a8758392ee4feae9da4f53374721138ab66dac + + + https://mirrors.sdu.edu.cn/spark-store/store/./chat/wechat/wechat_4.0.1.11_amd64.deb + https://spark.home.sunnypai.top:43443/store/./chat/wechat/wechat_4.0.1.11_amd64.deb + + + + "#; + + let result = parse_metalink(content).unwrap(); + assert_eq!(result.name, "wechat_4.0.1.11_amd64.deb"); + assert_eq!(result.hashes.get("md5").unwrap(), "a6af060b8a27417ed972b8b004815725"); + assert_eq!(result.urls.len(), 2); + } +} \ No newline at end of file diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 50dcee8..d346668 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,6 +1,8 @@ use crate::handlers::server::get_img_server_url; pub mod search; +pub mod metalink; +pub mod download_manager; pub const UA: &str = concat!("Spark-Store/", env!("CARGO_PKG_VERSION"));