Skip to content

Commit 1809849

Browse files
committed
refactor(backend): reorganize utils and game modules
- Move game_cover, launch, game_monitor, scan to game/ module - Move backup functions from fs.rs to backup/covers.rs and backup/database.rs - Merge image.rs and image_proxy.rs into single image.rs - Clean up fs.rs to pure file operations only - Remove all mod.rs files, use same-name file pattern
1 parent d168379 commit 1809849

23 files changed

Lines changed: 899 additions & 915 deletions

File tree

src-tauri/src/backup.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
pub mod archive;
2-
pub mod savedata;
1+
pub mod archive;
2+
pub mod common;
3+
pub mod covers;
4+
pub mod database;
5+
pub mod savedata;

src-tauri/src/backup/common.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use crate::database::dto::UpdateSettingsData;
2+
use crate::database::repository::settings_repository::{DbSettingsExt, SettingsRepository};
3+
use sea_orm::DatabaseConnection;
4+
use serde::{Deserialize, Serialize};
5+
use std::fs;
6+
use std::path::{Path, PathBuf};
7+
8+
#[derive(Debug, Default, Serialize, Deserialize)]
9+
#[serde(default, rename_all = "camelCase")]
10+
pub struct BackupOptions {
11+
pub auto: bool,
12+
pub max_auto_backups: Option<usize>,
13+
}
14+
15+
#[derive(Debug, Serialize, Deserialize)]
16+
pub struct BackupResult {
17+
pub success: bool,
18+
pub path: Option<String>,
19+
pub message: String,
20+
}
21+
22+
pub async fn resolve_backup_dir(db: &DatabaseConnection) -> Result<PathBuf, String> {
23+
let settings = db.get_settings().await?;
24+
25+
if let Some(custom) = settings.db_backup_path_value() {
26+
let custom_path = PathBuf::from(custom);
27+
if custom_path.is_dir() {
28+
return Ok(custom_path);
29+
}
30+
31+
log::warn!(
32+
"自定义数据库备份目录无效,清空设置并回退默认目录: {}",
33+
custom
34+
);
35+
SettingsRepository::update_settings(
36+
db,
37+
UpdateSettingsData {
38+
db_backup_path: Some(None),
39+
..Default::default()
40+
},
41+
)
42+
.await
43+
.map_err(|e| format!("清空无效数据库备份路径失败: {}", e))?;
44+
}
45+
46+
let backup_dir = reina_path::get_default_db_backup_path()?;
47+
fs::create_dir_all(&backup_dir).map_err(|e| format!("无法创建备份目录: {}", e))?;
48+
49+
Ok(backup_dir)
50+
}
51+
52+
pub fn cleanup_auto_backup_files(
53+
backup_dir: &Path,
54+
prefix: &str,
55+
extension: &str,
56+
max_count: usize,
57+
) -> Result<Vec<String>, String> {
58+
let max_count = max_count.max(1);
59+
let entries = fs::read_dir(backup_dir).map_err(|e| format!("读取备份目录失败: {}", e))?;
60+
let mut files = Vec::new();
61+
62+
for entry in entries {
63+
let entry = entry.map_err(|e| format!("读取备份文件失败: {}", e))?;
64+
let path = entry.path();
65+
if !path.is_file() {
66+
continue;
67+
}
68+
69+
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
70+
continue;
71+
};
72+
73+
if file_name.starts_with(prefix) && file_name.ends_with(extension) {
74+
files.push((file_name.to_string(), path));
75+
}
76+
}
77+
78+
files.sort_by(|a, b| a.0.cmp(&b.0));
79+
if files.len() <= max_count {
80+
return Ok(Vec::new());
81+
}
82+
83+
let remove_count = files.len() - max_count;
84+
let mut deleted_files = Vec::new();
85+
for (file_name, path) in files.into_iter().take(remove_count) {
86+
fs::remove_file(&path)
87+
.map_err(|e| format!("删除旧自动备份失败 {}: {}", path.to_string_lossy(), e))?;
88+
deleted_files.push(file_name);
89+
}
90+
91+
Ok(deleted_files)
92+
}

src-tauri/src/backup/covers.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use crate::backup::archive::create_7z_archive;
2+
use crate::backup::common::{
3+
BackupOptions, BackupResult, cleanup_auto_backup_files, resolve_backup_dir,
4+
};
5+
use sea_orm::DatabaseConnection;
6+
use std::fs;
7+
use std::path::Path;
8+
use tauri::{State, command};
9+
10+
/// 备份所有自定义封面(仅自定义封面,不含云端缓存)
11+
///
12+
/// 将自定义封面文件复制到临时目录后压缩为 7z 文件,
13+
/// 备份路径跟随数据库备份路径逻辑。
14+
///
15+
/// 压缩包内结构(解压后直接覆盖到数据目录即可):
16+
/// ```text
17+
/// covers/
18+
/// game_123/
19+
/// cover_123_jpg_1703123456789.jpg
20+
/// game_456/
21+
/// cover_456_png_1703123456789.png
22+
/// ```
23+
#[command]
24+
pub async fn backup_custom_covers(
25+
db: State<'_, DatabaseConnection>,
26+
options: Option<BackupOptions>,
27+
) -> Result<BackupResult, String> {
28+
let options = options.unwrap_or_default();
29+
let result = backup_custom_covers_archive(&db, options.auto).await?;
30+
31+
if options.auto
32+
&& let Some(max_auto_backups) = options.max_auto_backups
33+
{
34+
let backup_dir = resolve_backup_dir(&db).await?;
35+
if let Err(e) =
36+
cleanup_auto_backup_files(&backup_dir, "custom_covers_auto_", ".7z", max_auto_backups)
37+
{
38+
log::warn!("清理旧自定义封面自动备份失败: {}", e);
39+
}
40+
}
41+
42+
Ok(result)
43+
}
44+
45+
pub async fn backup_custom_covers_archive(
46+
db: &DatabaseConnection,
47+
auto: bool,
48+
) -> Result<BackupResult, String> {
49+
// 1. 获取封面根目录
50+
let covers_dir = reina_path::get_base_data_dir()?.join("covers");
51+
if !covers_dir.exists() {
52+
return Ok(BackupResult {
53+
success: true,
54+
path: None,
55+
message: "没有自定义封面需要备份".to_string(),
56+
});
57+
}
58+
59+
// 2. 创建临时目录,内层套 covers 文件夹方便用户直接覆盖
60+
let timestamp = chrono::Local::now().timestamp_millis();
61+
let temp_dir = std::env::temp_dir().join(format!("reina_covers_{}", timestamp));
62+
let covers_temp_dir = temp_dir.join("covers");
63+
64+
// 3. 遍历 covers 目录,仅复制自定义封面文件
65+
let mut has_covers = false;
66+
let scan_result = scan_and_copy_custom_covers(&covers_dir, &covers_temp_dir, &mut has_covers);
67+
68+
// 扫描失败时清理临时目录
69+
if let Err(e) = scan_result {
70+
fs::remove_dir_all(&temp_dir).ok();
71+
return Err(e);
72+
}
73+
74+
if !has_covers {
75+
fs::remove_dir_all(&temp_dir).ok();
76+
return Ok(BackupResult {
77+
success: true,
78+
path: None,
79+
message: "没有自定义封面需要备份".to_string(),
80+
});
81+
}
82+
83+
// 4. 压缩为 7z 文件
84+
let backup_dir = match resolve_backup_dir(db).await {
85+
Ok(dir) => dir,
86+
Err(e) => {
87+
fs::remove_dir_all(&temp_dir).ok();
88+
return Err(e);
89+
}
90+
};
91+
let archive_prefix = if auto {
92+
"custom_covers_auto"
93+
} else {
94+
"custom_covers"
95+
};
96+
let archive_name = format!(
97+
"{}_{}.7z",
98+
archive_prefix,
99+
chrono::Local::now().format("%Y%m%d_%H%M%S")
100+
);
101+
let archive_path = backup_dir.join(&archive_name);
102+
103+
let size = match create_7z_archive(&temp_dir, &archive_path) {
104+
Ok(size) => size,
105+
Err(e) => {
106+
fs::remove_dir_all(&temp_dir).ok();
107+
return Err(format!("压缩自定义封面失败: {}", e));
108+
}
109+
};
110+
111+
// 5. 清理临时目录
112+
fs::remove_dir_all(&temp_dir).ok();
113+
114+
log::info!(
115+
"自定义封面备份成功: {} ({} bytes)",
116+
archive_path.display(),
117+
size
118+
);
119+
120+
Ok(BackupResult {
121+
success: true,
122+
path: Some(archive_path.to_string_lossy().to_string()),
123+
message: "自定义封面备份成功".to_string(),
124+
})
125+
}
126+
127+
pub fn delete_all_covers_dir() -> Result<(), String> {
128+
let covers_dir = reina_path::get_base_data_dir()?.join("covers");
129+
130+
if !covers_dir.exists() {
131+
return Ok(());
132+
}
133+
134+
fs::remove_dir_all(&covers_dir)
135+
.map_err(|e| format!("无法删除封面目录 {}: {}", covers_dir.display(), e))?;
136+
137+
Ok(())
138+
}
139+
140+
/// 扫描 covers 目录并将自定义封面文件复制到临时目录
141+
///
142+
/// 只复制匹配 `cover_{game_id}_*` 模式的文件,跳过云端缓存(`cloud_*`)。
143+
/// 无自定义封面的 game 目录不会被创建。
144+
fn scan_and_copy_custom_covers(
145+
covers_dir: &Path,
146+
temp_dir: &Path,
147+
has_covers: &mut bool,
148+
) -> Result<(), String> {
149+
let entries = fs::read_dir(covers_dir).map_err(|e| format!("无法读取封面目录: {}", e))?;
150+
151+
for entry in entries {
152+
let entry = entry.map_err(|e| format!("读取目录项失败: {}", e))?;
153+
let entry_path = entry.path();
154+
155+
if !entry_path.is_dir() {
156+
continue;
157+
}
158+
159+
let dir_name = entry.file_name().to_string_lossy().to_string();
160+
161+
// 只处理 game_{id} 格式的目录
162+
let game_id_str = match dir_name.strip_prefix("game_") {
163+
Some(id) => id,
164+
None => continue,
165+
};
166+
167+
// 自定义封面文件名格式:cover_{game_id}_{ext}_{timestamp}.{ext}
168+
let expected_prefix = format!("cover_{}", game_id_str);
169+
let mut game_has_covers = false;
170+
171+
let file_entries = fs::read_dir(&entry_path)
172+
.map_err(|e| format!("无法读取游戏封面目录 {}: {}", dir_name, e))?;
173+
174+
for file_entry in file_entries {
175+
let file_entry = file_entry.map_err(|e| format!("读取游戏封面目录项失败: {}", e))?;
176+
177+
if !file_entry.path().is_file() {
178+
continue;
179+
}
180+
181+
let file_name = file_entry.file_name().to_string_lossy().to_string();
182+
183+
// 匹配 cover_{id}_ 前缀(自定义封面),精确匹配游戏ID避免 cover_1 匹配到 cover_10
184+
if !file_name.starts_with(&expected_prefix) {
185+
continue;
186+
}
187+
// 确保是 cover_{id}_ 格式(前缀后面紧跟下划线),而不是 cover_{id}10 等
188+
let rest = &file_name[expected_prefix.len()..];
189+
if !rest.starts_with('_') {
190+
continue;
191+
}
192+
193+
// 首次为该游戏创建目录
194+
if !game_has_covers {
195+
let game_temp_dir = temp_dir.join(&dir_name);
196+
fs::create_dir_all(&game_temp_dir)
197+
.map_err(|e| format!("创建临时目录失败: {}", e))?;
198+
game_has_covers = true;
199+
}
200+
201+
let target_path = temp_dir.join(&dir_name).join(&file_name);
202+
fs::copy(file_entry.path(), &target_path)
203+
.map_err(|e| format!("复制自定义封面文件失败 {}: {}", file_name, e))?;
204+
}
205+
206+
if game_has_covers {
207+
*has_covers = true;
208+
}
209+
}
210+
211+
Ok(())
212+
}

0 commit comments

Comments
 (0)