✨ 添加下载功能
This commit is contained in:
25
src-tauri/Cargo.lock
generated
25
src-tauri/Cargo.lock
generated
@@ -2943,7 +2943,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap 2.7.1",
|
"indexmap 2.7.1",
|
||||||
"quick-xml 0.32.0",
|
"quick-xml",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
@@ -3064,15 +3064,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
@@ -3629,17 +3620,6 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"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]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.8"
|
||||||
@@ -3761,15 +3741,14 @@ dependencies = [
|
|||||||
name = "spark-store"
|
name = "spark-store"
|
||||||
version = "4.9.9"
|
version = "4.9.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"dirs 6.0.0",
|
"dirs 6.0.0",
|
||||||
"futures",
|
"futures",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"pinyin",
|
"pinyin",
|
||||||
"quick-xml 0.31.0",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
|
|||||||
@@ -31,5 +31,4 @@ futures = "0.3.31"
|
|||||||
pinyin = "0.10.0"
|
pinyin = "0.10.0"
|
||||||
tauri-plugin-clipboard-manager = "2.2.0"
|
tauri-plugin-clipboard-manager = "2.2.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
quick-xml = "0.31.0"
|
base64 = "0.22.1"
|
||||||
sha1 = "0.10.6"
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::{models::download::DownloadTask, utils::download_manager::DownloadManager};
|
use crate::{models::download::DownloadTaskResponse, utils::download_manager::DownloadManager};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_downloads(manager: State<'_, DownloadManager>) -> Result<Vec<DownloadTask>, String> {
|
pub async fn get_downloads(manager: State<'_, DownloadManager>) -> Result<Vec<DownloadTaskResponse>, String> {
|
||||||
manager.get_downloads()
|
manager.get_downloads().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -14,15 +14,15 @@ pub async fn add_download(category: String, pkgname: String, filename: String, n
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn pause_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> {
|
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]
|
#[tauri::command]
|
||||||
pub async fn resume_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> {
|
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]
|
#[tauri::command]
|
||||||
pub async fn cancel_download(category: String, pkgname: String, manager: State<'_, DownloadManager>) -> Result<(), String> {
|
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
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,26 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::utils::metalink::MetalinkFile;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct DownloadTask {
|
pub struct DownloadTask {
|
||||||
pub category: String,
|
pub category: String,
|
||||||
pub pkgname: String,
|
pub pkgname: String,
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub status: DownloadStatus,
|
|
||||||
pub progress: f32,
|
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub name: 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<String>,
|
pub speed: Option<String>,
|
||||||
pub size: Option<String>,
|
pub size: Option<String>,
|
||||||
pub metalink: MetalinkFile,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
|||||||
366
src-tauri/src/utils/aria2.rs
Normal file
366
src-tauri/src/utils/aria2.rs
Normal file
@@ -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<String>,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct JsonRpcRequest {
|
||||||
|
jsonrpc: String,
|
||||||
|
id: String,
|
||||||
|
method: String,
|
||||||
|
params: Vec<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct JsonRpcResponse {
|
||||||
|
id: String,
|
||||||
|
jsonrpc: String,
|
||||||
|
result: Option<Value>,
|
||||||
|
error: Option<JsonRpcError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct JsonRpcError {
|
||||||
|
code: i32,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aria2Client {
|
||||||
|
pub fn new(host: &str, port: u16, secret: Option<String>) -> 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<HashMap<String, Value>>,
|
||||||
|
position: Option<usize>,
|
||||||
|
) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
// 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<Value>,
|
||||||
|
) -> Result<JsonRpcResponse, Box<dyn Error>> {
|
||||||
|
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::<JsonRpcResponse>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取下载状态
|
||||||
|
pub async fn tell_status(&self, gid: &str) -> Result<Value, Box<dyn Error>> {
|
||||||
|
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<Vec<Value>, Box<dyn Error>> {
|
||||||
|
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<Vec<Value>, Box<dyn Error>> {
|
||||||
|
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<Vec<Value>, Box<dyn Error>> {
|
||||||
|
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<String, Box<dyn Error>> {
|
||||||
|
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<String, Box<dyn Error>> {
|
||||||
|
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<String, Box<dyn Error>> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +1,197 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use crate::models::download::{DownloadStatus, DownloadTask};
|
use std::process::Command;
|
||||||
use crate::utils::metalink::parse_metalink;
|
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::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 {
|
pub struct DownloadManager {
|
||||||
queue: Mutex<HashMap<String, DownloadTask>>,
|
queue: Mutex<HashMap<String, DownloadTask>>,
|
||||||
|
aria2_started: Arc<AtomicBool>,
|
||||||
|
aria2_port: Arc<Mutex<u16>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DownloadManager {
|
impl DownloadManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
DownloadManager {
|
DownloadManager {
|
||||||
queue: Mutex::new(HashMap::new()),
|
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<Vec<DownloadTask>, String> {
|
pub async fn get_downloads(&self) -> Result<Vec<DownloadTaskResponse>, String> {
|
||||||
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
// 获取队列中的任务信息并立即克隆所需数据,然后释放锁
|
||||||
Ok(downloads.values().cloned().collect())
|
let tasks_clone: Vec<DownloadTask>;
|
||||||
}
|
let aria2_started;
|
||||||
|
let port;
|
||||||
|
|
||||||
// 检查是否有正在下载的任务
|
{
|
||||||
fn has_downloading_task(&self) -> Result<bool, String> {
|
// 使用作用域限制锁的生命周期
|
||||||
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
||||||
Ok(downloads.values().any(|task| matches!(task.status, DownloadStatus::Downloading)))
|
tasks_clone = downloads.values().cloned().collect();
|
||||||
}
|
aria2_started = self.aria2_started.load(Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取下一个等待下载的任务
|
// 如果 aria2 未启动,直接返回队列中的任务
|
||||||
fn get_next_queued_task(&self) -> Result<Option<String>, String> {
|
if !aria2_started {
|
||||||
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
return Ok(tasks_clone.into_iter().map(|task| DownloadTaskResponse {
|
||||||
Ok(downloads
|
category: task.category,
|
||||||
.iter()
|
pkgname: task.pkgname,
|
||||||
.find(|(_, task)| matches!(task.status, DownloadStatus::Queued))
|
filename: task.filename,
|
||||||
.map(|(task_id, _)| task_id.clone()))
|
status: DownloadStatus::Error, // 如果 aria2 未启动,标记为错误状态
|
||||||
}
|
icon: task.icon,
|
||||||
|
name: task.name,
|
||||||
|
progress: 0.0,
|
||||||
|
speed: None,
|
||||||
|
size: None,
|
||||||
|
}).collect());
|
||||||
|
}
|
||||||
|
|
||||||
// 开始下载下一个任务
|
// 获取端口(在单独的作用域中获取锁)
|
||||||
fn start_next_download(&self) -> Result<(), String> {
|
{
|
||||||
let mut downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
port = *self.aria2_port.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;
|
// 创建 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::<f64>().ok()),
|
||||||
|
aria2_task["totalLength"].as_str().and_then(|s| s.parse::<f64>().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::<u64>() {
|
||||||
|
response.speed = Some(format_speed(speed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件大小
|
||||||
|
if let Some(total_length) = aria2_task["totalLength"].as_str() {
|
||||||
|
if let Ok(size) = total_length.parse::<u64>() {
|
||||||
|
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> {
|
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);
|
let task_id = format!("{}/{}", category, pkgname);
|
||||||
|
|
||||||
// 获取metalink文件URL和内容(在获取锁之前完成)
|
// 获取metalink文件URL和内容(在获取锁之前完成)
|
||||||
@@ -70,13 +212,23 @@ impl DownloadManager {
|
|||||||
return Err(format!("获取metalink文件失败: HTTP {}", response.status()));
|
return Err(format!("获取metalink文件失败: HTTP {}", response.status()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 metalink 内容
|
// 获取metalink文件内容
|
||||||
let metalink_content = response.text().await
|
let metalink_content = response.bytes()
|
||||||
.map_err(|e| format!("读取metalink内容失败: {}", e))?;
|
.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);
|
||||||
|
|
||||||
println!("{:?}", metalink_data);
|
// 使用Aria2Client添加metalink下载
|
||||||
|
let gids = aria2_client.add_metalink(
|
||||||
|
&metalink_content,
|
||||||
|
None,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("添加下载任务失败: {}", e))?;
|
||||||
|
|
||||||
// 获取一次锁,完成所有状态更新操作
|
// 获取一次锁,完成所有状态更新操作
|
||||||
let mut downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let mut downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
||||||
@@ -86,14 +238,6 @@ impl DownloadManager {
|
|||||||
return Ok(());
|
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 {
|
let task = DownloadTask {
|
||||||
category: category.clone(),
|
category: category.clone(),
|
||||||
@@ -101,11 +245,7 @@ impl DownloadManager {
|
|||||||
filename,
|
filename,
|
||||||
name,
|
name,
|
||||||
icon: format_icon_url(&category, &pkgname),
|
icon: format_icon_url(&category, &pkgname),
|
||||||
status: initial_status,
|
gid: gids[0].clone()
|
||||||
progress: 0.0,
|
|
||||||
speed: None,
|
|
||||||
size: None,
|
|
||||||
metalink: metalink_data,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加任务到队列
|
// 添加任务到队列
|
||||||
@@ -115,64 +255,95 @@ impl DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 暂停下载任务
|
// 暂停下载任务
|
||||||
pub fn pause_download(&self, category: String, pkgname: String) -> Result<(), String> {
|
pub async 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);
|
let task_id = format!("{}/{}", category, pkgname);
|
||||||
|
|
||||||
if let Some(task) = downloads.get_mut(&task_id) {
|
// 获取任务信息,并在作用域结束时释放锁
|
||||||
let old_status = task.status.clone();
|
let task_gid = {
|
||||||
task.status = DownloadStatus::Paused;
|
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
||||||
if matches!(old_status, DownloadStatus::Downloading) {
|
match downloads.get(&task_id) {
|
||||||
self.start_next_download()?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复下载任务
|
// 恢复下载任务
|
||||||
pub fn resume_download(&self, category: String, pkgname: String) -> Result<(), String> {
|
pub async 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 task_id = format!("{}/{}", category, pkgname);
|
||||||
|
|
||||||
// 先检查任务是否存在且处于暂停状态
|
// 获取任务信息,并在作用域结束时释放锁
|
||||||
let should_resume = downloads
|
let task_gid = {
|
||||||
.get(&task_id)
|
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
||||||
.map(|task| matches!(task.status, DownloadStatus::Paused))
|
match downloads.get(&task_id) {
|
||||||
.unwrap_or(false);
|
Some(task) => task.gid.clone(),
|
||||||
|
None => return Err(format!("找不到下载任务: {}", task_id)),
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消下载任务
|
// 取消下载任务
|
||||||
pub fn cancel_download(&self, category: String, pkgname: String) -> Result<(), String> {
|
pub async 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 task_id = format!("{}/{}", category, pkgname);
|
||||||
|
|
||||||
let was_downloading = downloads
|
// 获取任务信息,并在作用域结束时释放锁
|
||||||
.get(&task_id)
|
let task_gid = {
|
||||||
.map(|task| matches!(task.status, DownloadStatus::Downloading))
|
let mut downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
||||||
.unwrap_or(false);
|
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);
|
downloads.remove(&task_id);
|
||||||
|
|
||||||
if was_downloading {
|
|
||||||
self.start_next_download()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
21
src-tauri/src/utils/format.rs
Normal file
21
src-tauri/src/utils/format.rs
Normal file
@@ -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))
|
||||||
|
}
|
||||||
@@ -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<String, String>,
|
|
||||||
pub urls: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_metalink(content: &str) -> Result<MetalinkFile, String> {
|
|
||||||
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#"<?xml version="1.0" ?>
|
|
||||||
<metalink xmlns="http://www.metalinker.org/" version="3.0">
|
|
||||||
<files>
|
|
||||||
<file name="wechat_4.0.1.11_amd64.deb">
|
|
||||||
<verification>
|
|
||||||
<hash type="md5">a6af060b8a27417ed972b8b004815725</hash>
|
|
||||||
<hash type="sha1">45a8758392ee4feae9da4f53374721138ab66dac</hash>
|
|
||||||
</verification>
|
|
||||||
<resources>
|
|
||||||
<url type="http" location="cn" preference="100">https://mirrors.sdu.edu.cn/spark-store/store/./chat/wechat/wechat_4.0.1.11_amd64.deb</url>
|
|
||||||
<url type="http" location="cn" preference="100">https://spark.home.sunnypai.top:43443/store/./chat/wechat/wechat_4.0.1.11_amd64.deb</url>
|
|
||||||
</resources>
|
|
||||||
</file>
|
|
||||||
</files>
|
|
||||||
</metalink>"#;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::handlers::server::get_img_server_url;
|
use crate::handlers::server::get_img_server_url;
|
||||||
|
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod metalink;
|
|
||||||
pub mod download_manager;
|
pub mod download_manager;
|
||||||
|
pub mod aria2;
|
||||||
|
pub mod format;
|
||||||
|
|
||||||
pub const UA: &str = concat!("Spark-Store/", env!("CARGO_PKG_VERSION"));
|
pub const UA: &str = concat!("Spark-Store/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, createEffect } from 'solid-js';
|
import { createSignal, createEffect, onCleanup } from 'solid-js';
|
||||||
import { DownloadTask } from '@/types/download';
|
import { DownloadTask } from '@/types/download';
|
||||||
import { getDownloads, addDownload as addDownloadApi, pauseDownload as pauseDownloadApi, resumeDownload as resumeDownloadApi, cancelDownload as cancelDownloadApi } from '@/lib/api/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 = () => {
|
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 =>
|
const activeDownloads = () => downloads().filter(item =>
|
||||||
['downloading', 'paused', 'queued'].includes(item.status)
|
['downloading', 'paused', 'queued'].includes(item.status)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user