Files
Thing/src-tauri/src/download_engine/storage.rs
T
2026-08-06 10:33:16 +08:00

92 lines
3.3 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 std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use super::task::{DownloadTask, DownloaderSettings};
/// 引擎持久化状态(序列化到 engine_state.json
#[derive(Serialize, Deserialize)]
pub struct EngineState {
/// 所有任务(active + queued + paused + complete + error
#[serde(default)]
pub tasks: Vec<DownloadTask>,
/// 下载设置
#[serde(default)]
pub settings: DownloaderSettings,
/// 自增 ID 计数器(不序列化为 hex,存原始数字)
#[serde(default)]
pub next_id: u64,
}
impl Default for EngineState {
fn default() -> Self {
Self {
tasks: Vec::new(),
settings: DownloaderSettings::default(),
next_id: 1,
}
}
}
/// 存储管理器:负责加载/保存引擎状态到 JSON 文件
pub struct Storage {
state_path: PathBuf,
/// ID 计数器(内存中维护,与 EngineState.next_id 同步)
pub id_counter: AtomicU64,
}
impl Storage {
pub fn new(data_dir: PathBuf) -> Self {
// 确保数据目录存在(首次启动或目录被删除时自动创建)
if let Err(e) = fs::create_dir_all(&data_dir) {
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);
let next_id = existing.as_ref().map(|s| s.next_id).unwrap_or(1);
Self {
state_path,
id_counter: AtomicU64::new(next_id),
}
}
/// 生成下一个任务 ID(16 位 hex 字符串)
pub fn next_task_id(&self) -> String {
let id = self.id_counter.fetch_add(1, Ordering::SeqCst);
format!("{:016x}", id)
}
/// 从磁盘加载状态
fn load_raw(path: &PathBuf) -> Option<EngineState> {
let raw = fs::read_to_string(path).ok()?;
serde_json::from_str::<EngineState>(&raw).ok()
}
/// 加载完整状态(若文件不存在返回默认值)
pub fn load(&self) -> EngineState {
Self::load_raw(&self.state_path).unwrap_or_default()
}
/// 保存状态到磁盘(原子写:先写 .tmp 再 rename 覆盖,
/// 避免进程崩溃时产生半写/截断的状态文件导致任务列表丢失)
/// 失败返回 Err,由调用方决定是否将任务置为 Error(防止"看似已保存"的假象)。
pub fn save(&self, mut state: EngineState) -> Result<(), String> {
// 同步 ID 计数器
state.next_id = self.id_counter.load(Ordering::SeqCst);
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(())
}
}