性能优化

This commit is contained in:
zhongluofeng
2026-08-06 10:33:16 +08:00
parent c7578a2e6b
commit e66c53e66d
105 changed files with 7273 additions and 5002 deletions
+10
View File
@@ -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>)
+152 -88
View File
@@ -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();
}
}
+121 -12
View File
@@ -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);
}
}
+62 -1
View File
@@ -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));
}
}
+3 -3
View File
@@ -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));
}
}
}
+17 -16
View File
@@ -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(())
}
}
+5 -4
View File
@@ -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 {
/// 下载目录