This commit is contained in:
zhongluofeng
2026-07-30 09:09:29 +08:00
parent 74902c4cec
commit f452322aad
32 changed files with 4586 additions and 115 deletions
+36 -2
View File
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use tauri::{AppHandle, State};
use tauri_plugin_opener::OpenerExt;
use super::engine::DownloadEngine;
use super::engine::{CheckUrlResult, DownloadEngine};
use super::task::{DownloadTask, DownloaderSettings};
/// 获取所有任务
@@ -12,6 +12,39 @@ pub fn downloader_get_tasks(engine: State<'_, DownloadEngine>) -> Vec<DownloadTa
engine.get_tasks()
}
/// 检查 URL 重复性并探测文件信息(添加下载前调用)
#[tauri::command]
pub async fn downloader_check_url(
engine: State<'_, DownloadEngine>,
url: String,
dir: Option<String>,
headers: Option<HashMap<String, String>>,
) -> Result<CheckUrlResult, String> {
let headers = headers.unwrap_or_default();
let (probe, duplicate, existing) = engine.check_url(&url, dir.as_deref(), &headers).await;
let result = match probe {
Ok(p) => CheckUrlResult {
ok: true,
error: None,
filename: p.filename.clone(),
total_size: p.total_size,
supports_resume: p.supports_resume,
duplicate,
existing,
},
Err(e) => CheckUrlResult {
ok: false,
error: Some(e),
filename: None,
total_size: None,
supports_resume: false,
duplicate,
existing,
},
};
Ok(result)
}
/// 添加下载任务
#[tauri::command]
pub async fn downloader_add_task(
@@ -20,8 +53,9 @@ pub async fn downloader_add_task(
filename: Option<String>,
dir: Option<String>,
headers: Option<HashMap<String, String>>,
auto_rename: Option<bool>,
) -> Result<String, String> {
engine.add_task(url, filename, dir, headers.unwrap_or_default()).await
engine.add_task(url, filename, dir, headers.unwrap_or_default(), auto_rename.unwrap_or(false)).await
}
/// 暂停任务
+146 -2
View File
@@ -9,7 +9,50 @@ use tauri::{AppHandle, Emitter};
use super::http_dl::{HttpDownloader, split_segments};
use super::rate_limit::RateLimiter;
use super::storage::{EngineState, Storage};
use super::task::{DownloadTask, DownloaderSettings, Segment, TaskStatus};
use super::task::{DownloadTask, DownloaderSettings, ProbeResult, Segment, TaskStatus};
/// 重复类型
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum DuplicateKind {
/// 无重复
None,
/// URL 重复(已有相同链接的任务)
Url,
/// 文件名重复(已有同名任务下载到同一目录)
Filename,
/// 磁盘文件已存在
FileExists,
}
/// 已存在的任务信息(用于前端展示)
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExistingTaskInfo {
pub id: String,
pub filename: String,
pub status: TaskStatus,
}
/// check_url 命令返回的结果
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckUrlResult {
/// 探测是否成功
pub ok: bool,
/// 错误信息(探测失败时)
pub error: Option<String>,
/// 文件名(探测成功时)
pub filename: Option<String>,
/// 文件大小(字节)
pub total_size: Option<u64>,
/// 是否支持断点续传
pub supports_resume: bool,
/// 重复类型
pub duplicate: DuplicateKind,
/// 已存在的任务信息
pub existing: Option<ExistingTaskInfo>,
}
/// 进度事件载荷(发给前端 download-progress 事件)
#[derive(Debug, Clone, serde::Serialize)]
@@ -130,13 +173,109 @@ impl DownloadEngine {
// ===================== 公开 API =====================
/// 检查 URL 重复性并探测文件信息。
/// 返回 (探测结果, 重复类型, 已存在任务信息)。
pub async fn check_url(
&self,
url: &str,
dir: Option<&str>,
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 task_dir = dir.map(|d| d.to_string()).unwrap_or_else(|| settings.download_dir.clone());
let filename = probe.as_ref().ok()
.and_then(|p| p.filename.clone())
.unwrap_or_else(|| {
url.split('?').next()
.and_then(|u| u.rsplit('/').next())
.filter(|n| !n.is_empty())
.map(|n| n.to_string())
.unwrap_or_else(|| format!("download_{}", chrono::Utc::now().timestamp()))
});
let mut duplicate = DuplicateKind::None;
let mut existing: Option<ExistingTaskInfo> = None;
{
let tasks = self.inner.tasks.lock().unwrap();
for t in tasks.values() {
// URL 完全相同
if t.url == url {
duplicate = DuplicateKind::Url;
existing = Some(ExistingTaskInfo {
id: t.id.clone(),
filename: t.filename.clone(),
status: t.status.clone(),
});
break;
}
// 目标文件名 + 目录相同(可能 URL 不同但下载到同一文件)
if t.filename == filename && t.dir == task_dir {
duplicate = DuplicateKind::Filename;
existing = Some(ExistingTaskInfo {
id: t.id.clone(),
filename: t.filename.clone(),
status: t.status.clone(),
});
break;
}
}
}
// 检查磁盘文件是否已存在(仅探测成功时)
if duplicate == DuplicateKind::None {
if let Ok(ref _p) = probe {
let filepath = PathBuf::from(&task_dir).join(&filename);
if filepath.exists() {
duplicate = DuplicateKind::FileExists;
existing = Some(ExistingTaskInfo {
id: String::new(),
filename,
status: TaskStatus::Complete,
});
}
}
}
(probe, duplicate, existing)
}
/// 生成不冲突的文件名(同名时追加 (1)、(2)...)
fn generate_unique_filename(&self, dir: &str, filename: &str) -> String {
let path = PathBuf::from(dir).join(filename);
if !path.exists() {
return filename.to_string();
}
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 {
let new_name = match ext {
Some(e) => format!("{} ({}).{}", stem, i, e),
None => format!("{} ({})", stem, i),
};
let new_path = PathBuf::from(dir).join(&new_name);
if !new_path.exists() {
return new_name;
}
}
// 极端情况:追加时间戳
match ext {
Some(e) => format!("{} ({}).{}", stem, chrono::Utc::now().timestamp_millis(), e),
None => format!("{} ({})", stem, chrono::Utc::now().timestamp_millis()),
}
}
/// 添加下载任务
/// auto_rename: 同名时自动重命名(追加 (1)、(2)...),否则覆盖
pub async fn add_task(
&self,
url: String,
filename: Option<String>,
dir: Option<String>,
headers: HashMap<String, String>,
auto_rename: bool,
) -> Result<String, String> {
// 探测资源信息
let probe = self.inner.http.probe(&url, &headers).await;
@@ -145,7 +284,7 @@ impl DownloadEngine {
let task_dir = dir.unwrap_or_else(|| settings.download_dir.clone());
// 确定文件名
let task_filename = filename
let mut task_filename = filename
.or_else(|| probe.as_ref().ok().and_then(|p| p.filename.clone()))
.unwrap_or_else(|| {
url.split('?')
@@ -156,6 +295,11 @@ impl DownloadEngine {
.unwrap_or_else(|| format!("download_{}", chrono::Utc::now().timestamp()))
});
// 自动重命名:若磁盘已存在同名文件,追加 (1)、(2)...
if auto_rename {
task_filename = self.generate_unique_filename(&task_dir, &task_filename);
}
let id = self.inner.storage.next_task_id();
// 创建分段(受 continue_download 设置控制:关闭时强制单线程、不支持续传)
+1 -1
View File
@@ -7,7 +7,7 @@ pub mod storage;
pub mod task;
pub use commands::{
downloader_add_task, downloader_get_extension_info, downloader_get_settings, downloader_get_tasks,
downloader_add_task, downloader_check_url, downloader_get_extension_info, downloader_get_settings, downloader_get_tasks,
downloader_open_dir, downloader_open_url, downloader_pause_task, downloader_remove_task,
downloader_resume_task, downloader_save_settings, downloader_status,
};
+1 -1
View File
@@ -108,7 +108,7 @@ async fn create_download(
return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() })));
}
match state.engine.add_task(req.url, req.filename, req.dir, req.headers).await {
match state.engine.add_task(req.url, req.filename, req.dir, req.headers, true).await {
Ok(id) => Ok(Json(CreateDownloadResponse { id })),
Err(e) => Err((StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }))),
}
+5
View File
@@ -125,6 +125,9 @@ pub struct DownloaderSettings {
/// 删除任务时是否同时删除已下载的文件
#[serde(default)]
pub delete_files_on_remove: bool,
/// 添加下载前检查重复(URL 或文件名重复时询问)
#[serde(default = "default_true")]
pub check_duplicate: bool,
}
fn default_max_concurrent() -> u32 {
@@ -162,11 +165,13 @@ impl Default for DownloaderSettings {
extension_port: default_extension_port(),
extension_secret: String::new(),
delete_files_on_remove: false,
check_duplicate: true,
}
}
}
/// HEAD/Range 探测结果
#[derive(Debug, Clone)]
pub struct ProbeResult {
/// 文件大小(字节),None=未知
pub total_size: Option<u64>,