托盘
This commit is contained in:
@@ -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 设置控制:关闭时强制单线程、不支持续传)
|
||||
|
||||
Reference in New Issue
Block a user