✨ 添加安装功能
This commit is contained in:
@@ -10,12 +10,20 @@ pub struct DownloadTask {
|
|||||||
pub gid: String,
|
pub gid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct InstallTask {
|
||||||
|
pub category: String,
|
||||||
|
pub pkgname: String,
|
||||||
|
pub filepath: String,
|
||||||
|
pub status: InstallStatus,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct DownloadTaskResponse {
|
pub struct DownloadTaskResponse {
|
||||||
pub category: String,
|
pub category: String,
|
||||||
pub pkgname: String,
|
pub pkgname: String,
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub status: DownloadStatus,
|
pub status: ResponseStatus,
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub progress: f32,
|
pub progress: f32,
|
||||||
@@ -25,10 +33,21 @@ pub struct DownloadTaskResponse {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum DownloadStatus {
|
pub enum ResponseStatus {
|
||||||
Downloading,
|
Downloading, // 下载中
|
||||||
Queued,
|
Queued, // 等待下载
|
||||||
Paused,
|
Paused, // 下载暂停
|
||||||
Completed,
|
Completed, // 下载完成
|
||||||
Error,
|
Error, // 错误
|
||||||
|
Installing, // 安装中
|
||||||
|
Installed, // 安装完成
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum InstallStatus {
|
||||||
|
Queued, // 等待安装
|
||||||
|
Error, // 错误
|
||||||
|
Installing, // 安装中
|
||||||
|
Installed, // 安装完成
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Aria2Client {
|
pub struct Aria2Client {
|
||||||
|
|||||||
@@ -4,26 +4,30 @@ use std::process::Command;
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use crate::models::download::{DownloadStatus, DownloadTask, DownloadTaskResponse};
|
use crate::models::download::{DownloadTask, DownloadTaskResponse, InstallStatus, InstallTask, ResponseStatus};
|
||||||
use crate::handlers::server::get_json_server_url;
|
use crate::handlers::server::get_json_server_url;
|
||||||
use crate::utils::{format_icon_url, UA, aria2::Aria2Client};
|
use crate::utils::{format_icon_url, UA, aria2::Aria2Client};
|
||||||
|
|
||||||
use super::format::{format_size, format_speed};
|
use super::format::{format_size, format_speed};
|
||||||
|
|
||||||
pub struct DownloadManager {
|
pub struct DownloadManager {
|
||||||
queue: Mutex<HashMap<String, DownloadTask>>,
|
download_queue: Mutex<HashMap<String, DownloadTask>>,
|
||||||
|
install_queue: Mutex<HashMap<String, InstallTask>>,
|
||||||
aria2_started: Arc<AtomicBool>,
|
aria2_started: Arc<AtomicBool>,
|
||||||
aria2_port: Arc<Mutex<u16>>,
|
aria2_port: Arc<Mutex<u16>>,
|
||||||
aria2_pid: Arc<Mutex<Option<u32>>>,
|
aria2_pid: Arc<Mutex<Option<u32>>>,
|
||||||
|
installing: Arc<AtomicBool>, // 新增:标记是否有正在安装的任务
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DownloadManager {
|
impl DownloadManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
DownloadManager {
|
DownloadManager {
|
||||||
queue: Mutex::new(HashMap::new()),
|
download_queue: Mutex::new(HashMap::new()),
|
||||||
|
install_queue: Mutex::new(HashMap::new()),
|
||||||
aria2_started: Arc::new(AtomicBool::new(false)),
|
aria2_started: Arc::new(AtomicBool::new(false)),
|
||||||
aria2_port: Arc::new(Mutex::new(5144)),
|
aria2_port: Arc::new(Mutex::new(5144)),
|
||||||
aria2_pid: Arc::new(Mutex::new(None)),
|
aria2_pid: Arc::new(Mutex::new(None)),
|
||||||
|
installing: Arc::new(AtomicBool::new(false)), // 初始化为 false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +40,7 @@ impl DownloadManager {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// 使用作用域限制锁的生命周期
|
// 使用作用域限制锁的生命周期
|
||||||
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let downloads = self.download_queue.lock().map_err(|e| e.to_string())?;
|
||||||
tasks_clone = downloads.values().cloned().collect();
|
tasks_clone = downloads.values().cloned().collect();
|
||||||
aria2_started = self.aria2_started.load(Ordering::SeqCst);
|
aria2_started = self.aria2_started.load(Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,7 @@ impl DownloadManager {
|
|||||||
category: task.category,
|
category: task.category,
|
||||||
pkgname: task.pkgname,
|
pkgname: task.pkgname,
|
||||||
filename: task.filename,
|
filename: task.filename,
|
||||||
status: DownloadStatus::Error, // 如果 aria2 未启动,标记为错误状态
|
status: ResponseStatus::Error, // 如果 aria2 未启动,标记为错误状态
|
||||||
icon: task.icon,
|
icon: task.icon,
|
||||||
name: task.name,
|
name: task.name,
|
||||||
progress: 0.0,
|
progress: 0.0,
|
||||||
@@ -82,7 +86,7 @@ impl DownloadManager {
|
|||||||
// 处理活动中的下载任务
|
// 处理活动中的下载任务
|
||||||
for task in active_downloads {
|
for task in active_downloads {
|
||||||
if let Some(gid) = task["gid"].as_str() {
|
if let Some(gid) = task["gid"].as_str() {
|
||||||
aria2_tasks.insert(gid.to_string(), (task, DownloadStatus::Downloading));
|
aria2_tasks.insert(gid.to_string(), (task, ResponseStatus::Downloading));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +94,9 @@ impl DownloadManager {
|
|||||||
for task in waiting_downloads {
|
for task in waiting_downloads {
|
||||||
if let Some(gid) = task["gid"].as_str() {
|
if let Some(gid) = task["gid"].as_str() {
|
||||||
let status = if task["status"].as_str() == Some("paused") {
|
let status = if task["status"].as_str() == Some("paused") {
|
||||||
DownloadStatus::Paused
|
ResponseStatus::Paused
|
||||||
} else {
|
} else {
|
||||||
DownloadStatus::Queued
|
ResponseStatus::Queued
|
||||||
};
|
};
|
||||||
aria2_tasks.insert(gid.to_string(), (task, status));
|
aria2_tasks.insert(gid.to_string(), (task, status));
|
||||||
}
|
}
|
||||||
@@ -102,9 +106,40 @@ impl DownloadManager {
|
|||||||
for task in stopped_downloads {
|
for task in stopped_downloads {
|
||||||
if let Some(gid) = task["gid"].as_str() {
|
if let Some(gid) = task["gid"].as_str() {
|
||||||
let status = if task["status"].as_str() == Some("complete") {
|
let status = if task["status"].as_str() == Some("complete") {
|
||||||
DownloadStatus::Completed
|
// 当任务完成时,将其加入到安装队列
|
||||||
|
let task_info = {
|
||||||
|
let downloads = self.download_queue.lock().map_err(|e| e.to_string())?;
|
||||||
|
downloads.values()
|
||||||
|
.find(|t| t.gid == gid)
|
||||||
|
.cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(task_info) = task_info {
|
||||||
|
let task_id = format!("{}/{}", task_info.category, task_info.pkgname);
|
||||||
|
let should_process = {
|
||||||
|
let mut install_queue = self.install_queue.lock().map_err(|e| e.to_string())?;
|
||||||
|
if !install_queue.contains_key(&task_id) {
|
||||||
|
let install_task = InstallTask {
|
||||||
|
category: task_info.category.clone(),
|
||||||
|
pkgname: task_info.pkgname.clone(),
|
||||||
|
filepath: task["files"][0]["path"].to_string(),
|
||||||
|
status: InstallStatus::Queued,
|
||||||
|
};
|
||||||
|
install_queue.insert(task_id, install_task);
|
||||||
|
true
|
||||||
} else {
|
} else {
|
||||||
DownloadStatus::Error
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在所有锁都释放后再处理安装队列
|
||||||
|
if should_process {
|
||||||
|
let _ = self.process_install_queue().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResponseStatus::Completed
|
||||||
|
} else {
|
||||||
|
ResponseStatus::Error
|
||||||
};
|
};
|
||||||
aria2_tasks.insert(gid.to_string(), (task, status));
|
aria2_tasks.insert(gid.to_string(), (task, status));
|
||||||
}
|
}
|
||||||
@@ -113,12 +148,17 @@ impl DownloadManager {
|
|||||||
// 将队列中的任务与 aria2 任务状态结合,生成响应
|
// 将队列中的任务与 aria2 任务状态结合,生成响应
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
// 获取安装队列(在生成响应之前)
|
||||||
|
let install_queue = self.install_queue.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
for task in tasks_clone {
|
for task in tasks_clone {
|
||||||
|
let task_id = format!("{}/{}", task.category, task.pkgname);
|
||||||
|
|
||||||
let mut response = DownloadTaskResponse {
|
let mut response = DownloadTaskResponse {
|
||||||
category: task.category.clone(),
|
category: task.category.clone(),
|
||||||
pkgname: task.pkgname.clone(),
|
pkgname: task.pkgname.clone(),
|
||||||
filename: task.filename.clone(),
|
filename: task.filename.clone(),
|
||||||
status: DownloadStatus::Error, // 默认为错误状态
|
status: ResponseStatus::Error, // 默认为错误状态
|
||||||
icon: task.icon.clone(),
|
icon: task.icon.clone(),
|
||||||
name: task.name.clone(),
|
name: task.name.clone(),
|
||||||
progress: 0.0,
|
progress: 0.0,
|
||||||
@@ -126,7 +166,16 @@ impl DownloadManager {
|
|||||||
size: None,
|
size: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果在 aria2 任务中找到对应的 GID,更新状态信息
|
// 首先检查是否在安装队列中
|
||||||
|
if let Some(install_task) = install_queue.get(&task_id) {
|
||||||
|
response.status = match install_task.status {
|
||||||
|
InstallStatus::Queued => ResponseStatus::Completed,
|
||||||
|
InstallStatus::Installing => ResponseStatus::Installing,
|
||||||
|
InstallStatus::Installed => ResponseStatus::Installed,
|
||||||
|
InstallStatus::Error => ResponseStatus::Error,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 如果不在安装队列中,则检查下载状态
|
||||||
if let Some((aria2_task, status)) = aria2_tasks.get(&task.gid) {
|
if let Some((aria2_task, status)) = aria2_tasks.get(&task.gid) {
|
||||||
response.status = status.clone();
|
response.status = status.clone();
|
||||||
|
|
||||||
@@ -136,7 +185,6 @@ impl DownloadManager {
|
|||||||
aria2_task["totalLength"].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 {
|
if total_length > 0.0 {
|
||||||
// 保留两位小数
|
|
||||||
let progress = ((completed_length / total_length) * 100.0) as f32;
|
let progress = ((completed_length / total_length) * 100.0) as f32;
|
||||||
response.progress = (progress * 100.0).round() / 100.0;
|
response.progress = (progress * 100.0).round() / 100.0;
|
||||||
}
|
}
|
||||||
@@ -156,6 +204,7 @@ impl DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.push(response);
|
result.push(response);
|
||||||
}
|
}
|
||||||
@@ -238,7 +287,7 @@ impl DownloadManager {
|
|||||||
.map_err(|e| format!("添加下载任务失败: {}", e))?;
|
.map_err(|e| format!("添加下载任务失败: {}", e))?;
|
||||||
|
|
||||||
// 获取一次锁,完成所有状态更新操作
|
// 获取一次锁,完成所有状态更新操作
|
||||||
let mut downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// 检查任务是否已存在
|
// 检查任务是否已存在
|
||||||
if downloads.contains_key(&task_id) {
|
if downloads.contains_key(&task_id) {
|
||||||
@@ -267,7 +316,7 @@ impl DownloadManager {
|
|||||||
|
|
||||||
// 获取任务信息,并在作用域结束时释放锁
|
// 获取任务信息,并在作用域结束时释放锁
|
||||||
let task_gid = {
|
let task_gid = {
|
||||||
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let downloads = self.download_queue.lock().map_err(|e| e.to_string())?;
|
||||||
match downloads.get(&task_id) {
|
match downloads.get(&task_id) {
|
||||||
Some(task) => task.gid.clone(),
|
Some(task) => task.gid.clone(),
|
||||||
None => return Err(format!("找不到下载任务: {}", task_id)),
|
None => return Err(format!("找不到下载任务: {}", task_id)),
|
||||||
@@ -296,7 +345,7 @@ impl DownloadManager {
|
|||||||
|
|
||||||
// 获取任务信息,并在作用域结束时释放锁
|
// 获取任务信息,并在作用域结束时释放锁
|
||||||
let task_gid = {
|
let task_gid = {
|
||||||
let downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let downloads = self.download_queue.lock().map_err(|e| e.to_string())?;
|
||||||
match downloads.get(&task_id) {
|
match downloads.get(&task_id) {
|
||||||
Some(task) => task.gid.clone(),
|
Some(task) => task.gid.clone(),
|
||||||
None => return Err(format!("找不到下载任务: {}", task_id)),
|
None => return Err(format!("找不到下载任务: {}", task_id)),
|
||||||
@@ -325,7 +374,7 @@ impl DownloadManager {
|
|||||||
|
|
||||||
// 获取任务信息,并在作用域结束时释放锁
|
// 获取任务信息,并在作用域结束时释放锁
|
||||||
let task_gid = {
|
let task_gid = {
|
||||||
let mut downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?;
|
||||||
match downloads.get(&task_id) {
|
match downloads.get(&task_id) {
|
||||||
Some(task) => {
|
Some(task) => {
|
||||||
// 如果 aria2 未启动,只从队列中移除任务
|
// 如果 aria2 未启动,只从队列中移除任务
|
||||||
@@ -348,7 +397,7 @@ impl DownloadManager {
|
|||||||
.map_err(|e| format!("取消下载任务失败: {}", e))?;
|
.map_err(|e| format!("取消下载任务失败: {}", e))?;
|
||||||
|
|
||||||
// 从队列中移除任务
|
// 从队列中移除任务
|
||||||
let mut downloads = self.queue.lock().map_err(|e| e.to_string())?;
|
let mut downloads = self.download_queue.lock().map_err(|e| e.to_string())?;
|
||||||
downloads.remove(&task_id);
|
downloads.remove(&task_id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -375,4 +424,82 @@ impl DownloadManager {
|
|||||||
self.aria2_started.store(false, Ordering::SeqCst);
|
self.aria2_started.store(false, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开始安装任务
|
||||||
|
pub async fn process_install_queue(&self) -> Result<(), String> {
|
||||||
|
// 如果已经有任务在安装,直接返回
|
||||||
|
if self.installing.load(Ordering::SeqCst) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找第一个等待安装的任务
|
||||||
|
let (task_id, task) = {
|
||||||
|
let mut install_queue = self.install_queue.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some((id, task)) = install_queue.iter_mut()
|
||||||
|
.find(|(_, task)| matches!(task.status, InstallStatus::Queued))
|
||||||
|
.map(|(id, task)| (id.clone(), task.clone()))
|
||||||
|
{
|
||||||
|
// 更新任务状态为安装中
|
||||||
|
if let Some(task) = install_queue.get_mut(&id) {
|
||||||
|
task.status = InstallStatus::Installing;
|
||||||
|
}
|
||||||
|
(id, task)
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记为正在安装
|
||||||
|
self.installing.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
// 执行安装操作
|
||||||
|
let install_result = self.install_package(&task).await;
|
||||||
|
|
||||||
|
// 更新任务状态
|
||||||
|
{
|
||||||
|
let mut install_queue = self.install_queue.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(task) = install_queue.get_mut(&task_id) {
|
||||||
|
task.status = match &install_result {
|
||||||
|
Ok(_) => InstallStatus::Installed,
|
||||||
|
Err(_) => InstallStatus::Error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装完成,重置安装状态
|
||||||
|
self.installing.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
|
// 如果安装失败,返回错误
|
||||||
|
if let Err(e) = install_result {
|
||||||
|
return Err(format!("安装失败: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实际执行安装的方法
|
||||||
|
async fn install_package(&self, task: &InstallTask) -> Result<(), String> {
|
||||||
|
println!("开始安装包: {}", task.filepath);
|
||||||
|
|
||||||
|
let output = Command::new("pkexec")
|
||||||
|
.arg("ssinstall")
|
||||||
|
.arg(&task.filepath)
|
||||||
|
.arg("--delete-after-install")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| {
|
||||||
|
println!("安装命令执行失败: {}", e);
|
||||||
|
format!("执行安装命令失败: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
println!("安装成功完成");
|
||||||
|
println!("命令输出: {}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let error_msg = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
println!("安装失败: {}", error_msg);
|
||||||
|
println!("命令输出: {}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
Err(error_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,11 @@ const DownloadCard: Component<{ download: DownloadTask }> = (props) => {
|
|||||||
<div class="text-sm text-muted-foreground">
|
<div class="text-sm text-muted-foreground">
|
||||||
{props.download.status === 'queued' ? '排队中' :
|
{props.download.status === 'queued' ? '排队中' :
|
||||||
props.download.status === 'downloading' && props.download.speed ?
|
props.download.status === 'downloading' && props.download.speed ?
|
||||||
`${props.download.speed} - ` : ''}
|
`${props.download.speed} - ` :
|
||||||
|
props.download.status === 'error' ? '失败' :
|
||||||
|
props.download.status === 'installing' ? '正在安装' :
|
||||||
|
props.download.status === 'installed' ? '安装完成' :
|
||||||
|
''}
|
||||||
{props.download.status !== 'queued' && props.download.size}
|
{props.download.status !== 'queued' && props.download.size}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export const useDownloadsStore = () => {
|
|||||||
onCleanup(() => clearInterval(intervalId));
|
onCleanup(() => clearInterval(intervalId));
|
||||||
|
|
||||||
const activeDownloads = () => downloads().filter(item =>
|
const activeDownloads = () => downloads().filter(item =>
|
||||||
['downloading', 'paused', 'queued'].includes(item.status)
|
['downloading', 'paused', 'queued', 'completed', 'installing'].includes(item.status)
|
||||||
);
|
);
|
||||||
|
|
||||||
const completedDownloads = () => downloads().filter(item =>
|
const completedDownloads = () => downloads().filter(item =>
|
||||||
item.status === 'completed'
|
['installed', 'error'].includes(item.status)
|
||||||
);
|
);
|
||||||
|
|
||||||
const addDownload = async (category: string, pkgname: string, filename: string, name: string) => {
|
const addDownload = async (category: string, pkgname: string, filename: string, name: string) => {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ export enum DownloadStatus {
|
|||||||
queued = 'queued',
|
queued = 'queued',
|
||||||
paused = 'paused',
|
paused = 'paused',
|
||||||
completed = 'completed',
|
completed = 'completed',
|
||||||
error = 'error'
|
error = 'error',
|
||||||
|
installing = 'installing',
|
||||||
|
installed = 'installed'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载任务接口
|
// 下载任务接口
|
||||||
|
|||||||
Reference in New Issue
Block a user