Files
Thing/src-tauri/src/download_engine/task.rs
T

250 lines
7.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::{Deserialize, Serialize};
use specta::Type;
use std::collections::HashMap;
/// 任务下载协议类型
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type, Default)]
#[serde(rename_all = "lowercase")]
pub enum TaskProtocol {
/// HTTP/HTTPS 直链
#[default]
Http,
/// BitTorrent(磁力链 / .torrent 文件)
BitTorrent,
}
/// 任务状态
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
#[serde(rename_all = "lowercase")]
pub enum TaskStatus {
/// 排队等待(并发数已满)
Queued,
/// 下载中
Active,
/// 已暂停
Paused,
/// 已完成
Complete,
/// 错误
Error,
/// 已取消(用户取消:进度与文件已清除,仅保留记录,只能再次下载)
Cancelled,
}
/// 下载分段(多线程 Range 下载 / 断点续传用)
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct Segment {
/// 分段索引
pub index: u32,
/// 起始字节(含)
pub start: u64,
/// 结束字节(含)
pub end: u64,
/// 已下载字节
pub completed: u64,
}
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()
}
}
/// BT 种子内文件条目(多文件任务用;阶段1下载全部文件,但保留列表供 UI 展示)
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct BtFileInfo {
/// 文件在种子内的索引
pub index: u32,
/// 相对种子根目录的路径(如 "sub/file.mkv"
pub path: String,
/// 文件大小(字节)
pub size: u64,
}
/// 下载任务
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct DownloadTask {
/// 任务 ID(自增 hex 字符串)
pub id: String,
/// 下载地址(HTTP URL 或磁力链接)
pub url: String,
/// 文件名(HTTP:目标文件名;BT:种子名称)
pub filename: String,
/// 保存目录(绝对路径)
pub dir: String,
/// 协议类型
#[serde(default)]
pub protocol: TaskProtocol,
/// BT 种子 infohash(协议=BitTorrent 时存在)
#[serde(default)]
pub info_hash: Option<String>,
/// BT 种子内文件列表(协议=BitTorrent 时存在)
#[serde(default)]
pub bt_files: Vec<BtFileInfo>,
/// BT 元数据是否已解析就绪(异步添加时:后台解析完成前为 false,调度器跳过)
#[serde(default)]
pub bt_metadata_ready: bool,
/// 状态
pub status: TaskStatus,
/// 文件总大小(字节),0=未知
pub total_size: u64,
/// 已下载字节
pub completed_size: u64,
/// 当前下载速度 bytes/s
pub speed: u64,
/// 服务器是否支持断点续传
pub supports_resume: bool,
/// 分段信息
#[serde(default)]
pub segments: Vec<Segment>,
/// 错误信息
#[serde(default)]
pub error: Option<String>,
/// 创建时间(Unix 时间戳,毫秒)
pub created_at: i64,
/// 自定义请求头(Cookie / Referer 等)
#[serde(default)]
pub headers: HashMap<String, String>,
}
impl DownloadTask {
/// 文件完整路径(最终文件名)
pub fn file_path(&self) -> std::path::PathBuf {
std::path::PathBuf::from(&self.dir).join(&self.filename)
}
/// 下载临时文件路径(下载未完成时使用,完成后重命名为 file_path)
pub fn temp_file_path(&self) -> std::path::PathBuf {
std::path::PathBuf::from(&self.dir).join(format!("{}.thingdl", self.filename))
}
/// 更新已下载总量(聚合所有分段)
pub fn recalc_completed(&mut self) {
if self.segments.is_empty() {
return;
}
self.completed_size = self.segments.iter().map(|s| s.completed).sum();
}
}
/// 下载设置
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct DownloaderSettings {
/// 下载目录
#[serde(default = "default_download_dir")]
pub download_dir: String,
/// 最大同时下载数
#[serde(default = "default_max_concurrent")]
pub max_concurrent: u32,
/// 单任务最大连接数(多线程分段数)
#[serde(default = "default_max_connections")]
pub max_connections: u32,
/// 断点续传
#[serde(default = "default_true")]
pub continue_download: bool,
/// 全局速度限制 KB/s(0=不限)
#[serde(default)]
pub global_speed_limit: u64,
/// 扩展 HTTP API 端口
#[serde(default = "default_extension_port")]
pub extension_port: u16,
/// 扩展认证密钥(空=不认证)
#[serde(default)]
pub extension_secret: String,
/// 删除任务时是否同时删除已下载的文件
#[serde(default)]
pub delete_files_on_remove: bool,
/// 添加下载前检查重复(URL 或文件名重复时询问)
#[serde(default = "default_true")]
pub check_duplicate: bool,
/// 下载是否使用代理:true=尊重系统代理(mihomo 开启系统代理时经其转发),false=强制直连
#[serde(default = "default_true")]
pub use_proxy: bool,
/// BitTorrent 上传限速 KB/s0=不限)
#[serde(default)]
pub bt_upload_limit_kb: u64,
/// BitTorrent 下载完成后是否继续做种上传(false=下载完即停止上传)
#[serde(default)]
pub bt_seed_after_download: bool,
/// BitTorrent 监听端口(0=自动选择)
#[serde(default)]
pub bt_listen_port: u16,
/// BitTorrent 使用代理下载:开启后自动使用代理模块(mihomo)的 SOCKS5 端口;代理不可用时降级直连
#[serde(default)]
pub bt_use_proxy: bool,
}
fn default_max_concurrent() -> u32 {
5
}
fn default_max_connections() -> u32 {
8
}
fn default_true() -> bool {
true
}
fn default_extension_port() -> u16 {
16800
}
fn default_download_dir() -> String {
dirs::download_dir()
.or_else(|| {
std::env::var("USERPROFILE")
.ok()
.map(|p| std::path::PathBuf::from(p).join("Downloads"))
})
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "downloads".to_string())
}
impl Default for DownloaderSettings {
fn default() -> Self {
Self {
download_dir: default_download_dir(),
max_concurrent: default_max_concurrent(),
max_connections: default_max_connections(),
continue_download: true,
global_speed_limit: 0,
extension_port: default_extension_port(),
extension_secret: String::new(),
delete_files_on_remove: false,
check_duplicate: true,
use_proxy: true,
bt_upload_limit_kb: 0,
bt_seed_after_download: false,
bt_listen_port: 0,
bt_use_proxy: false,
}
}
}
/// HEAD/Range 探测结果
#[derive(Debug, Clone)]
pub struct ProbeResult {
/// 文件大小(字节),None=未知
pub total_size: Option<u64>,
/// 是否支持 Range 请求
pub supports_resume: bool,
/// 从 Content-Disposition 或 URL 推断的文件名
pub filename: Option<String>,
}