下载非内核
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
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};
|
||||
|
||||
/// 进度事件载荷(发给前端 download-progress 事件)
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProgressPayload {
|
||||
pub id: String,
|
||||
pub completed_size: u64,
|
||||
pub total_size: u64,
|
||||
pub speed: u64,
|
||||
pub status: TaskStatus,
|
||||
}
|
||||
|
||||
/// 完成事件载荷(发给前端 download-complete 事件)
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompletePayload {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub status: TaskStatus,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 活跃下载句柄
|
||||
struct TaskHandle {
|
||||
cancel: Arc<AtomicBool>,
|
||||
/// 每个分段的已下载字节(与 segments 一一对应)
|
||||
progress: Vec<Arc<std::sync::atomic::AtomicU64>>,
|
||||
/// 下载任务的 JoinHandle(None = 已完成/已取消)
|
||||
join: Mutex<Option<tauri::async_runtime::JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
/// 下载引擎(进程内运行,通过 Arc 共享状态)
|
||||
#[derive(Clone)]
|
||||
pub struct DownloadEngine {
|
||||
inner: Arc<EngineInner>,
|
||||
}
|
||||
|
||||
struct EngineInner {
|
||||
/// 所有任务状态
|
||||
tasks: Mutex<HashMap<String, DownloadTask>>,
|
||||
/// 活跃下载句柄
|
||||
handles: Mutex<HashMap<String, TaskHandle>>,
|
||||
/// 持久化存储
|
||||
storage: Storage,
|
||||
/// 下载设置
|
||||
settings: Mutex<DownloaderSettings>,
|
||||
/// 全局限速器(bytes/s,0=不限)
|
||||
global_limiter: Arc<RateLimiter>,
|
||||
/// HTTP 下载器
|
||||
http: HttpDownloader,
|
||||
/// Tauri 应用句柄(用于发事件)
|
||||
app_handle: AppHandle,
|
||||
/// 引擎是否已启动
|
||||
started: AtomicBool,
|
||||
/// 持久化节流:上次保存时间
|
||||
last_save: Mutex<Instant>,
|
||||
}
|
||||
|
||||
impl DownloadEngine {
|
||||
/// 创建引擎并从磁盘恢复状态
|
||||
pub fn new(data_dir: PathBuf, app_handle: AppHandle) -> Self {
|
||||
let storage = Storage::new(data_dir);
|
||||
let state = storage.load();
|
||||
|
||||
// 恢复时,将 Active 任务标记为 Paused(避免自动恢复下载)
|
||||
let mut tasks: HashMap<String, DownloadTask> = state
|
||||
.tasks
|
||||
.into_iter()
|
||||
.map(|t| (t.id.clone(), t))
|
||||
.collect();
|
||||
for task in tasks.values_mut() {
|
||||
if task.status == TaskStatus::Active {
|
||||
task.status = TaskStatus::Paused;
|
||||
task.speed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let settings = state.settings;
|
||||
// 创建全局限速器(KB/s → bytes/s)
|
||||
let global_limit = if settings.global_speed_limit > 0 {
|
||||
settings.global_speed_limit * 1024
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let global_limiter = Arc::new(RateLimiter::new(global_limit));
|
||||
|
||||
let engine = Self {
|
||||
inner: Arc::new(EngineInner {
|
||||
tasks: Mutex::new(tasks),
|
||||
handles: Mutex::new(HashMap::new()),
|
||||
storage,
|
||||
settings: Mutex::new(settings),
|
||||
global_limiter,
|
||||
http: HttpDownloader::new(),
|
||||
app_handle,
|
||||
started: AtomicBool::new(false),
|
||||
last_save: Mutex::new(Instant::now()),
|
||||
}),
|
||||
};
|
||||
|
||||
// 启动后台持久化定时器
|
||||
engine.start_save_timer();
|
||||
engine.inner.started.store(true, Ordering::SeqCst);
|
||||
engine
|
||||
}
|
||||
|
||||
/// 启动后台定时持久化(每 5 秒检查一次)
|
||||
fn start_save_timer(&self) {
|
||||
let engine = self.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
engine.persist_throttled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 公开 API =====================
|
||||
|
||||
/// 添加下载任务
|
||||
pub async fn add_task(
|
||||
&self,
|
||||
url: String,
|
||||
filename: Option<String>,
|
||||
dir: Option<String>,
|
||||
headers: HashMap<String, String>,
|
||||
) -> Result<String, String> {
|
||||
// 探测资源信息
|
||||
let probe = self.inner.http.probe(&url, &headers).await;
|
||||
|
||||
let settings = self.inner.settings.lock().unwrap().clone();
|
||||
let task_dir = dir.unwrap_or_else(|| settings.download_dir.clone());
|
||||
|
||||
// 确定文件名
|
||||
let task_filename = filename
|
||||
.or_else(|| 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 id = self.inner.storage.next_task_id();
|
||||
|
||||
// 创建分段
|
||||
let segments = match &probe {
|
||||
Ok(p) if p.supports_resume && p.total_size.map(|s| s > 0).unwrap_or(false) => {
|
||||
split_segments(p.total_size.unwrap(), settings.max_connections)
|
||||
}
|
||||
_ => vec![Segment {
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 0,
|
||||
completed: 0,
|
||||
}],
|
||||
};
|
||||
|
||||
let total_size = probe.as_ref().ok().and_then(|p| p.total_size).unwrap_or(0);
|
||||
let supports_resume = probe.as_ref().ok().map(|p| p.supports_resume).unwrap_or(false);
|
||||
|
||||
let task = DownloadTask {
|
||||
id: id.clone(),
|
||||
url: url.clone(),
|
||||
filename: task_filename,
|
||||
dir: task_dir,
|
||||
status: TaskStatus::Queued,
|
||||
total_size,
|
||||
completed_size: 0,
|
||||
speed: 0,
|
||||
supports_resume,
|
||||
segments: segments.clone(),
|
||||
error: None,
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
headers,
|
||||
};
|
||||
|
||||
// 如果探测失败,标记为 Error
|
||||
let task = match probe {
|
||||
Ok(_) => task,
|
||||
Err(e) => DownloadTask {
|
||||
status: TaskStatus::Error,
|
||||
error: Some(e),
|
||||
..task
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
tasks.insert(id.clone(), task);
|
||||
}
|
||||
self.persist_now();
|
||||
|
||||
// 如果探测成功,尝试调度
|
||||
let should_schedule = {
|
||||
let tasks = self.inner.tasks.lock().unwrap();
|
||||
tasks.get(&id).map(|t| t.status == TaskStatus::Queued).unwrap_or(false)
|
||||
};
|
||||
if should_schedule {
|
||||
self.schedule();
|
||||
}
|
||||
|
||||
// 通知前端有新任务加入(扩展通过 HTTP API 添加时,前端需要刷新)
|
||||
let _ = self.inner.app_handle.emit("download-added", serde_json::json!({ "id": id }));
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// 暂停任务
|
||||
pub fn pause_task(&self, id: &str) -> Result<(), String> {
|
||||
// 1. 设置取消标志 + 读取进度(锁 handles)
|
||||
let progress_values: Vec<u64> = {
|
||||
let handles = self.inner.handles.lock().unwrap();
|
||||
if let Some(handle) = handles.get(id) {
|
||||
handle.cancel.store(true, Ordering::SeqCst);
|
||||
handle.progress.iter().map(|p| p.load(Ordering::Relaxed)).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 更新任务状态 + 同步进度(锁 tasks,不嵌套锁 handles)
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
if let Some(task) = tasks.get_mut(id) {
|
||||
if task.status == TaskStatus::Active || task.status == TaskStatus::Queued {
|
||||
task.status = TaskStatus::Paused;
|
||||
task.speed = 0;
|
||||
// 同步分段进度
|
||||
for (i, val) in progress_values.iter().enumerate() {
|
||||
if let Some(seg) = task.segments.get_mut(i) {
|
||||
seg.completed = *val;
|
||||
}
|
||||
}
|
||||
task.recalc_completed();
|
||||
}
|
||||
} else {
|
||||
return Err("任务不存在".to_string());
|
||||
}
|
||||
}
|
||||
self.persist_now();
|
||||
self.schedule();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 恢复任务
|
||||
pub fn resume_task(&self, id: &str) -> Result<(), String> {
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
if let Some(task) = tasks.get_mut(id) {
|
||||
if task.status != TaskStatus::Paused && task.status != TaskStatus::Error {
|
||||
return Err("任务不在可恢复状态".to_string());
|
||||
}
|
||||
// 如果不支持断点续传,从头开始
|
||||
if !task.supports_resume {
|
||||
task.completed_size = 0;
|
||||
for seg in &mut task.segments {
|
||||
seg.completed = 0;
|
||||
}
|
||||
}
|
||||
task.status = TaskStatus::Queued;
|
||||
task.error = None;
|
||||
} else {
|
||||
return Err("任务不存在".to_string());
|
||||
}
|
||||
}
|
||||
self.persist_now();
|
||||
self.schedule();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 移除任务
|
||||
pub fn remove_task(&self, id: &str, delete_files: bool) -> Result<(), String> {
|
||||
// 1. 设置取消标志 + abort join handle(锁 handles)
|
||||
{
|
||||
let mut handles = self.inner.handles.lock().unwrap();
|
||||
if let Some(handle) = handles.remove(id) {
|
||||
handle.cancel.store(true, Ordering::SeqCst);
|
||||
if let Ok(mut join) = handle.join.lock() {
|
||||
if let Some(j) = join.take() {
|
||||
j.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从任务列表移除(锁 tasks)
|
||||
let task = {
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
tasks.remove(id)
|
||||
};
|
||||
|
||||
// 3. 可选删除文件(临时文件 + 最终文件)
|
||||
if delete_files {
|
||||
if let Some(task) = &task {
|
||||
let _ = std::fs::remove_file(task.file_path());
|
||||
let _ = std::fs::remove_file(task.temp_file_path());
|
||||
}
|
||||
}
|
||||
|
||||
self.persist_now();
|
||||
self.schedule();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取所有任务
|
||||
pub fn get_tasks(&self) -> Vec<DownloadTask> {
|
||||
self.inner.tasks.lock().unwrap().values().cloned().collect()
|
||||
}
|
||||
|
||||
/// 获取设置
|
||||
pub fn get_settings(&self) -> DownloaderSettings {
|
||||
self.inner.settings.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// 保存设置
|
||||
pub fn save_settings(&self, settings: DownloaderSettings) {
|
||||
// 更新全局限速器
|
||||
let new_limit = if settings.global_speed_limit > 0 {
|
||||
settings.global_speed_limit * 1024
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.inner.global_limiter.set_limit(new_limit);
|
||||
|
||||
{
|
||||
let mut s = self.inner.settings.lock().unwrap();
|
||||
*s = settings;
|
||||
}
|
||||
self.persist_now();
|
||||
}
|
||||
|
||||
/// 引擎是否已启动
|
||||
pub fn is_started(&self) -> bool {
|
||||
self.inner.started.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// 退出时清理:停止所有下载、保存状态
|
||||
pub fn cleanup_on_exit(&self) {
|
||||
// 取消所有活跃下载
|
||||
{
|
||||
let handles = self.inner.handles.lock().unwrap();
|
||||
for handle in handles.values() {
|
||||
handle.cancel.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
// 将 Active 任务标记为 Paused
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
for task in tasks.values_mut() {
|
||||
if task.status == TaskStatus::Active {
|
||||
task.status = TaskStatus::Paused;
|
||||
task.speed = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 等待短暂时间让下载任务退出
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
|
||||
// 最终保存
|
||||
self.persist_now();
|
||||
}
|
||||
|
||||
// ===================== 内部调度逻辑 =====================
|
||||
|
||||
/// 调度:如果活跃任务数 < max_concurrent,启动排队任务
|
||||
fn schedule(&self) {
|
||||
let max_concurrent = self.inner.settings.lock().unwrap().max_concurrent as usize;
|
||||
|
||||
let (active_count, queued_ids) = {
|
||||
let tasks = self.inner.tasks.lock().unwrap();
|
||||
let active = tasks.values().filter(|t| t.status == TaskStatus::Active).count();
|
||||
let mut queued: Vec<_> = tasks
|
||||
.values()
|
||||
.filter(|t| t.status == TaskStatus::Queued)
|
||||
.collect();
|
||||
queued.sort_by_key(|t| t.created_at);
|
||||
(active, queued.into_iter().map(|t| t.id.clone()).collect::<Vec<_>>())
|
||||
};
|
||||
|
||||
if active_count >= max_concurrent {
|
||||
return;
|
||||
}
|
||||
|
||||
let slots = max_concurrent.saturating_sub(active_count);
|
||||
for id in queued_ids.into_iter().take(slots) {
|
||||
self.start_download(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动单个下载任务
|
||||
fn start_download(&self, id: String) {
|
||||
let task = {
|
||||
let mut tasks = self.inner.tasks.lock().unwrap();
|
||||
match tasks.get_mut(&id) {
|
||||
Some(task) if task.status == TaskStatus::Queued => {
|
||||
task.status = TaskStatus::Active;
|
||||
task.speed = 0;
|
||||
task.clone()
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
};
|
||||
|
||||
// 创建取消标志和进度计数器
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let progress: Vec<Arc<std::sync::atomic::AtomicU64>> = task
|
||||
.segments
|
||||
.iter()
|
||||
.map(|s| Arc::new(std::sync::atomic::AtomicU64::new(s.completed)))
|
||||
.collect();
|
||||
|
||||
// 存储句柄
|
||||
let handle = TaskHandle {
|
||||
cancel: cancel.clone(),
|
||||
progress: progress.iter().map(|p| p.clone()).collect(),
|
||||
join: Mutex::new(None),
|
||||
};
|
||||
self.inner.handles.lock().unwrap().insert(id.clone(), handle);
|
||||
|
||||
// 生成下载 future
|
||||
let engine = self.clone();
|
||||
let id_clone = id.clone();
|
||||
let cancel_clone = cancel.clone();
|
||||
let progress_clone: Vec<Arc<std::sync::atomic::AtomicU64>> =
|
||||
progress.iter().map(|p| p.clone()).collect();
|
||||
let limiter = self.inner.global_limiter.clone();
|
||||
let url = task.url.clone();
|
||||
let headers = task.headers.clone();
|
||||
let segments = task.segments.clone();
|
||||
let temp_file_path = task.temp_file_path();
|
||||
let final_file_path = task.file_path();
|
||||
let http = self.inner.http.clone();
|
||||
|
||||
let join = tauri::async_runtime::spawn(async move {
|
||||
let result = http
|
||||
.download(&url, &headers, &segments, &temp_file_path, cancel_clone, &progress_clone, limiter)
|
||||
.await;
|
||||
|
||||
// 下载结束,更新任务状态
|
||||
let final_status = match &result {
|
||||
Ok(()) => TaskStatus::Complete,
|
||||
Err(e) if e == "已取消" => TaskStatus::Paused,
|
||||
Err(_) => TaskStatus::Error,
|
||||
};
|
||||
|
||||
// 下载成功后,将临时文件重命名为最终文件名
|
||||
if final_status == TaskStatus::Complete {
|
||||
let _ = tokio::fs::rename(&temp_file_path, &final_file_path).await;
|
||||
}
|
||||
|
||||
// 同步最终进度到任务
|
||||
{
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap();
|
||||
if let Some(task) = tasks.get_mut(&id_clone) {
|
||||
for (i, prog) in progress_clone.iter().enumerate() {
|
||||
if let Some(seg) = task.segments.get_mut(i) {
|
||||
seg.completed = prog.load(Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
task.recalc_completed();
|
||||
task.speed = 0;
|
||||
task.status = final_status.clone();
|
||||
if let Err(e) = &result {
|
||||
if e != "已取消" {
|
||||
task.error = Some(e.clone());
|
||||
}
|
||||
}
|
||||
if final_status == TaskStatus::Complete {
|
||||
task.completed_size = task.total_size.max(task.completed_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从活跃句柄中移除
|
||||
engine.inner.handles.lock().unwrap().remove(&id_clone);
|
||||
|
||||
// 发送完成事件
|
||||
let task = engine.inner.tasks.lock().unwrap().get(&id_clone).cloned();
|
||||
if let Some(task) = task {
|
||||
let _ = engine.inner.app_handle.emit(
|
||||
"download-complete",
|
||||
CompletePayload {
|
||||
id: id_clone.clone(),
|
||||
filename: task.filename.clone(),
|
||||
status: task.status.clone(),
|
||||
error: task.error.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 持久化 + 调度下一个
|
||||
engine.persist_now();
|
||||
engine.schedule();
|
||||
});
|
||||
|
||||
// 存储 JoinHandle
|
||||
if let Some(h) = self.inner.handles.lock().unwrap().get_mut(&id) {
|
||||
if let Ok(mut join_guard) = h.join.lock() {
|
||||
*join_guard = Some(join);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动进度监控(每 500ms 更新一次)
|
||||
let engine = self.clone();
|
||||
let id_monitor = id.clone();
|
||||
let progress_monitor = progress;
|
||||
let cancel_monitor = cancel;
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 初始化为当前已下载量,避免恢复下载时首次计算速度异常
|
||||
let mut last_completed: u64 = progress_monitor
|
||||
.iter()
|
||||
.map(|p| p.load(Ordering::Relaxed))
|
||||
.sum();
|
||||
let mut last_time = Instant::now();
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(500));
|
||||
interval.tick().await; // 跳过第一次立即触发
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
// 如果任务已不在活跃状态,停止监控
|
||||
let is_active = {
|
||||
let tasks = engine.inner.tasks.lock().unwrap();
|
||||
tasks.get(&id_monitor).map(|t| t.status == TaskStatus::Active).unwrap_or(false)
|
||||
};
|
||||
if !is_active || cancel_monitor.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 读取进度
|
||||
let completed: u64 = progress_monitor
|
||||
.iter()
|
||||
.map(|p| p.load(Ordering::Relaxed))
|
||||
.sum();
|
||||
|
||||
// 计算速度
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(last_time).as_secs_f64();
|
||||
let speed = if elapsed > 0.0 && completed >= last_completed {
|
||||
((completed - last_completed) as f64 / elapsed) as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
last_completed = completed;
|
||||
last_time = now;
|
||||
|
||||
// 更新任务状态 + 发送进度事件
|
||||
let total_size = {
|
||||
let mut tasks = engine.inner.tasks.lock().unwrap();
|
||||
if let Some(task) = tasks.get_mut(&id_monitor) {
|
||||
task.completed_size = completed;
|
||||
task.speed = speed;
|
||||
for (i, prog) in progress_monitor.iter().enumerate() {
|
||||
if let Some(seg) = task.segments.get_mut(i) {
|
||||
seg.completed = prog.load(Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
task.total_size
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let _ = engine.inner.app_handle.emit(
|
||||
"download-progress",
|
||||
ProgressPayload {
|
||||
id: id_monitor.clone(),
|
||||
completed_size: completed,
|
||||
total_size,
|
||||
speed,
|
||||
status: TaskStatus::Active,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 持久化 =====================
|
||||
|
||||
/// 节流持久化(至少间隔 3 秒)
|
||||
fn persist_throttled(&self) {
|
||||
let should_save = {
|
||||
let last = self.inner.last_save.lock().unwrap();
|
||||
last.elapsed() >= Duration::from_secs(3)
|
||||
};
|
||||
if should_save {
|
||||
self.persist_now();
|
||||
}
|
||||
}
|
||||
|
||||
/// 立即持久化
|
||||
fn persist_now(&self) {
|
||||
let tasks: Vec<DownloadTask> = {
|
||||
let tasks = self.inner.tasks.lock().unwrap();
|
||||
tasks.values().cloned().collect()
|
||||
};
|
||||
let settings = self.inner.settings.lock().unwrap().clone();
|
||||
let state = EngineState {
|
||||
tasks,
|
||||
settings,
|
||||
next_id: 0, // storage.save 会从 id_counter 读取
|
||||
};
|
||||
self.inner.storage.save(state);
|
||||
*self.inner.last_save.lock().unwrap() = Instant::now();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user