性能优化
This commit is contained in:
@@ -8,12 +8,14 @@ use super::task::{DownloadTask, DownloaderSettings};
|
||||
|
||||
/// 获取所有任务
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_get_tasks(engine: State<'_, DownloadEngine>) -> Vec<DownloadTask> {
|
||||
engine.get_tasks()
|
||||
}
|
||||
|
||||
/// 检查 URL 重复性并探测文件信息(添加下载前调用)
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn downloader_check_url(
|
||||
engine: State<'_, DownloadEngine>,
|
||||
url: String,
|
||||
@@ -47,6 +49,7 @@ pub async fn downloader_check_url(
|
||||
|
||||
/// 添加下载任务
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn downloader_add_task(
|
||||
engine: State<'_, DownloadEngine>,
|
||||
url: String,
|
||||
@@ -60,18 +63,21 @@ pub async fn downloader_add_task(
|
||||
|
||||
/// 暂停任务
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_pause_task(engine: State<'_, DownloadEngine>, id: String) -> Result<(), String> {
|
||||
engine.pause_task(&id)
|
||||
}
|
||||
|
||||
/// 恢复任务
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_resume_task(engine: State<'_, DownloadEngine>, id: String) -> Result<(), String> {
|
||||
engine.resume_task(&id)
|
||||
}
|
||||
|
||||
/// 移除任务
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_remove_task(
|
||||
engine: State<'_, DownloadEngine>,
|
||||
id: String,
|
||||
@@ -82,12 +88,14 @@ pub fn downloader_remove_task(
|
||||
|
||||
/// 获取设置
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_get_settings(engine: State<'_, DownloadEngine>) -> DownloaderSettings {
|
||||
engine.get_settings()
|
||||
}
|
||||
|
||||
/// 保存设置
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_save_settings(
|
||||
engine: State<'_, DownloadEngine>,
|
||||
settings: DownloaderSettings,
|
||||
@@ -118,6 +126,7 @@ pub fn downloader_get_extension_info(engine: State<'_, DownloadEngine>) -> serde
|
||||
|
||||
/// 用系统资源管理器打开目录
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_open_dir(app: AppHandle, path: String) -> Result<(), String> {
|
||||
app.opener()
|
||||
.open_path(path, None::<&str>)
|
||||
@@ -126,6 +135,7 @@ pub fn downloader_open_dir(app: AppHandle, path: String) -> Result<(), String> {
|
||||
|
||||
/// 用系统默认浏览器打开 URL
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn downloader_open_url(app: AppHandle, url: String) -> Result<(), String> {
|
||||
app.opener()
|
||||
.open_url(url, None::<&str>)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -10,9 +10,10 @@ use super::http_dl::{HttpDownloader, split_segments};
|
||||
use super::rate_limit::RateLimiter;
|
||||
use super::storage::{EngineState, Storage};
|
||||
use super::task::{DownloadTask, DownloaderSettings, ProbeResult, Segment, TaskStatus};
|
||||
use specta::Type;
|
||||
|
||||
/// 重复类型
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum DuplicateKind {
|
||||
/// 无重复
|
||||
@@ -26,7 +27,7 @@ pub enum DuplicateKind {
|
||||
}
|
||||
|
||||
/// 已存在的任务信息(用于前端展示)
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExistingTaskInfo {
|
||||
pub id: String,
|
||||
@@ -35,7 +36,7 @@ pub struct ExistingTaskInfo {
|
||||
}
|
||||
|
||||
/// check_url 命令返回的结果
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckUrlResult {
|
||||
/// 探测是否成功
|
||||
@@ -77,6 +78,9 @@ pub struct CompletePayload {
|
||||
|
||||
/// 活跃下载句柄
|
||||
struct TaskHandle {
|
||||
/// 代际号:同一任务每次 start_download 递增。
|
||||
/// 旧代际任务完成时不删除新任务句柄、不覆盖新任务状态(防 pause→resume 竞态)
|
||||
gen: u64,
|
||||
cancel: Arc<AtomicBool>,
|
||||
/// 每个分段的已下载字节(与 segments 一一对应)
|
||||
progress: Vec<Arc<std::sync::atomic::AtomicU64>>,
|
||||
@@ -107,6 +111,8 @@ struct EngineInner {
|
||||
app_handle: AppHandle,
|
||||
/// 引擎是否已启动
|
||||
started: AtomicBool,
|
||||
/// 任务代际计数器(每次 start_download 递增,分配给新句柄)
|
||||
next_gen: AtomicU64,
|
||||
/// 持久化节流:上次保存时间
|
||||
last_save: Mutex<Instant>,
|
||||
}
|
||||
@@ -149,6 +155,7 @@ impl DownloadEngine {
|
||||
http: HttpDownloader::new(),
|
||||
app_handle,
|
||||
started: AtomicBool::new(false),
|
||||
next_gen: AtomicU64::new(0),
|
||||
last_save: Mutex::new(Instant::now()),
|
||||
}),
|
||||
};
|
||||
@@ -182,7 +189,7 @@ impl DownloadEngine {
|
||||
headers: &HashMap<String, String>,
|
||||
) -> (Result<ProbeResult, String>, DuplicateKind, Option<ExistingTaskInfo>) {
|
||||
let probe = self.inner.http.probe(url, headers).await;
|
||||
let settings = self.inner.settings.lock().unwrap().clone();
|
||||
let settings = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let task_dir = dir.map(|d| d.to_string()).unwrap_or_else(|| settings.download_dir.clone());
|
||||
|
||||
let filename = probe.as_ref().ok()
|
||||
@@ -199,7 +206,7 @@ impl DownloadEngine {
|
||||
let mut existing: Option<ExistingTaskInfo> = None;
|
||||
|
||||
{
|
||||
let tasks = self.inner.tasks.lock().unwrap();
|
||||
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for t in tasks.values() {
|
||||
// URL 完全相同
|
||||
if t.url == url {
|
||||
@@ -280,7 +287,7 @@ impl DownloadEngine {
|
||||
// 探测资源信息
|
||||
let probe = self.inner.http.probe(&url, &headers).await;
|
||||
|
||||
let settings = self.inner.settings.lock().unwrap().clone();
|
||||
let settings = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let task_dir = dir.unwrap_or_else(|| settings.download_dir.clone());
|
||||
|
||||
// 确定文件名
|
||||
@@ -347,14 +354,14 @@ impl DownloadEngine {
|
||||
};
|
||||
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks.insert(id.clone(), task);
|
||||
}
|
||||
self.persist_now();
|
||||
|
||||
// 如果探测成功,尝试调度
|
||||
let should_schedule = {
|
||||
let tasks = self.inner.tasks.lock().unwrap();
|
||||
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks.get(&id).map(|t| t.status == TaskStatus::Queued).unwrap_or(false)
|
||||
};
|
||||
if should_schedule {
|
||||
@@ -362,7 +369,7 @@ impl DownloadEngine {
|
||||
}
|
||||
|
||||
// 通知前端有新任务加入(扩展通过 HTTP API 添加时,前端需要刷新)
|
||||
let _ = self.inner.app_handle.emit("download-added", serde_json::json!({ "id": id }));
|
||||
let _ = self.inner.app_handle.emit(crate::constants::events::DOWNLOAD_ADDED, serde_json::json!({ "id": id }));
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
@@ -371,7 +378,7 @@ impl DownloadEngine {
|
||||
pub fn pause_task(&self, id: &str) -> Result<(), String> {
|
||||
// 1. 设置取消标志 + 读取进度(锁 handles)
|
||||
let progress_values: Vec<u64> = {
|
||||
let handles = self.inner.handles.lock().unwrap();
|
||||
let handles = self.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(handle) = handles.get(id) {
|
||||
handle.cancel.store(true, Ordering::SeqCst);
|
||||
handle.progress.iter().map(|p| p.load(Ordering::Relaxed)).collect()
|
||||
@@ -382,7 +389,7 @@ impl DownloadEngine {
|
||||
|
||||
// 2. 更新任务状态 + 同步进度(锁 tasks,不嵌套锁 handles)
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(task) = tasks.get_mut(id) {
|
||||
if task.status == TaskStatus::Active || task.status == TaskStatus::Queued {
|
||||
task.status = TaskStatus::Paused;
|
||||
@@ -407,7 +414,7 @@ impl DownloadEngine {
|
||||
/// 恢复任务
|
||||
pub fn resume_task(&self, id: &str) -> Result<(), String> {
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(task) = tasks.get_mut(id) {
|
||||
if task.status != TaskStatus::Paused && task.status != TaskStatus::Error {
|
||||
return Err("任务不在可恢复状态".to_string());
|
||||
@@ -434,7 +441,7 @@ impl DownloadEngine {
|
||||
pub fn remove_task(&self, id: &str, delete_files: bool) -> Result<(), String> {
|
||||
// 1. 设置取消标志 + abort join handle(锁 handles)
|
||||
{
|
||||
let mut handles = self.inner.handles.lock().unwrap();
|
||||
let mut handles = self.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(handle) = handles.remove(id) {
|
||||
handle.cancel.store(true, Ordering::SeqCst);
|
||||
if let Ok(mut join) = handle.join.lock() {
|
||||
@@ -447,7 +454,7 @@ impl DownloadEngine {
|
||||
|
||||
// 2. 从任务列表移除(锁 tasks)
|
||||
let task = {
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks.remove(id)
|
||||
};
|
||||
|
||||
@@ -466,12 +473,12 @@ impl DownloadEngine {
|
||||
|
||||
/// 获取所有任务
|
||||
pub fn get_tasks(&self) -> Vec<DownloadTask> {
|
||||
self.inner.tasks.lock().unwrap().values().cloned().collect()
|
||||
self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner()).values().cloned().collect()
|
||||
}
|
||||
|
||||
/// 获取设置
|
||||
pub fn get_settings(&self) -> DownloaderSettings {
|
||||
self.inner.settings.lock().unwrap().clone()
|
||||
self.inner.settings.lock().unwrap_or_else(|e| e.into_inner()).clone()
|
||||
}
|
||||
|
||||
/// 保存设置
|
||||
@@ -485,7 +492,7 @@ impl DownloadEngine {
|
||||
self.inner.global_limiter.set_limit(new_limit);
|
||||
|
||||
{
|
||||
let mut s = self.inner.settings.lock().unwrap();
|
||||
let mut s = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*s = settings;
|
||||
}
|
||||
self.persist_now();
|
||||
@@ -500,7 +507,7 @@ impl DownloadEngine {
|
||||
pub fn cleanup_on_exit(&self) {
|
||||
// 取消所有活跃下载
|
||||
{
|
||||
let handles = self.inner.handles.lock().unwrap();
|
||||
let handles = self.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for handle in handles.values() {
|
||||
handle.cancel.store(true, Ordering::SeqCst);
|
||||
}
|
||||
@@ -508,7 +515,7 @@ impl DownloadEngine {
|
||||
|
||||
// 将 Active 任务标记为 Paused
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for task in tasks.values_mut() {
|
||||
if task.status == TaskStatus::Active {
|
||||
task.status = TaskStatus::Paused;
|
||||
@@ -528,10 +535,10 @@ impl DownloadEngine {
|
||||
|
||||
/// 调度:如果活跃任务数 < max_concurrent,启动排队任务
|
||||
fn schedule(&self) {
|
||||
let max_concurrent = self.inner.settings.lock().unwrap().max_concurrent as usize;
|
||||
let max_concurrent = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner()).max_concurrent as usize;
|
||||
|
||||
let (active_count, queued_ids) = {
|
||||
let tasks = self.inner.tasks.lock().unwrap();
|
||||
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let active = tasks.values().filter(|t| t.status == TaskStatus::Active).count();
|
||||
let mut queued: Vec<_> = tasks
|
||||
.values()
|
||||
@@ -554,7 +561,7 @@ impl DownloadEngine {
|
||||
/// 启动单个下载任务
|
||||
fn start_download(&self, id: String) {
|
||||
let task = {
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match tasks.get_mut(&id) {
|
||||
Some(task) if task.status == TaskStatus::Queued => {
|
||||
task.status = TaskStatus::Active;
|
||||
@@ -565,6 +572,9 @@ impl DownloadEngine {
|
||||
}
|
||||
};
|
||||
|
||||
// 分配代际号(同一任务每次重启递增)
|
||||
let gen = self.inner.next_gen.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
|
||||
// 创建取消标志和进度计数器
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let progress: Vec<Arc<std::sync::atomic::AtomicU64>> = task
|
||||
@@ -573,13 +583,22 @@ impl DownloadEngine {
|
||||
.map(|s| Arc::new(std::sync::atomic::AtomicU64::new(s.completed)))
|
||||
.collect();
|
||||
|
||||
// 存储句柄
|
||||
let handle = TaskHandle {
|
||||
cancel: cancel.clone(),
|
||||
progress: progress.iter().map(|p| p.clone()).collect(),
|
||||
join: Mutex::new(None),
|
||||
};
|
||||
self.inner.handles.lock().unwrap().insert(id.clone(), handle);
|
||||
// 存储句柄:先取消旧代际(若存在),确保旧任务尽快退出,避免新旧并发写同一临时文件
|
||||
{
|
||||
let mut handles = self.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(old) = handles.get(&id) {
|
||||
old.cancel.store(true, Ordering::SeqCst);
|
||||
}
|
||||
handles.insert(
|
||||
id.clone(),
|
||||
TaskHandle {
|
||||
gen,
|
||||
cancel: cancel.clone(),
|
||||
progress: progress.iter().map(|p| p.clone()).collect(),
|
||||
join: Mutex::new(None),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 生成下载 future
|
||||
let engine = self.clone();
|
||||
@@ -596,69 +615,94 @@ impl DownloadEngine {
|
||||
let http = self.inner.http.clone();
|
||||
|
||||
let join = tauri::async_runtime::spawn(async move {
|
||||
let my_gen = gen;
|
||||
let result = http
|
||||
.download(&url, &headers, &segments, &temp_file_path, cancel_clone, &progress_clone, limiter)
|
||||
.await;
|
||||
|
||||
// 下载结束,更新任务状态
|
||||
let final_status = match &result {
|
||||
Ok(()) => TaskStatus::Complete,
|
||||
Err(e) if e == "已取消" => TaskStatus::Paused,
|
||||
Err(_) => TaskStatus::Error,
|
||||
// 代际守卫:仅最新代际的任务能更新状态 / 移除句柄 / 发完成事件。
|
||||
// pause→resume 后旧代际任务才退出,此时句柄已被新代际替换,
|
||||
// 若仍按旧逻辑执行会覆盖新任务状态并误删新句柄(pause/remove 失效)
|
||||
let is_current = {
|
||||
let handles = engine.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
handles.get(&id_clone).map(|h| h.gen == my_gen).unwrap_or(false)
|
||||
};
|
||||
|
||||
// 下载成功后,将临时文件重命名为最终文件名
|
||||
if final_status == TaskStatus::Complete {
|
||||
let _ = tokio::fs::rename(&temp_file_path, &final_file_path).await;
|
||||
}
|
||||
if is_current {
|
||||
// 下载结束,更新任务状态
|
||||
let mut final_status = match &result {
|
||||
Ok(()) => TaskStatus::Complete,
|
||||
Err(e) if e == "已取消" => TaskStatus::Paused,
|
||||
Err(_) => TaskStatus::Error,
|
||||
};
|
||||
let mut final_error: Option<String> = match &result {
|
||||
Err(e) if e != "已取消" => Some(e.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// 同步最终进度到任务
|
||||
{
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap();
|
||||
if let Some(task) = tasks.get_mut(&id_clone) {
|
||||
for (i, prog) in progress_clone.iter().enumerate() {
|
||||
if let Some(seg) = task.segments.get_mut(i) {
|
||||
seg.completed = prog.load(Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
task.recalc_completed();
|
||||
task.speed = 0;
|
||||
task.status = final_status.clone();
|
||||
if let Err(e) = &result {
|
||||
if e != "已取消" {
|
||||
task.error = Some(e.clone());
|
||||
}
|
||||
}
|
||||
if final_status == TaskStatus::Complete {
|
||||
task.completed_size = task.total_size.max(task.completed_size);
|
||||
// 下载成功后,将临时文件重命名为最终文件名
|
||||
if final_status == TaskStatus::Complete {
|
||||
if let Err(e) = tokio::fs::rename(&temp_file_path, &final_file_path).await {
|
||||
// 重命名失败(如目标被占用/路径不可写)→ 置 Error,
|
||||
// 避免"标记完成但文件缺失"的状态不一致
|
||||
final_status = TaskStatus::Error;
|
||||
final_error = Some(format!("移动文件到最终路径失败: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 同步最终进度到任务
|
||||
{
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(task) = tasks.get_mut(&id_clone) {
|
||||
for (i, prog) in progress_clone.iter().enumerate() {
|
||||
if let Some(seg) = task.segments.get_mut(i) {
|
||||
seg.completed = prog.load(Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
task.recalc_completed();
|
||||
task.speed = 0;
|
||||
task.status = final_status.clone();
|
||||
if let Some(e) = final_error {
|
||||
task.error = Some(e);
|
||||
}
|
||||
if final_status == TaskStatus::Complete {
|
||||
task.completed_size = task.total_size.max(task.completed_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从活跃句柄中移除(仅移除自己代际的句柄)
|
||||
{
|
||||
let mut handles = engine.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(h) = handles.get(&id_clone) {
|
||||
if h.gen == my_gen {
|
||||
handles.remove(&id_clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送完成事件
|
||||
let task = engine.inner.tasks.lock().unwrap_or_else(|e| e.into_inner()).get(&id_clone).cloned();
|
||||
if let Some(task) = task {
|
||||
let _ = engine.inner.app_handle.emit(
|
||||
"download-complete",
|
||||
CompletePayload {
|
||||
id: id_clone.clone(),
|
||||
filename: task.filename.clone(),
|
||||
status: task.status.clone(),
|
||||
error: task.error.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 持久化 + 调度下一个
|
||||
engine.persist_now();
|
||||
engine.schedule();
|
||||
}
|
||||
|
||||
// 从活跃句柄中移除
|
||||
engine.inner.handles.lock().unwrap().remove(&id_clone);
|
||||
|
||||
// 发送完成事件
|
||||
let task = engine.inner.tasks.lock().unwrap().get(&id_clone).cloned();
|
||||
if let Some(task) = task {
|
||||
let _ = engine.inner.app_handle.emit(
|
||||
"download-complete",
|
||||
CompletePayload {
|
||||
id: id_clone.clone(),
|
||||
filename: task.filename.clone(),
|
||||
status: task.status.clone(),
|
||||
error: task.error.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 持久化 + 调度下一个
|
||||
engine.persist_now();
|
||||
engine.schedule();
|
||||
});
|
||||
|
||||
// 存储 JoinHandle
|
||||
if let Some(h) = self.inner.handles.lock().unwrap().get_mut(&id) {
|
||||
if let Some(h) = self.inner.handles.lock().unwrap_or_else(|e| e.into_inner()).get_mut(&id) {
|
||||
if let Ok(mut join_guard) = h.join.lock() {
|
||||
*join_guard = Some(join);
|
||||
}
|
||||
@@ -670,6 +714,7 @@ impl DownloadEngine {
|
||||
let progress_monitor = progress;
|
||||
let cancel_monitor = cancel;
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let my_gen = gen;
|
||||
// 初始化为当前已下载量,避免恢复下载时首次计算速度异常
|
||||
let mut last_completed: u64 = progress_monitor
|
||||
.iter()
|
||||
@@ -681,9 +726,17 @@ impl DownloadEngine {
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
// 代际守卫:pause→resume 后旧监控立即退出,避免用过期进度覆盖新任务
|
||||
let is_current = {
|
||||
let handles = engine.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
handles.get(&id_monitor).map(|h| h.gen == my_gen).unwrap_or(false)
|
||||
};
|
||||
if !is_current {
|
||||
break;
|
||||
}
|
||||
// 如果任务已不在活跃状态,停止监控
|
||||
let is_active = {
|
||||
let tasks = engine.inner.tasks.lock().unwrap();
|
||||
let tasks = engine.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks.get(&id_monitor).map(|t| t.status == TaskStatus::Active).unwrap_or(false)
|
||||
};
|
||||
if !is_active || cancel_monitor.load(Ordering::SeqCst) {
|
||||
@@ -709,7 +762,7 @@ impl DownloadEngine {
|
||||
|
||||
// 更新任务状态 + 发送进度事件
|
||||
let total_size = {
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap();
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(task) = tasks.get_mut(&id_monitor) {
|
||||
task.completed_size = completed;
|
||||
task.speed = speed;
|
||||
@@ -743,7 +796,7 @@ impl DownloadEngine {
|
||||
/// 节流持久化(至少间隔 3 秒)
|
||||
fn persist_throttled(&self) {
|
||||
let should_save = {
|
||||
let last = self.inner.last_save.lock().unwrap();
|
||||
let last = self.inner.last_save.lock().unwrap_or_else(|e| e.into_inner());
|
||||
last.elapsed() >= Duration::from_secs(3)
|
||||
};
|
||||
if should_save {
|
||||
@@ -751,19 +804,30 @@ impl DownloadEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// 立即持久化
|
||||
/// 立即持久化。
|
||||
/// 保存失败(磁盘不可写/rename 失败)时把进行中任务标记为 Error,
|
||||
/// 防止用户误以为任务已持久化而关闭应用导致数据丢失。
|
||||
fn persist_now(&self) {
|
||||
let tasks: Vec<DownloadTask> = {
|
||||
let tasks = self.inner.tasks.lock().unwrap();
|
||||
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
tasks.values().cloned().collect()
|
||||
};
|
||||
let settings = self.inner.settings.lock().unwrap().clone();
|
||||
let settings = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let state = EngineState {
|
||||
tasks,
|
||||
settings,
|
||||
next_id: 0, // storage.save 会从 id_counter 读取
|
||||
};
|
||||
self.inner.storage.save(state);
|
||||
*self.inner.last_save.lock().unwrap() = Instant::now();
|
||||
if let Err(e) = self.inner.storage.save(state) {
|
||||
crate::logger::log_error("download", &format!("状态持久化失败,进行中任务标记为 Error: {}", e));
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for task in tasks.values_mut() {
|
||||
if task.status == TaskStatus::Active {
|
||||
task.status = TaskStatus::Error;
|
||||
}
|
||||
}
|
||||
return; // 不更新 last_save,下次定时器会重试
|
||||
}
|
||||
*self.inner.last_save.lock().unwrap_or_else(|e| e.into_inner()) = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ use tokio::task::JoinSet;
|
||||
use super::rate_limit::RateLimiter;
|
||||
use super::task::{ProbeResult, Segment};
|
||||
|
||||
/// 请求超时:连接 + 响应头必须在 30s 内就绪(分段请求若无超时,
|
||||
/// 服务器挂死时任务将永久卡在 Active,pause→resume 会出现新旧任务并发写同一临时文件)
|
||||
const REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
/// 分块读取停滞超时:30s 内无任何数据视为连接挂死,主动中断(配合取消标志及时退出)
|
||||
const READ_STALL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// HTTP/HTTPS 下载器
|
||||
#[derive(Clone)]
|
||||
pub struct HttpDownloader {
|
||||
@@ -43,7 +49,10 @@ impl HttpDownloader {
|
||||
req = req.header(k, v);
|
||||
}
|
||||
|
||||
match req.send().await {
|
||||
match tokio::time::timeout(REQUEST_TIMEOUT, req.send())
|
||||
.await
|
||||
.map_err(|_| "探测超时(30s 内未收到响应头)".to_string())?
|
||||
{
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let headers_map = resp.headers().clone();
|
||||
@@ -92,9 +101,9 @@ impl HttpDownloader {
|
||||
for (k, v) in headers {
|
||||
head_req = head_req.header(k, v);
|
||||
}
|
||||
let resp = head_req
|
||||
.send()
|
||||
let resp = tokio::time::timeout(REQUEST_TIMEOUT, head_req.send())
|
||||
.await
|
||||
.map_err(|_| "探测超时(HEAD 30s 内未收到响应头)".to_string())?
|
||||
.map_err(|e| format!("探测失败(GET 和 HEAD 均失败): {}", e))?;
|
||||
let headers_map = resp.headers().clone();
|
||||
let total_size = headers_map
|
||||
@@ -295,14 +304,26 @@ async fn download_segment_with_client(
|
||||
req = req.header(k, v);
|
||||
}
|
||||
|
||||
let resp = req
|
||||
.send()
|
||||
let resp = tokio::time::timeout(REQUEST_TIMEOUT, req.send())
|
||||
.await
|
||||
.map_err(|_| "请求超时(30s 内未收到响应头)".to_string())?
|
||||
.map_err(|e| format!("请求失败: {}", e))?;
|
||||
|
||||
let status = resp.status();
|
||||
if !status.is_success() && status.as_u16() != 206 {
|
||||
return Err(format!("服务器返回 HTTP {}", status));
|
||||
if unknown_size {
|
||||
// 未知大小:未发送 Range 头,接受任意 2xx
|
||||
if !status.is_success() {
|
||||
return Err(format!("服务器返回 HTTP {}", status));
|
||||
}
|
||||
} else {
|
||||
// 已发送 Range 头:必须返回 206。若服务器忽略 Range 返回 200 全文,
|
||||
// 按 range_start 偏移写入会错位 → 静默损坏文件;此处直接中断。
|
||||
if status.as_u16() != 206 {
|
||||
return Err(format!(
|
||||
"服务器未按分段请求响应(期望 206,实际 {}),已中断以避免文件损坏",
|
||||
status
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 流式读取并写入文件
|
||||
@@ -315,8 +336,9 @@ async fn download_segment_with_client(
|
||||
return Err("已取消".to_string());
|
||||
}
|
||||
|
||||
match stream.next().await {
|
||||
Some(Ok(chunk)) => {
|
||||
// 停滞超时:30s 无数据即中断,确保 cancel 标志能及时被感知(配合代际句柄防并发写)
|
||||
match tokio::time::timeout(READ_STALL_TIMEOUT, stream.next()).await {
|
||||
Ok(Some(Ok(chunk))) => {
|
||||
buf.extend_from_slice(&chunk);
|
||||
// 接收到数据立即更新进度(避免监控周期内进度无变化导致速度显示为 0)
|
||||
local_completed += chunk.len() as u64;
|
||||
@@ -331,10 +353,10 @@ async fn download_segment_with_client(
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
Ok(Some(Err(e))) => {
|
||||
return Err(format!("读取数据失败: {}", e));
|
||||
}
|
||||
None => {
|
||||
Ok(None) => {
|
||||
// 流结束,写入剩余数据
|
||||
if !buf.is_empty() {
|
||||
file.write_all(&buf)
|
||||
@@ -346,6 +368,9 @@ async fn download_segment_with_client(
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
return Err("读取超时(30s 无数据,已中断下载)".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,10 +461,11 @@ fn percent_decode(s: &str) -> String {
|
||||
/// 将文件大小划分为 N 个分段
|
||||
pub fn split_segments(total_size: u64, num_connections: u32) -> Vec<Segment> {
|
||||
if total_size == 0 || num_connections == 0 {
|
||||
// 空文件或未指定连接数:单段覆盖整个文件(end 按 total_size 推导,不能硬编码 0)
|
||||
return vec![Segment {
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 0,
|
||||
end: total_size.saturating_sub(1),
|
||||
completed: 0,
|
||||
}];
|
||||
}
|
||||
@@ -473,3 +499,86 @@ pub fn split_segments(total_size: u64, num_connections: u32) -> Vec<Segment> {
|
||||
|
||||
segments
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod split_segments_tests {
|
||||
use super::*;
|
||||
|
||||
/// 分段必须完整覆盖 [0, total_size),且相互连续无重叠
|
||||
fn assert_contiguous(segments: &[Segment], total_size: u64) {
|
||||
assert!(!segments.is_empty());
|
||||
let mut prev_end: i64 = -1;
|
||||
for seg in segments {
|
||||
assert_eq!(seg.start as i64, prev_end + 1, "分段不连续");
|
||||
assert!(seg.end >= seg.start, "分段 start > end");
|
||||
prev_end = seg.end as i64;
|
||||
}
|
||||
assert_eq!(
|
||||
segments.last().unwrap().end,
|
||||
total_size - 1,
|
||||
"末段未覆盖文件末尾"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_size_returns_single_segment() {
|
||||
let segs = split_segments(0, 4);
|
||||
assert_eq!(segs.len(), 1);
|
||||
assert_eq!(segs[0].start, 0);
|
||||
assert_eq!(segs[0].end, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_connections_covers_whole_file() {
|
||||
let segs = split_segments(1024, 0);
|
||||
assert_eq!(segs.len(), 1);
|
||||
assert_eq!(segs[0].start, 0);
|
||||
assert_eq!(segs[0].end, 1023);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divides_evenly_with_contiguous_coverage() {
|
||||
// 10MB / 4 连接 → 4 段完整覆盖
|
||||
let total = 10 * 1024 * 1024;
|
||||
let segs = split_segments(total, 4);
|
||||
assert_eq!(segs.len(), 4);
|
||||
assert_contiguous(&segs, total);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamps_connections_by_min_segment_size() {
|
||||
// 2MB 文件请求 8 连接 → 受 1MB 最小分段限制,实际 ≤ 2 段
|
||||
let total = 2 * 1024 * 1024;
|
||||
let segs = split_segments(total, 8);
|
||||
assert!(segs.len() <= 2, "连接数未按最小分段收敛: {}", segs.len());
|
||||
assert_contiguous(&segs, total);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_requested_connection_count() {
|
||||
// 大文件按请求连接数切分
|
||||
let total = 100 * 1024 * 1024;
|
||||
let segs = split_segments(total, 3);
|
||||
assert_eq!(segs.len(), 3);
|
||||
assert_contiguous(&segs, total);
|
||||
// 每段大小均匀
|
||||
for seg in &segs {
|
||||
let seg_len = seg.end - seg.start + 1;
|
||||
assert!(
|
||||
seg_len >= total / 3,
|
||||
"分段大小不均: {} 段只有 {} 字节",
|
||||
seg.index,
|
||||
seg_len
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_file_single_segment() {
|
||||
// 小于 1MB 的文件始终单段
|
||||
let total = 100;
|
||||
let segs = split_segments(total, 4);
|
||||
assert_eq!(segs.len(), 1);
|
||||
assert_contiguous(&segs, total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ impl RateLimiter {
|
||||
// 尝试在当前窗口消费(用作用域确保 MutexGuard 在 await 前释放)
|
||||
let over_limit = {
|
||||
let now = std::time::Instant::now();
|
||||
let mut start = self.window_start.lock().unwrap();
|
||||
let mut start = self.window_start.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let elapsed = now.duration_since(*start);
|
||||
|
||||
// 窗口过期,重置
|
||||
@@ -72,3 +72,64 @@ impl Clone for RateLimiter {
|
||||
Self::new(self.limit.load(Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod rate_limiter_tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
#[tokio::test]
|
||||
async fn zero_limit_never_blocks() {
|
||||
let limiter = RateLimiter::new(0);
|
||||
let start = Instant::now();
|
||||
limiter.consume(1024 * 1024).await;
|
||||
limiter.consume(u64::MAX).await;
|
||||
assert!(start.elapsed() < Duration::from_millis(50));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn under_limit_returns_immediately() {
|
||||
let limiter = RateLimiter::new(100_000); // 100KB/s
|
||||
let start = Instant::now();
|
||||
limiter.consume(1024).await;
|
||||
assert!(start.elapsed() < Duration::from_millis(50));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exceeding_limit_waits_proportionally() {
|
||||
// 限速 200 B/s:先消耗 100 未超限,再消耗 150 → 超限 50 → 等待约 250ms
|
||||
let limiter = RateLimiter::new(200);
|
||||
limiter.consume(100).await;
|
||||
let start = Instant::now();
|
||||
limiter.consume(150).await;
|
||||
let elapsed = start.elapsed();
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(200),
|
||||
"等待时间不足: {:?}",
|
||||
elapsed
|
||||
);
|
||||
assert!(elapsed < Duration::from_millis(1100));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn window_resets_after_one_second() {
|
||||
// 限速 100 B/s:第一窗口耗尽后,1.1s 窗口重置,再消耗 100 不应阻塞
|
||||
let limiter = RateLimiter::new(100);
|
||||
limiter.consume(100).await;
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
let start = Instant::now();
|
||||
limiter.consume(100).await;
|
||||
assert!(start.elapsed() < Duration::from_millis(100));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_limit_takes_effect_dynamically() {
|
||||
let limiter = RateLimiter::new(0);
|
||||
limiter.consume(1024).await; // 不限速
|
||||
limiter.set_limit(100);
|
||||
limiter.consume(100).await;
|
||||
let start = Instant::now();
|
||||
limiter.consume(100).await; // 累计 200 > 100 → 等待 1000ms
|
||||
assert!(start.elapsed() >= Duration::from_millis(900));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,15 +57,15 @@ impl ExtensionServer {
|
||||
let listener = match tokio::net::TcpListener::bind(&addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("[download_engine] 扩展 HTTP 服务启动失败 ({}): {}", addr, e);
|
||||
crate::logger::log_error("download", &format!("扩展 HTTP 服务启动失败 ({}): {}", addr, e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!("[download_engine] 扩展 HTTP 服务已启动: http://{}", addr);
|
||||
crate::logger::log_info("download", &format!("扩展 HTTP 服务已启动: http://{}", addr));
|
||||
|
||||
if let Err(e) = axum::serve(listener, app).await {
|
||||
eprintln!("[download_engine] 扩展 HTTP 服务异常: {}", e);
|
||||
crate::logger::log_error("download", &format!("扩展 HTTP 服务异常: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl Storage {
|
||||
pub fn new(data_dir: PathBuf) -> Self {
|
||||
// 确保数据目录存在(首次启动或目录被删除时自动创建)
|
||||
if let Err(e) = fs::create_dir_all(&data_dir) {
|
||||
eprintln!("[download_engine] 创建数据目录失败: {} ({})", data_dir.display(), e);
|
||||
crate::logger::log_error("download", &format!("创建数据目录失败: {} ({})", data_dir.display(), e));
|
||||
}
|
||||
let state_path = data_dir.join("engine_state.json");
|
||||
let existing = Self::load_raw(&state_path);
|
||||
@@ -68,23 +68,24 @@ impl Storage {
|
||||
Self::load_raw(&self.state_path).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 保存状态到磁盘
|
||||
pub fn save(&self, mut state: EngineState) {
|
||||
/// 保存状态到磁盘(原子写:先写 .tmp 再 rename 覆盖,
|
||||
/// 避免进程崩溃时产生半写/截断的状态文件导致任务列表丢失)
|
||||
/// 失败返回 Err,由调用方决定是否将任务置为 Error(防止"看似已保存"的假象)。
|
||||
pub fn save(&self, mut state: EngineState) -> Result<(), String> {
|
||||
// 同步 ID 计数器
|
||||
state.next_id = self.id_counter.load(Ordering::SeqCst);
|
||||
match serde_json::to_string_pretty(&state) {
|
||||
Ok(json) => {
|
||||
// 兜底:若父目录被外部删除则在写入前重建
|
||||
if let Some(parent) = self.state_path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
if let Err(e) = fs::write(&self.state_path, json) {
|
||||
eprintln!("[download_engine] 保存状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[download_engine] 序列化状态失败: {}", e);
|
||||
}
|
||||
let json = serde_json::to_string_pretty(&state)
|
||||
.map_err(|e| format!("序列化状态失败: {}", e))?;
|
||||
// 兜底:若父目录被外部删除则在写入前重建
|
||||
if let Some(parent) = self.state_path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
let tmp_path = PathBuf::from(format!("{}.tmp", self.state_path.display()));
|
||||
fs::write(&tmp_path, json).map_err(|e| format!("保存状态失败: {}", e))?;
|
||||
fs::rename(&tmp_path, &self.state_path).map_err(|e| {
|
||||
let _ = fs::remove_file(&tmp_path);
|
||||
format!("替换状态文件失败: {}", e)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 任务状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TaskStatus {
|
||||
/// 排队等待(并发数已满)
|
||||
@@ -18,7 +19,7 @@ pub enum TaskStatus {
|
||||
}
|
||||
|
||||
/// 下载分段(多线程 Range 下载 / 断点续传用)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Segment {
|
||||
/// 分段索引
|
||||
@@ -43,7 +44,7 @@ impl Segment {
|
||||
}
|
||||
|
||||
/// 下载任务
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadTask {
|
||||
/// 任务 ID(自增 hex 字符串)
|
||||
@@ -98,7 +99,7 @@ impl DownloadTask {
|
||||
}
|
||||
|
||||
/// 下载设置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloaderSettings {
|
||||
/// 下载目录
|
||||
|
||||
Reference in New Issue
Block a user