Skip to content

Commit ff2a0a0

Browse files
committed
feat: run legacy cover migrations at startup
Add a new utils::legacy_migration module to migrate legacy "covers" from the portable resources directory into the current data directory, resolving duplicates by modification time and removing emptied legacy folders. Refactor get_system_data_dir to use a single identifier ("com.reinamanager.dev"). Misc small cleanups
1 parent 3d2dc37 commit ff2a0a0

6 files changed

Lines changed: 255 additions & 97 deletions

File tree

src-tauri/reina-path/src/lib.rs

Lines changed: 79 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,79 @@
1-
use std::path::PathBuf;
2-
3-
/// 数据库相关路径常量
4-
pub const DB_DATA_DIR: &str = "data";
5-
pub const DB_FILE_NAME: &str = "reina_manager.db";
6-
pub const DB_BACKUP_SUBDIR: &str = "backups";
7-
pub const RESOURCE_DIR: &str = "resources";
8-
9-
/// 判断是否处于便携模式(纯 Rust 版本)
10-
///
11-
/// 检测逻辑:检查可执行文件同级目录下是否存在 resources/data/reina_manager.db
12-
pub fn is_portable_mode() -> bool {
13-
if let Ok(exe_path) = std::env::current_exe() {
14-
if let Some(exe_dir) = exe_path.parent() {
15-
let portable_data_dir = exe_dir.join(RESOURCE_DIR).join(DB_DATA_DIR);
16-
let portable_db_file = portable_data_dir.join(DB_FILE_NAME);
17-
return portable_data_dir.exists() && portable_db_file.exists();
18-
}
19-
}
20-
false
21-
}
22-
23-
/// 获取基础数据目录(纯 Rust 版本)
24-
pub fn get_base_data_dir() -> Result<PathBuf, String> {
25-
if is_portable_mode() {
26-
// 便携模式:使用可执行文件所在目录的 resources 子目录
27-
let exe_path =
28-
std::env::current_exe().map_err(|e| format!("无法获取可执行文件路径: {}", e))?;
29-
let exe_dir = exe_path
30-
.parent()
31-
.ok_or_else(|| "无法获取可执行文件父目录".to_string())?;
32-
Ok(exe_dir.join(RESOURCE_DIR))
33-
} else {
34-
// 标准模式:使用系统应用数据目录
35-
get_system_data_dir()
36-
}
37-
}
38-
39-
/// 获取系统数据目录(跨平台)
40-
fn get_system_data_dir() -> Result<PathBuf, String> {
41-
use directories::BaseDirs;
42-
43-
let base_dirs = BaseDirs::new().ok_or_else(|| "无法获取系统目录信息".to_string())?;
44-
45-
#[cfg(target_os = "windows")]
46-
{
47-
Ok(base_dirs.data_dir().join("com.reinamanager.dev"))
48-
}
49-
50-
#[cfg(target_os = "macos")]
51-
{
52-
Ok(base_dirs.data_dir().join("com.reinamanager.dev"))
53-
}
54-
55-
#[cfg(target_os = "linux")]
56-
{
57-
Ok(base_dirs.data_dir().join("reina-manager"))
58-
}
59-
}
60-
61-
/// 获取数据库文件路径
62-
pub fn get_db_path() -> Result<PathBuf, String> {
63-
Ok(get_base_data_dir()?.join(DB_DATA_DIR).join(DB_FILE_NAME))
64-
}
65-
66-
/// 获取指定模式的数据库目录
67-
pub fn get_base_data_dir_for_mode(portable: bool) -> Result<PathBuf, String> {
68-
if portable {
69-
let exe_path =
70-
std::env::current_exe().map_err(|e| format!("无法获取可执行文件路径: {}", e))?;
71-
let exe_dir = exe_path
72-
.parent()
73-
.ok_or_else(|| "无法获取可执行文件父目录".to_string())?;
74-
Ok(exe_dir.join(RESOURCE_DIR))
75-
} else {
76-
get_system_data_dir()
77-
}
78-
}
79-
80-
/// 获取默认的数据库备份路径
81-
pub fn get_default_db_backup_path() -> Result<PathBuf, String> {
82-
Ok(get_base_data_dir()?
83-
.join(DB_DATA_DIR)
84-
.join(DB_BACKUP_SUBDIR))
85-
}
86-
87-
/// 获取默认的存档备份路径
88-
pub fn get_default_savedata_backup_path() -> Result<PathBuf, String> {
89-
Ok(get_base_data_dir()?.join("backups"))
90-
}
1+
use std::path::PathBuf;
2+
3+
/// 数据库相关路径常量
4+
pub const DB_DATA_DIR: &str = "data";
5+
pub const DB_FILE_NAME: &str = "reina_manager.db";
6+
pub const DB_BACKUP_SUBDIR: &str = "backups";
7+
pub const RESOURCE_DIR: &str = "resources";
8+
9+
/// 判断是否处于便携模式(纯 Rust 版本)
10+
///
11+
/// 检测逻辑:检查可执行文件同级目录下是否存在 resources/data/reina_manager.db
12+
pub fn is_portable_mode() -> bool {
13+
if let Ok(exe_path) = std::env::current_exe() {
14+
if let Some(exe_dir) = exe_path.parent() {
15+
let portable_data_dir = exe_dir.join(RESOURCE_DIR).join(DB_DATA_DIR);
16+
let portable_db_file = portable_data_dir.join(DB_FILE_NAME);
17+
return portable_data_dir.exists() && portable_db_file.exists();
18+
}
19+
}
20+
false
21+
}
22+
23+
/// 获取基础数据目录(纯 Rust 版本)
24+
pub fn get_base_data_dir() -> Result<PathBuf, String> {
25+
if is_portable_mode() {
26+
// 便携模式:使用可执行文件所在目录的 resources 子目录
27+
let exe_path =
28+
std::env::current_exe().map_err(|e| format!("无法获取可执行文件路径: {}", e))?;
29+
let exe_dir = exe_path
30+
.parent()
31+
.ok_or_else(|| "无法获取可执行文件父目录".to_string())?;
32+
Ok(exe_dir.join(RESOURCE_DIR))
33+
} else {
34+
// 标准模式:使用系统应用数据目录
35+
get_system_data_dir()
36+
}
37+
}
38+
39+
/// 获取系统数据目录(跨平台)
40+
fn get_system_data_dir() -> Result<PathBuf, String> {
41+
use directories::BaseDirs;
42+
43+
let identifier = "com.reinamanager.dev";
44+
45+
let base_dirs = BaseDirs::new().ok_or_else(|| "无法获取系统目录信息".to_string())?;
46+
47+
Ok(base_dirs.data_dir().join(identifier))
48+
}
49+
50+
/// 获取数据库文件路径
51+
pub fn get_db_path() -> Result<PathBuf, String> {
52+
Ok(get_base_data_dir()?.join(DB_DATA_DIR).join(DB_FILE_NAME))
53+
}
54+
55+
/// 获取指定模式的数据库目录
56+
pub fn get_base_data_dir_for_mode(portable: bool) -> Result<PathBuf, String> {
57+
if portable {
58+
let exe_path =
59+
std::env::current_exe().map_err(|e| format!("无法获取可执行文件路径: {}", e))?;
60+
let exe_dir = exe_path
61+
.parent()
62+
.ok_or_else(|| "无法获取可执行文件父目录".to_string())?;
63+
Ok(exe_dir.join(RESOURCE_DIR))
64+
} else {
65+
get_system_data_dir()
66+
}
67+
}
68+
69+
/// 获取默认的数据库备份路径
70+
pub fn get_default_db_backup_path() -> Result<PathBuf, String> {
71+
Ok(get_base_data_dir()?
72+
.join(DB_DATA_DIR)
73+
.join(DB_BACKUP_SUBDIR))
74+
}
75+
76+
/// 获取默认的存档备份路径
77+
pub fn get_default_savedata_backup_path() -> Result<PathBuf, String> {
78+
Ok(get_base_data_dir()?.join("backups"))
79+
}

src-tauri/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use utils::{
1515
},
1616
game_cover::{delete_cloud_cache, register_game_cover_protocol},
1717
launch::{launch_game, stop_game},
18+
legacy_migration::run_startup_migrations,
1819
logs::{get_reina_log_level, set_reina_log_level},
1920
scan::scan_directory_for_games,
2021
};
@@ -165,6 +166,25 @@ pub fn run() {
165166
let path_manager = PathManager::new();
166167
app.manage(path_manager);
167168

169+
match run_startup_migrations() {
170+
Ok(result) if result.executed == 0 => {
171+
log::debug!("启动迁移检查完成,无需执行");
172+
}
173+
Ok(result) => {
174+
log::info!(
175+
"启动迁移完成: executed={}, skipped={}, moved={}, replaced={}, removed_legacy={}",
176+
result.executed,
177+
result.skipped,
178+
result.migrated_files,
179+
result.replaced_files,
180+
result.removed_legacy_files
181+
);
182+
}
183+
Err(err) => {
184+
log::error!("启动迁移失败: {}", err);
185+
}
186+
}
187+
168188
// 执行 SeaORM 数据库迁移并注册到状态管理
169189
let app_handle = app.handle().clone();
170190
tauri::async_runtime::block_on(async move {

src-tauri/src/utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ pub mod fs;
33
pub mod game_cover;
44
pub mod game_monitor;
55
pub mod launch;
6+
pub mod legacy_migration;
67
pub mod logs;
78
pub mod scan;

src-tauri/src/utils/command_ext.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#[cfg(target_os = "windows")]
12
use std::process::{Command, Stdio};
23

34
/// 为 GUI 环境下启动子进程提供统一的标准流处理。
@@ -8,16 +9,10 @@ pub trait CommandGuiExt {
89

910
impl CommandGuiExt for Command {
1011
fn gui_safe(&mut self) -> &mut Self {
11-
#[cfg(target_os = "windows")]
1212
{
1313
self.stdin(Stdio::null())
1414
.stdout(Stdio::null())
1515
.stderr(Stdio::null())
1616
}
17-
18-
#[cfg(not(target_os = "windows"))]
19-
{
20-
self
21-
}
2217
}
2318
}

src-tauri/src/utils/fs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::process::Command;
88
use std::sync::Mutex;
99
use tauri::command;
1010

11-
// ==================== 路径相关常量(重导出) ====================
11+
// ==================== 路径相关常量 ====================
1212

1313
pub use reina_path::{DB_BACKUP_SUBDIR, DB_DATA_DIR};
1414

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use std::fs;
2+
use std::path::Path;
3+
use std::time::SystemTime;
4+
5+
use reina_path::{get_base_data_dir, get_base_data_dir_for_mode};
6+
7+
use crate::utils::fs::move_file;
8+
9+
#[derive(Debug, Default)]
10+
pub struct StartupMigrationResult {
11+
pub migrated_files: usize,
12+
pub replaced_files: usize,
13+
pub removed_legacy_files: usize,
14+
pub skipped: usize,
15+
pub executed: usize,
16+
}
17+
18+
pub fn run_startup_migrations() -> Result<StartupMigrationResult, String> {
19+
let mut result = StartupMigrationResult::default();
20+
21+
run_startup_migration(&mut result, m20260326_000001_migrate_legacy_covers)?;
22+
23+
Ok(result)
24+
}
25+
26+
fn run_startup_migration(
27+
aggregate: &mut StartupMigrationResult,
28+
migration: fn() -> Result<StartupMigrationResult, String>,
29+
) -> Result<(), String> {
30+
let result = migration()?;
31+
32+
aggregate.migrated_files += result.migrated_files;
33+
aggregate.replaced_files += result.replaced_files;
34+
aggregate.removed_legacy_files += result.removed_legacy_files;
35+
aggregate.skipped += result.skipped;
36+
aggregate.executed += result.executed;
37+
38+
Ok(())
39+
}
40+
41+
fn m20260326_000001_migrate_legacy_covers() -> Result<StartupMigrationResult, String> {
42+
let legacy_covers_dir = get_base_data_dir_for_mode(true)?.join("covers");
43+
let current_covers_dir = get_base_data_dir()?.join("covers");
44+
45+
if current_covers_dir == legacy_covers_dir || !legacy_covers_dir.exists() {
46+
return Ok(StartupMigrationResult {
47+
skipped: 1,
48+
..Default::default()
49+
});
50+
}
51+
52+
if !legacy_covers_dir.is_dir() {
53+
return Err(format!(
54+
"旧版 covers 路径不是目录: {}",
55+
legacy_covers_dir.display()
56+
));
57+
}
58+
59+
fs::create_dir_all(&current_covers_dir).map_err(|e| {
60+
format!(
61+
"无法创建新的 covers 目录 {}: {}",
62+
current_covers_dir.display(),
63+
e
64+
)
65+
})?;
66+
67+
let mut result = StartupMigrationResult {
68+
executed: 1,
69+
..Default::default()
70+
};
71+
72+
merge_covers_dir(&legacy_covers_dir, &current_covers_dir, &mut result)?;
73+
remove_dir_if_empty(&legacy_covers_dir)?;
74+
75+
Ok(result)
76+
}
77+
78+
fn merge_covers_dir(
79+
from_dir: &Path,
80+
to_dir: &Path,
81+
result: &mut StartupMigrationResult,
82+
) -> Result<(), String> {
83+
for entry in fs::read_dir(from_dir)
84+
.map_err(|e| format!("读取 legacy covers 目录失败 {}: {}", from_dir.display(), e))?
85+
{
86+
let entry = entry.map_err(|e| format!("读取 legacy covers 项失败: {}", e))?;
87+
let from_path = entry.path();
88+
let to_path = to_dir.join(entry.file_name());
89+
90+
if from_path.is_dir() {
91+
fs::create_dir_all(&to_path)
92+
.map_err(|e| format!("创建目标目录失败 {}: {}", to_path.display(), e))?;
93+
merge_covers_dir(&from_path, &to_path, result)?;
94+
remove_dir_if_empty(&from_path)?;
95+
continue;
96+
}
97+
98+
if !from_path.is_file() {
99+
continue;
100+
}
101+
102+
if !to_path.exists() {
103+
move_file(&from_path, &to_path)?;
104+
result.migrated_files += 1;
105+
continue;
106+
}
107+
108+
if !to_path.is_file() {
109+
return Err(format!(
110+
"目标 covers 路径已存在且不是文件: {}",
111+
to_path.display()
112+
));
113+
}
114+
115+
let from_modified = file_modified_time(&from_path)?;
116+
let to_modified = file_modified_time(&to_path)?;
117+
118+
if from_modified > to_modified {
119+
fs::remove_file(&to_path)
120+
.map_err(|e| format!("删除旧目标文件失败 {}: {}", to_path.display(), e))?;
121+
move_file(&from_path, &to_path)?;
122+
result.migrated_files += 1;
123+
result.replaced_files += 1;
124+
} else {
125+
fs::remove_file(&from_path)
126+
.map_err(|e| format!("删除 legacy 重复文件失败 {}: {}", from_path.display(), e))?;
127+
result.removed_legacy_files += 1;
128+
}
129+
}
130+
131+
Ok(())
132+
}
133+
134+
fn file_modified_time(path: &Path) -> Result<SystemTime, String> {
135+
fs::metadata(path)
136+
.and_then(|metadata| metadata.modified())
137+
.map_err(|e| format!("读取文件修改时间失败 {}: {}", path.display(), e))
138+
}
139+
140+
fn remove_dir_if_empty(path: &Path) -> Result<(), String> {
141+
if !path.exists() || !path.is_dir() {
142+
return Ok(());
143+
}
144+
145+
let mut entries = fs::read_dir(path)
146+
.map_err(|e| format!("检查目录是否为空失败 {}: {}", path.display(), e))?;
147+
148+
if entries.next().is_none() {
149+
fs::remove_dir(path).map_err(|e| format!("删除空目录失败 {}: {}", path.display(), e))?;
150+
}
151+
152+
Ok(())
153+
}

0 commit comments

Comments
 (0)