细节调整及优化(26.8.3)

This commit is contained in:
zhongluofeng
2026-08-21 17:25:47 +08:00
parent 0a19b4b38a
commit 4dd60f42a1
72 changed files with 4779 additions and 1395 deletions
+49 -9
View File
@@ -64,6 +64,8 @@ pub struct ProgressPayload {
pub total_size: u64,
pub speed: u64,
pub status: TaskStatus,
/// 每个分段的已下载字节(与任务 segments 一一对应,供前端详情弹窗实时展示)
pub segments: Vec<u64>,
}
/// 完成事件载荷(发给前端 download-complete 事件)
@@ -107,6 +109,9 @@ struct EngineInner {
global_limiter: Arc<RateLimiter>,
/// HTTP 下载器
http: HttpDownloader,
/// 下载是否使用代理(true=尊重系统代理,false=强制直连)
/// 独立于 settings 存储,避免下载过程中反复锁 settingssave_settings 时同步更新
use_proxy: AtomicBool,
/// Tauri 应用句柄(用于发事件)
app_handle: AppHandle,
/// 引擎是否已启动
@@ -144,6 +149,8 @@ impl DownloadEngine {
0
};
let global_limiter = Arc::new(RateLimiter::new(global_limit));
// 在 settings 移入 Mutex 前读取代理开关
let use_proxy = settings.use_proxy;
let engine = Self {
inner: Arc::new(EngineInner {
@@ -153,6 +160,7 @@ impl DownloadEngine {
settings: Mutex::new(settings),
global_limiter,
http: HttpDownloader::new(),
use_proxy: AtomicBool::new(use_proxy),
app_handle,
started: AtomicBool::new(false),
next_gen: AtomicU64::new(0),
@@ -188,7 +196,8 @@ impl DownloadEngine {
dir: Option<&str>,
headers: &HashMap<String, String>,
) -> (Result<ProbeResult, String>, DuplicateKind, Option<ExistingTaskInfo>) {
let probe = self.inner.http.probe(url, headers).await;
let use_proxy = self.inner.use_proxy.load(Ordering::SeqCst);
let probe = self.inner.http.probe(url, headers, use_proxy).await;
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());
@@ -250,11 +259,30 @@ impl DownloadEngine {
}
/// 生成不冲突的文件名(同名时追加 (1)、(2)...)
/// 冲突来源:磁盘已有同名最终文件、已存在同名临时文件(其他任务正在下载该名)、
/// 以及任务列表中已有任务占用同名(即使最终文件尚未落盘,避免共用同一 .thingdl 临时文件)
fn generate_unique_filename(&self, dir: &str, filename: &str) -> String {
let path = PathBuf::from(dir).join(filename);
if !path.exists() {
// 任务列表中已占用的文件名(同一目录下)
let taken: std::collections::HashSet<String> = {
let tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
tasks
.values()
.filter(|t| t.dir == dir)
.map(|t| t.filename.clone())
.collect()
};
// 磁盘最终文件 / 临时文件 / 任务占用,三者任一冲突即视为被占用
let used = |name: &str| {
taken.contains(name)
|| PathBuf::from(dir).join(name).exists()
|| PathBuf::from(dir).join(format!("{}.thingdl", name)).exists()
};
if !used(filename) {
return filename.to_string();
}
let path = PathBuf::from(dir).join(filename);
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("download");
let ext = path.extension().and_then(|s| s.to_str());
for i in 1..1000 {
@@ -262,8 +290,7 @@ impl DownloadEngine {
Some(e) => format!("{} ({}).{}", stem, i, e),
None => format!("{} ({})", stem, i),
};
let new_path = PathBuf::from(dir).join(&new_name);
if !new_path.exists() {
if !used(&new_name) {
return new_name;
}
}
@@ -285,7 +312,8 @@ impl DownloadEngine {
auto_rename: bool,
) -> Result<String, String> {
// 探测资源信息
let probe = self.inner.http.probe(&url, &headers).await;
let use_proxy = self.inner.use_proxy.load(Ordering::SeqCst);
let probe = self.inner.http.probe(&url, &headers, use_proxy).await;
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());
@@ -419,8 +447,8 @@ impl DownloadEngine {
if task.status != TaskStatus::Paused && task.status != TaskStatus::Error {
return Err("任务不在可恢复状态".to_string());
}
// 如果不支持断点续传,从头开始
if !task.supports_resume {
// 如果不支持断点续传,或文件大小未知(无法定位续传起点,需从头下载),从头开始
if !task.supports_resume || task.total_size == 0 {
task.completed_size = 0;
for seg in &mut task.segments {
seg.completed = 0;
@@ -491,6 +519,11 @@ impl DownloadEngine {
};
self.inner.global_limiter.set_limit(new_limit);
// 同步代理开关(新任务/探测立即生效,正在下载的任务不受影响)
self.inner
.use_proxy
.store(settings.use_proxy, Ordering::SeqCst);
{
let mut s = self.inner.settings.lock().unwrap_or_else(|e| e.into_inner());
*s = settings;
@@ -613,11 +646,12 @@ impl DownloadEngine {
let temp_file_path = task.temp_file_path();
let final_file_path = task.file_path();
let http = self.inner.http.clone();
let use_proxy = self.inner.use_proxy.load(Ordering::SeqCst);
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)
.download(&url, &headers, &segments, &temp_file_path, cancel_clone, &progress_clone, limiter, use_proxy)
.await;
// 代际守卫:仅最新代际的任务能更新状态 / 移除句柄 / 发完成事件。
@@ -748,6 +782,11 @@ impl DownloadEngine {
.iter()
.map(|p| p.load(Ordering::Relaxed))
.sum();
// 各分段实时进度(供前端详情弹窗分段条展示)
let segment_completed: Vec<u64> = progress_monitor
.iter()
.map(|p| p.load(Ordering::Relaxed))
.collect();
// 计算速度
let now = Instant::now();
@@ -788,6 +827,7 @@ impl DownloadEngine {
total_size,
speed,
status: live_status,
segments: segment_completed,
},
);
}
+43 -9
View File
@@ -21,15 +21,35 @@ const READ_STALL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3
/// HTTP/HTTPS 下载器
#[derive(Clone)]
pub struct HttpDownloader {
client: Client,
/// 默认客户端:尊重系统代理(reqwest 默认行为,mihomo 开启系统代理时经其转发)
system_client: Client,
/// 直连客户端:强制禁用系统代理(no_proxy)
direct_client: Client,
}
impl HttpDownloader {
pub fn new() -> Self {
let client = Client::builder()
let system_client = Client::builder()
.build()
.unwrap_or_else(|_| Client::new());
Self { client }
let direct_client = Client::builder()
// 强制直连:即使系统代理已开启,下载也不经过代理
.no_proxy()
.build()
.unwrap_or_else(|_| Client::new());
Self {
system_client,
direct_client,
}
}
/// 根据 use_proxy 选择客户端
fn client(&self, use_proxy: bool) -> &Client {
if use_proxy {
&self.system_client
} else {
&self.direct_client
}
}
/// 探测下载资源信息(大小、是否支持 Range、文件名)
@@ -38,10 +58,11 @@ impl HttpDownloader {
&self,
url: &str,
headers: &HashMap<String, String>,
use_proxy: bool,
) -> Result<ProbeResult, String> {
let client = self.client(use_proxy);
// 先尝试 Range 请求(能同时判断 Accept-Ranges 和获取大小)
let mut req = self
.client
let mut req = client
.get(url)
.header("Range", "bytes=0-0")
.header("User-Agent", "Thing-Download-Engine/1.0");
@@ -97,7 +118,7 @@ impl HttpDownloader {
}
Err(_) => {
// GET 失败,尝试 HEAD 作为回退
let mut head_req = self.client.head(url);
let mut head_req = client.head(url);
for (k, v) in headers {
head_req = head_req.header(k, v);
}
@@ -132,6 +153,7 @@ impl HttpDownloader {
/// - `cancel`: 取消标志
/// - `progress`: 每个分段的已下载字节(AtomicU64,与 segments 一一对应)
/// - `limiter`: 全局限速器
/// - `use_proxy`: 是否使用系统代理(false=强制直连)
pub async fn download(
&self,
url: &str,
@@ -141,7 +163,9 @@ impl HttpDownloader {
cancel: Arc<AtomicBool>,
progress: &[Arc<AtomicU64>],
limiter: Arc<RateLimiter>,
use_proxy: bool,
) -> Result<(), String> {
let client = self.client(use_proxy);
let total_size = segments.iter().map(|s| s.len()).sum();
// 预分配文件(若已知大小)
@@ -167,7 +191,7 @@ impl HttpDownloader {
// 单线程下载(不支持 Range 或文件太小)
let seg = &segments[0];
let prog = &progress[0];
self.download_segment(url, headers, seg, file_path, cancel.clone(), prog.clone(), limiter)
self.download_segment(url, headers, seg, file_path, cancel.clone(), prog.clone(), limiter, client)
.await?;
return Ok(());
}
@@ -187,7 +211,7 @@ impl HttpDownloader {
.unwrap_or_default();
let limiter = limiter.clone();
let file_path = file_path.to_path_buf();
let client = self.client.clone();
let client = client.clone();
join_set.spawn(async move {
download_segment_with_client(
@@ -241,9 +265,10 @@ impl HttpDownloader {
cancel: Arc<AtomicBool>,
progress: Arc<AtomicU64>,
limiter: Arc<RateLimiter>,
client: &Client,
) -> Result<(), String> {
download_segment_with_client(
&self.client,
client,
url,
headers,
seg,
@@ -366,6 +391,15 @@ async fn download_segment_with_client(
limiter.consume(buf.len() as u64).await;
buf.clear();
}
// 校验:已知大小的分段若流提前结束(收到的字节数不足分段长度),
// 说明服务器提前断开或返回不完整内容,不能标记为完成,否则文件会被截断
if !unknown_size && local_completed < seg.len() {
return Err(format!(
"文件不完整:已接收 {} / {} 字节,服务器提前结束连接",
local_completed,
seg.len()
));
}
break;
}
Err(_) => {
+15 -3
View File
@@ -9,10 +9,10 @@ use axum::{
Router,
};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager};
use tauri::{AppHandle, Emitter};
use super::engine::DownloadEngine;
use super::task::DownloadTask;
use super::task::{DownloadTask, TaskStatus};
/// 扩展 HTTP API 服务器
pub struct ExtensionServer;
@@ -114,6 +114,14 @@ async fn create_download(
return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() })));
}
// 去重:同 URL 已有非终态任务(活跃/排队/暂停)时直接返回既有任务,
// 避免浏览器重复转发同一下载造成重复下载
if let Some(existing) = state.engine.get_tasks().into_iter().find(|t| {
matches!(t.status, TaskStatus::Active | TaskStatus::Queued | TaskStatus::Paused) && t.url == req.url
}) {
return Ok(Json(CreateDownloadResponse { id: existing.id }));
}
match state.engine.add_task(req.url, req.filename, req.dir, req.headers, true).await {
Ok(id) => {
// 浏览器扩展发起下载:置前主窗口并通知前端跳到下载画面(替代原桌面通知)
@@ -146,7 +154,11 @@ async fn remove_download(
return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() })));
}
match state.engine.remove_task(&id, false) {
Ok(()) => Ok(StatusCode::NO_CONTENT),
Ok(()) => {
// 通知前端刷新任务列表(扩展删除时前端无从感知,否则列表残留已删除任务)
let _ = state.app_handle.emit(crate::constants::events::DOWNLOAD_REMOVED, serde_json::json!({ "id": id }));
Ok(StatusCode::NO_CONTENT)
}
Err(e) => Err((StatusCode::NOT_FOUND, Json(ErrorResponse { error: e }))),
}
}
+13
View File
@@ -37,8 +37,17 @@ impl Segment {
pub fn len(&self) -> u64 {
self.end.saturating_sub(self.start) + 1
}
/// 是否为未知大小哨兵段(start=0, end=0,表示不支持 Range 或未探测到大小)
pub fn is_unknown_size(&self) -> bool {
self.start == 0 && self.end == 0
}
/// 是否已完成
pub fn is_done(&self) -> bool {
// 未知大小段无法用长度判断是否完成,由流结束(Ok(None))判定;
// 若按 len()=1 判断,暂停/恢复后 completed>=1 会误判为已完成,导致文件被截断
if self.is_unknown_size() {
return false;
}
self.completed >= self.len()
}
}
@@ -129,6 +138,9 @@ pub struct DownloaderSettings {
/// 添加下载前检查重复(URL 或文件名重复时询问)
#[serde(default = "default_true")]
pub check_duplicate: bool,
/// 下载是否使用代理:true=尊重系统代理(mihomo 开启系统代理时经其转发),false=强制直连
#[serde(default = "default_true")]
pub use_proxy: bool,
}
fn default_max_concurrent() -> u32 {
@@ -167,6 +179,7 @@ impl Default for DownloaderSettings {
extension_secret: String::new(),
delete_files_on_remove: false,
check_duplicate: true,
use_proxy: true,
}
}
}