From aadc1370556a6f43bb85d32b7172bcb315698536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=9A=E5=AD=90?= <40852301+uiYzzi@users.noreply.github.com> Date: Tue, 4 Mar 2025 04:10:19 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 25 +- src-tauri/Cargo.toml | 3 +- src-tauri/src/handlers/download.rs | 12 +- src-tauri/src/models/download.rs | 17 +- src-tauri/src/utils/aria2.rs | 366 ++++++++++++++++++++++++ src-tauri/src/utils/download_manager.rs | 345 ++++++++++++++++------ src-tauri/src/utils/format.rs | 21 ++ src-tauri/src/utils/metalink.rs | 116 -------- src-tauri/src/utils/mod.rs | 3 +- src/features/downloads/store.ts | 15 +- 10 files changed, 682 insertions(+), 241 deletions(-) create mode 100644 src-tauri/src/utils/aria2.rs create mode 100644 src-tauri/src/utils/format.rs delete mode 100644 src-tauri/src/utils/metalink.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5cac561..e4b1099 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 0.32.0", + "quick-xml", "serde", "time", ] @@ -3064,15 +3064,6 @@ 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" @@ -3629,17 +3620,6 @@ 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" @@ -3761,15 +3741,14 @@ dependencies = [ name = "spark-store" version = "4.9.9" dependencies = [ + "base64 0.22.1", "dirs 6.0.0", "futures", "lazy_static", "pinyin", - "quick-xml 0.31.0", "reqwest", "serde", "serde_json", - "sha1", "tauri", "tauri-build", "tauri-plugin-clipboard-manager", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b183780..f55f4cb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,5 +31,4 @@ 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" +base64 = "0.22.1" diff --git a/src-tauri/src/handlers/download.rs b/src-tauri/src/handlers/download.rs index 1f4ce73..aa86007 100644 --- a/src-tauri/src/handlers/download.rs +++ b/src-tauri/src/handlers/download.rs @@ -1,10 +1,10 @@ use tauri::State; -use crate::{models::download::DownloadTask, utils::download_manager::DownloadManager}; +use crate::{models::download::DownloadTaskResponse, utils::download_manager::DownloadManager}; #[tauri::command] -pub async fn get_downloads(manager: State<'_, DownloadManager>) -> Result, String> { - manager.get_downloads() +pub async fn get_downloads(manager: State<'_, DownloadManager>) -> Result, String> { + manager.get_downloads().await } #[tauri::command] @@ -14,15 +14,15 @@ pub async fn add_download(category: String, pkgname: String, filename: String, n #[tauri::command] pub async fn pause_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { - manager.pause_download(category, pkgname) + manager.pause_download(category, pkgname).await } #[tauri::command] pub async fn resume_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { - manager.resume_download(category, pkgname) + manager.resume_download(category, pkgname).await } #[tauri::command] pub async fn cancel_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> { - manager.cancel_download(category, pkgname) + manager.cancel_download(category, pkgname).await } \ No newline at end of file diff --git a/src-tauri/src/models/download.rs b/src-tauri/src/models/download.rs index 2bff942..53cb1b1 100644 --- a/src-tauri/src/models/download.rs +++ b/src-tauri/src/models/download.rs @@ -1,19 +1,26 @@ use serde::{Deserialize, Serialize}; -use crate::utils::metalink::MetalinkFile; - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DownloadTask { pub category: String, pub pkgname: String, pub filename: String, - pub status: DownloadStatus, - pub progress: f32, pub icon: String, pub name: String, + pub gid: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DownloadTaskResponse { + pub category: String, + pub pkgname: String, + pub filename: String, + pub status: DownloadStatus, + pub icon: String, + pub name: String, + pub progress: f32, pub speed: Option, pub size: Option, - pub metalink: MetalinkFile, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src-tauri/src/utils/aria2.rs b/src-tauri/src/utils/aria2.rs new file mode 100644 index 0000000..fec4144 --- /dev/null +++ b/src-tauri/src/utils/aria2.rs @@ -0,0 +1,366 @@ +use base64::{engine::general_purpose, Engine as _}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::error::Error; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct Aria2Client { + rpc_url: String, + secret: Option, + client: Client, +} + +#[derive(Debug, Serialize, Deserialize)] +struct JsonRpcRequest { + jsonrpc: String, + id: String, + method: String, + params: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct JsonRpcResponse { + id: String, + jsonrpc: String, + result: Option, + error: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct JsonRpcError { + code: i32, + message: String, +} + +impl Aria2Client { + pub fn new(host: &str, port: u16, secret: Option) -> Self { + let rpc_url = format!("http://{}:{}/jsonrpc", host, port); + Aria2Client { + rpc_url, + secret, + client: Client::new(), + } + } + + /// 添加Metalink下载 + /// + /// 通过上传.metalink文件内容添加Metalink下载 + /// + /// # 参数 + /// + /// * `metalink` - metalink文件的内容 + /// * `options` - 可选的下载选项 + /// * `position` - 可选的队列位置 + /// + /// # 返回 + /// + /// 返回新注册下载的GID数组 + pub async fn add_metalink( + &self, + metalink: &[u8], + options: Option>, + position: Option, + ) -> Result, Box> { + // Base64编码metalink内容 + let encoded_metalink = general_purpose::STANDARD.encode(metalink); + + // 构建参数列表 + let mut params = Vec::new(); + + // 如果有密钥,添加到参数列表 + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + // 添加metalink内容 + params.push(json!(encoded_metalink)); + + // 添加选项(如果有) + if let Some(opts) = options { + params.push(json!(opts)); + } + + // 添加位置(如果有) + if let Some(pos) = position { + params.push(json!(pos)); + } + + // 发送RPC请求 + let response = self.send_request("aria2.addMetalink", params).await?; + + // 解析结果 + if let Some(result) = response.result { + if let Some(gids) = result.as_array() { + let gid_vec = gids + .iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect(); + return Ok(gid_vec); + } + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法解析Aria2响应".into()) + } + + /// 发送JSON-RPC请求到aria2 + async fn send_request( + &self, + method: &str, + params: Vec, + ) -> Result> { + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: "spark-store".to_string(), + method: method.to_string(), + params, + }; + + let response = self.client + .post(&self.rpc_url) + .json(&request) + .send() + .await? + .json::() + .await?; + + Ok(response) + } + + /// 获取下载状态 + pub async fn tell_status(&self, gid: &str) -> Result> { + let mut params = Vec::new(); + + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + params.push(json!(gid)); + + let response = self.send_request("aria2.tellStatus", params).await?; + + if let Some(result) = response.result { + return Ok(result); + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法获取下载状态".into()) + } + + /// 获取所有活动中的下载任务 + /// + /// 返回所有正在下载的任务的状态信息 + /// + /// # 返回 + /// + /// 返回包含所有活动下载任务信息的数组 + pub async fn tell_active(&self) -> Result, Box> { + let mut params = Vec::new(); + + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + let response = self.send_request("aria2.tellActive", params).await?; + + if let Some(result) = response.result { + if let Some(downloads) = result.as_array() { + return Ok(downloads.clone()); + } + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法获取活动下载任务".into()) + } + + /// 获取等待中的下载任务 + /// + /// 返回等待队列中的下载任务状态信息 + /// + /// # 参数 + /// + /// * `offset` - 从队列开始位置的偏移量 + /// * `num` - 要获取的任务数量 + /// + /// # 返回 + /// + /// 返回包含等待中下载任务信息的数组 + pub async fn tell_waiting(&self, offset: usize, num: usize) -> Result, Box> { + let mut params = Vec::new(); + + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + params.push(json!(offset)); + params.push(json!(num)); + + let response = self.send_request("aria2.tellWaiting", params).await?; + + if let Some(result) = response.result { + if let Some(downloads) = result.as_array() { + return Ok(downloads.clone()); + } + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法获取等待中的下载任务".into()) + } + + /// 获取已停止的下载任务 + /// + /// 返回已完成/错误/已移除的下载任务状态信息 + /// + /// # 参数 + /// + /// * `offset` - 从队列开始位置的偏移量 + /// * `num` - 要获取的任务数量 + /// + /// # 返回 + /// + /// 返回包含已停止下载任务信息的数组 + pub async fn tell_stopped(&self, offset: usize, num: usize) -> Result, Box> { + let mut params = Vec::new(); + + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + params.push(json!(offset)); + params.push(json!(num)); + + let response = self.send_request("aria2.tellStopped", params).await?; + + if let Some(result) = response.result { + if let Some(downloads) = result.as_array() { + return Ok(downloads.clone()); + } + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法获取已停止的下载任务".into()) + } + + /// 暂停下载任务 + /// + /// 暂停指定GID的下载任务 + /// + /// # 参数 + /// + /// * `gid` - 下载任务的GID + /// + /// # 返回 + /// + /// 成功时返回被暂停的GID + pub async fn pause(&self, gid: &str) -> Result> { + let mut params = Vec::new(); + + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + params.push(json!(gid)); + + let response = self.send_request("aria2.pause", params).await?; + + if let Some(result) = response.result { + if let Some(paused_gid) = result.as_str() { + return Ok(paused_gid.to_string()); + } + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法暂停下载任务".into()) + } + + /// 恢复下载任务 + /// + /// 恢复指定GID的下载任务 + /// + /// # 参数 + /// + /// * `gid` - 下载任务的GID + /// + /// # 返回 + /// + /// 成功时返回被恢复的GID + pub async fn unpause(&self, gid: &str) -> Result> { + let mut params = Vec::new(); + + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + params.push(json!(gid)); + + let response = self.send_request("aria2.unpause", params).await?; + + if let Some(result) = response.result { + if let Some(unpaused_gid) = result.as_str() { + return Ok(unpaused_gid.to_string()); + } + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法恢复下载任务".into()) + } + + /// 删除下载任务 + /// + /// 删除指定GID的下载任务 + /// + /// # 参数 + /// + /// * `gid` - 下载任务的GID + /// + /// # 返回 + /// + /// 成功时返回被删除的GID + pub async fn remove(&self, gid: &str) -> Result> { + let mut params = Vec::new(); + + if let Some(secret) = &self.secret { + params.push(json!(secret)); + } + + params.push(json!(gid)); + + let response = self.send_request("aria2.remove", params).await?; + + if let Some(result) = response.result { + if let Some(removed_gid) = result.as_str() { + return Ok(removed_gid.to_string()); + } + } + + if let Some(error) = response.error { + return Err(format!("Aria2 RPC错误: {} ({})", error.message, error.code).into()); + } + + Err("无法删除下载任务".into()) + } +} \ No newline at end of file diff --git a/src-tauri/src/utils/download_manager.rs b/src-tauri/src/utils/download_manager.rs index 089d366..98068a9 100644 --- a/src-tauri/src/utils/download_manager.rs +++ b/src-tauri/src/utils/download_manager.rs @@ -1,61 +1,203 @@ use std::collections::HashMap; use std::sync::Mutex; -use crate::models::download::{DownloadStatus, DownloadTask}; -use crate::utils::metalink::parse_metalink; +use std::process::Command; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::net::TcpListener; +use crate::models::download::{DownloadStatus, DownloadTask, DownloadTaskResponse}; use crate::handlers::server::get_json_server_url; -use crate::utils::{format_icon_url, UA}; +use crate::utils::{format_icon_url, UA, aria2::Aria2Client}; + +use super::format::{format_size, format_speed}; pub struct DownloadManager { queue: Mutex>, + aria2_started: Arc, + aria2_port: Arc>, } impl DownloadManager { pub fn new() -> Self { DownloadManager { queue: Mutex::new(HashMap::new()), + aria2_started: Arc::new(AtomicBool::new(false)), + aria2_port: Arc::new(Mutex::new(5144)), } } // 获取所有下载任务 - 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; + pub async fn get_downloads(&self) -> Result, String> { + // 获取队列中的任务信息并立即克隆所需数据,然后释放锁 + let tasks_clone: Vec; + let aria2_started; + let port; + + { + // 使用作用域限制锁的生命周期 + let downloads = self.queue.lock().map_err(|e| e.to_string())?; + tasks_clone = downloads.values().cloned().collect(); + aria2_started = self.aria2_started.load(Ordering::SeqCst); + } + + // 如果 aria2 未启动,直接返回队列中的任务 + if !aria2_started { + return Ok(tasks_clone.into_iter().map(|task| DownloadTaskResponse { + category: task.category, + pkgname: task.pkgname, + filename: task.filename, + status: DownloadStatus::Error, // 如果 aria2 未启动,标记为错误状态 + icon: task.icon, + name: task.name, + progress: 0.0, + speed: None, + size: None, + }).collect()); + } + + // 获取端口(在单独的作用域中获取锁) + { + port = *self.aria2_port.lock().map_err(|e| e.to_string())?; + } + + // 创建 Aria2Client 实例 + let aria2_client = Aria2Client::new("127.0.0.1", port, None); + + // 获取所有活动中的下载任务 + let active_downloads = aria2_client.tell_active().await + .map_err(|e| format!("获取活动下载任务失败: {}", e))?; + + // 获取所有等待中的下载任务(最多100个) + let waiting_downloads = aria2_client.tell_waiting(0, 100).await + .map_err(|e| format!("获取等待中下载任务失败: {}", e))?; + + // 获取所有已停止的下载任务(最多100个) + let stopped_downloads = aria2_client.tell_stopped(0, 100).await + .map_err(|e| format!("获取已停止下载任务失败: {}", e))?; + + // 创建一个映射,用于存储 GID 到 aria2 任务状态的映射 + let mut aria2_tasks = HashMap::new(); + + // 处理活动中的下载任务 + for task in active_downloads { + if let Some(gid) = task["gid"].as_str() { + aria2_tasks.insert(gid.to_string(), (task, DownloadStatus::Downloading)); } } - Ok(()) + + // 处理等待中的下载任务 + for task in waiting_downloads { + if let Some(gid) = task["gid"].as_str() { + let status = if task["status"].as_str() == Some("paused") { + DownloadStatus::Paused + } else { + DownloadStatus::Queued + }; + aria2_tasks.insert(gid.to_string(), (task, status)); + } + } + + // 处理已停止的下载任务 + for task in stopped_downloads { + if let Some(gid) = task["gid"].as_str() { + let status = if task["status"].as_str() == Some("complete") { + DownloadStatus::Completed + } else { + DownloadStatus::Error + }; + aria2_tasks.insert(gid.to_string(), (task, status)); + } + } + + // 将队列中的任务与 aria2 任务状态结合,生成响应 + let mut result = Vec::new(); + + for task in tasks_clone { + let mut response = DownloadTaskResponse { + category: task.category.clone(), + pkgname: task.pkgname.clone(), + filename: task.filename.clone(), + status: DownloadStatus::Error, // 默认为错误状态 + icon: task.icon.clone(), + name: task.name.clone(), + progress: 0.0, + speed: None, + size: None, + }; + + // 如果在 aria2 任务中找到对应的 GID,更新状态信息 + if let Some((aria2_task, status)) = aria2_tasks.get(&task.gid) { + response.status = status.clone(); + + // 计算进度(百分比) + if let (Some(completed_length), Some(total_length)) = ( + aria2_task["completedLength"].as_str().and_then(|s| s.parse::().ok()), + aria2_task["totalLength"].as_str().and_then(|s| s.parse::().ok()) + ) { + if total_length > 0.0 { + // 保留两位小数 + let progress = ((completed_length / total_length) * 100.0) as f32; + response.progress = (progress * 100.0).round() / 100.0; + } + } + + // 获取下载速度 + if let Some(download_speed) = aria2_task["downloadSpeed"].as_str() { + if let Ok(speed) = download_speed.parse::() { + response.speed = Some(format_speed(speed)); + } + } + + // 获取文件大小 + if let Some(total_length) = aria2_task["totalLength"].as_str() { + if let Ok(size) = total_length.parse::() { + response.size = Some(format_size(size)); + } + } + } + + result.push(response); + } + + Ok(result) + } + + fn start_aria2(&self) { + // 寻找可用端口 + let mut port = 5144; + while TcpListener::bind(format!("127.0.0.1:{}", port)).is_err() { + port += 1; + } + if let Ok(mut port_guard) = self.aria2_port.lock() { + *port_guard = port; + } + + // 启动 aria2c + Command::new("aria2c") + .args([ + "--enable-rpc", + "--rpc-listen-all=false", + &format!("--rpc-listen-port={}", port), + &format!("--user-agent={}", UA), + ]) + .spawn() + .map_err(|e| format!("启动 aria2 失败: {}", e)).unwrap(); + + self.aria2_started.store(true, Ordering::SeqCst); } // 添加下载任务 pub async fn add_download(&self, category: String, pkgname: String, filename: String, name: String) -> Result<(), String> { + // 检查并启动 aria2(如果还没启动) + if !self.aria2_started.load(Ordering::SeqCst) { + self.start_aria2(); + } + 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 @@ -70,14 +212,24 @@ impl DownloadManager { return Err(format!("获取metalink文件失败: HTTP {}", response.status())); } - // 解析 metalink 内容 - let metalink_content = response.text().await - .map_err(|e| format!("读取metalink内容失败: {}", e))?; + // 获取metalink文件内容 + let metalink_content = response.bytes() + .await + .map_err(|e| format!("读取metalink内容失败: {}", e))?; - let metalink_data = parse_metalink(&metalink_content)?; + // 创建Aria2Client并添加下载任务 + let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; + let aria2_client = Aria2Client::new("127.0.0.1", port, None); + + // 使用Aria2Client添加metalink下载 + let gids = aria2_client.add_metalink( + &metalink_content, + None, + None + ) + .await + .map_err(|e| format!("添加下载任务失败: {}", e))?; - println!("{:?}", metalink_data); - // 获取一次锁,完成所有状态更新操作 let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; @@ -85,15 +237,7 @@ impl DownloadManager { 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(), @@ -101,11 +245,7 @@ impl DownloadManager { filename, name, icon: format_icon_url(&category, &pkgname), - status: initial_status, - progress: 0.0, - speed: None, - size: None, - metalink: metalink_data, + gid: gids[0].clone() }; // 添加任务到队列 @@ -115,64 +255,95 @@ impl DownloadManager { } // 暂停下载任务 - pub fn pause_download(&self, category: String, pkgname: String) -> Result<(), String> { - let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + pub async fn pause_download(&self, category: String, pkgname: String) -> Result<(), 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()?; + // 获取任务信息,并在作用域结束时释放锁 + let task_gid = { + let downloads = self.queue.lock().map_err(|e| e.to_string())?; + match downloads.get(&task_id) { + Some(task) => task.gid.clone(), + None => return Err(format!("找不到下载任务: {}", task_id)), } + }; + + // 如果 aria2 未启动,返回错误 + if !self.aria2_started.load(Ordering::SeqCst) { + return Err("aria2 未启动".to_string()); } + + // 创建 Aria2Client 实例 + let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; + let aria2_client = Aria2Client::new("127.0.0.1", port, None); + + // 调用 aria2 的 pause 方法 + aria2_client.pause(task_gid.as_str()).await + .map_err(|e| format!("暂停下载任务失败: {}", e))?; + Ok(()) } // 恢复下载任务 - pub fn resume_download(&self, category: String, pkgname: String) -> Result<(), String> { - let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + pub async fn resume_download(&self, category: String, pkgname: String) -> Result<(), 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 - }; + // 获取任务信息,并在作用域结束时释放锁 + let task_gid = { + let downloads = self.queue.lock().map_err(|e| e.to_string())?; + match downloads.get(&task_id) { + Some(task) => task.gid.clone(), + None => return Err(format!("找不到下载任务: {}", task_id)), } + }; + + // 如果 aria2 未启动,返回错误 + if !self.aria2_started.load(Ordering::SeqCst) { + return Err("aria2 未启动".to_string()); } + // 创建 Aria2Client 实例 + let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; + let aria2_client = Aria2Client::new("127.0.0.1", port, None); + + // 调用 aria2 的 unpause 方法 + aria2_client.unpause(task_gid.as_str()).await + .map_err(|e| format!("恢复下载任务失败: {}", e))?; + Ok(()) } // 取消下载任务 - pub fn cancel_download(&self, category: String, pkgname: String) -> Result<(), String> { - let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + pub async fn cancel_download(&self, category: String, pkgname: String) -> Result<(), String> { let task_id = format!("{}/{}", category, pkgname); - let was_downloading = downloads - .get(&task_id) - .map(|task| matches!(task.status, DownloadStatus::Downloading)) - .unwrap_or(false); + // 获取任务信息,并在作用域结束时释放锁 + let task_gid = { + let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; + match downloads.get(&task_id) { + Some(task) => { + // 如果 aria2 未启动,只从队列中移除任务 + if !self.aria2_started.load(Ordering::SeqCst) { + downloads.remove(&task_id); + return Ok(()); + } + task.gid.clone() + }, + None => return Err(format!("找不到下载任务: {}", task_id)), + } + }; + // 创建 Aria2Client 实例 + let port = *self.aria2_port.lock().map_err(|e| e.to_string())?; + let aria2_client = Aria2Client::new("127.0.0.1", port, None); + + // 调用 aria2 的 remove 方法 + aria2_client.remove(task_gid.as_str()).await + .map_err(|e| format!("取消下载任务失败: {}", e))?; + + // 从队列中移除任务 + let mut downloads = self.queue.lock().map_err(|e| e.to_string())?; 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/format.rs b/src-tauri/src/utils/format.rs new file mode 100644 index 0000000..4aa07e7 --- /dev/null +++ b/src-tauri/src/utils/format.rs @@ -0,0 +1,21 @@ +// 格式化文件大小 +pub fn format_size(size: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if size >= GB { + format!("{:.2} GB", size as f64 / GB as f64) + } else if size >= MB { + format!("{:.2} MB", size as f64 / MB as f64) + } else if size >= KB { + format!("{:.2} KB", size as f64 / KB as f64) + } else { + format!("{} B", size) + } +} + +// 格式化下载速度 +pub fn format_speed(speed: u64) -> String { + format!("{}/s", format_size(speed)) +} \ No newline at end of file diff --git a/src-tauri/src/utils/metalink.rs b/src-tauri/src/utils/metalink.rs deleted file mode 100644 index 5fdc79b..0000000 --- a/src-tauri/src/utils/metalink.rs +++ /dev/null @@ -1,116 +0,0 @@ -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 d346668..b9c2960 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,8 +1,9 @@ use crate::handlers::server::get_img_server_url; pub mod search; -pub mod metalink; pub mod download_manager; +pub mod aria2; +pub mod format; pub const UA: &str = concat!("Spark-Store/", env!("CARGO_PKG_VERSION")); diff --git a/src/features/downloads/store.ts b/src/features/downloads/store.ts index ed2f5fa..efc067e 100644 --- a/src/features/downloads/store.ts +++ b/src/features/downloads/store.ts @@ -1,4 +1,4 @@ -import { createSignal, createEffect } from 'solid-js'; +import { createSignal, createEffect, onCleanup } from 'solid-js'; import { DownloadTask } from '@/types/download'; import { getDownloads, addDownload as addDownloadApi, pauseDownload as pauseDownloadApi, resumeDownload as resumeDownloadApi, cancelDownload as cancelDownloadApi } from '@/lib/api/download'; @@ -15,6 +15,19 @@ createEffect(async () => { }); export const useDownloadsStore = () => { + // 设置每秒更新一次下载列表的定时器 + const intervalId = setInterval(async () => { + try { + const updatedList = await getDownloads(); + setDownloads(updatedList); + } catch (error) { + console.error('定时更新下载列表失败:', error); + } + }, 1000); // 1000毫秒 = 1秒 + + // 组件卸载时清除定时器 + onCleanup(() => clearInterval(intervalId)); + const activeDownloads = () => downloads().filter(item => ['downloading', 'paused', 'queued'].includes(item.status) );