game_manager_lib\database/
core.rs1use crate::constants::{
14 DB_FILENAME_LIBRARY, DB_FILENAME_METADATA, DB_FILENAME_SECRETS, DB_JOURNAL_MODE,
15};
16use crate::errors::AppError;
17use crate::security;
18use rusqlite::{params, Connection};
19use std::sync::Mutex;
20use tauri::State;
21use tauri::{AppHandle, Manager};
22
23pub struct AppState {
25 pub library_db: Mutex<Connection>,
26 pub secrets_db: Mutex<Connection>,
27 pub metadata_db: Mutex<Connection>,
28}
29
30pub const SCHEMA_VERSION: u32 = 3;
31
32pub fn current_schema_version(conn: &Connection) -> Result<u32, AppError> {
34 let version: i32 = conn
35 .query_row("PRAGMA user_version", [], |row| row.get(0))
36 .unwrap_or(0);
37
38 Ok(version.max(0) as u32)
39}
40
41pub fn expected_schema_version(app: &AppHandle) -> u32 {
43 let version = app.package_info().version.clone();
45 version.major as u32
46}
47
48pub fn initialize_databases(app: &AppHandle) -> Result<AppState, String> {
57 let app_data_dir = app
58 .path()
59 .app_data_dir()
60 .map_err(|e| format!("Falha ao obter app_data_dir: {}", e))?;
61
62 std::fs::create_dir_all(&app_data_dir)
63 .map_err(|e| format!("Falha ao criar diretório: {}", e))?;
64
65 let library_path = app_data_dir.join(DB_FILENAME_LIBRARY);
67 let library_conn = Connection::open(&library_path)
68 .map_err(|e| format!("Erro ao abrir {}: {}", DB_FILENAME_LIBRARY, e))?;
69
70 library_conn
71 .pragma_update(None, "journal_mode", DB_JOURNAL_MODE)
72 .map_err(|e| format!("Erro ao configurar WAL no library.db: {}", e))?;
73
74 crate::database::migrations::run_migrations(app, &library_conn)?;
76
77 create_schema(&library_conn)?;
79
80 let secrets_path = app_data_dir.join(DB_FILENAME_SECRETS);
82 let secrets_conn = Connection::open(&secrets_path)
83 .map_err(|e| format!("Erro ao abrir {}: {}", DB_FILENAME_SECRETS, e))?;
84
85 secrets_conn
86 .pragma_update(None, "journal_mode", DB_JOURNAL_MODE)
87 .map_err(|e| format!("Erro ao configurar WAL no secrets.db: {}", e))?;
88
89 let metadata_path = app_data_dir.join(DB_FILENAME_METADATA);
91 let metadata_conn = Connection::open(&metadata_path)
92 .map_err(|e| format!("Erro ao abrir {}: {}", DB_FILENAME_METADATA, e))?;
93
94 metadata_conn
95 .pragma_update(None, "journal_mode", DB_JOURNAL_MODE)
96 .map_err(|e| format!("Erro ao configurar WAL no metadata.db: {}", e))?;
97
98 crate::services::cache::initialize_cache_db(&metadata_conn)?;
100
101 Ok(AppState {
102 library_db: Mutex::new(library_conn),
103 secrets_db: Mutex::new(secrets_conn),
104 metadata_db: Mutex::new(metadata_conn),
105 })
106}
107
108fn create_schema(conn: &Connection) -> Result<(), String> {
117 conn.execute(
118 "CREATE TABLE IF NOT EXISTS games (
119 id TEXT PRIMARY KEY,
120 name TEXT NOT NULL,
121 cover_url TEXT,
122 platform TEXT NOT NULL,
123 platform_id TEXT,
124 install_path TEXT,
125 executable_path TEXT,
126 launch_args TEXT,
127 user_rating INTEGER,
128 favorite BOOLEAN DEFAULT 0,
129 status TEXT,
130 playtime INTEGER,
131 last_played TEXT,
132 added_at TEXT NOT NULL
133 )",
134 [],
135 )
136 .map_err(|e| e.to_string())?;
137
138 conn.execute(
139 "CREATE TABLE IF NOT EXISTS game_details (
140 game_id TEXT PRIMARY KEY,
141 steam_app_id TEXT,
142 developer TEXT,
143 publisher TEXT,
144 release_date TEXT,
145 genres TEXT,
146 tags TEXT,
147 series TEXT,
148 description_raw TEXT,
149 description_ptbr TEXT,
150 background_image TEXT,
151 critic_score INTEGER,
152 steam_review_label TEXT,
153 steam_review_count INTEGER,
154 steam_review_score REAL,
155 steam_review_updated_at TEXT,
156 esrb_rating TEXT,
157 is_adult BOOLEAN DEFAULT 0,
158 adult_tags TEXT,
159 external_links TEXT,
160 median_playtime INTEGER,
161 estimated_playtime REAL,
162 FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE
163 )",
164 [],
165 )
166 .map_err(|e| e.to_string())?;
167
168 conn.execute(
169 "CREATE TABLE IF NOT EXISTS wishlist (
170 id TEXT PRIMARY KEY,
171 name TEXT NOT NULL,
172 cover_url TEXT,
173 store_url TEXT,
174 store_platform TEXT,
175 current_price REAL,
176 normal_price REAL,
177 lowest_price REAL,
178 currency TEXT,
179 on_sale BOOLEAN DEFAULT 0,
180 voucher TEXT,
181 added_at TEXT,
182 itad_id TEXT
183 )",
184 [],
185 )
186 .map_err(|e| e.to_string())?;
187
188 conn.execute(
190 "CREATE INDEX IF NOT EXISTS idx_name ON games(name COLLATE NOCASE)",
191 [],
192 )
193 .map_err(|e| e.to_string())?;
194
195 conn.execute(
196 "CREATE INDEX IF NOT EXISTS idx_platform ON games(platform)",
197 [],
198 )
199 .map_err(|e| e.to_string())?;
200
201 conn.execute(
202 "CREATE INDEX IF NOT EXISTS idx_favorite ON games(favorite)",
203 [],
204 )
205 .map_err(|e| e.to_string())?;
206
207 conn.execute("CREATE INDEX IF NOT EXISTS idx_status ON games(status)", [])
208 .map_err(|e| e.to_string())?;
209
210 conn.pragma_update(None, "user_version", SCHEMA_VERSION)
212 .map_err(|e| format!("Erro ao definir versão do schema: {}", e))?;
213
214 Ok(())
215}
216
217#[tauri::command]
221pub fn init_db(app: AppHandle, state: State<AppState>) -> Result<String, String> {
222 let conn = state
223 .library_db
224 .lock()
225 .map_err(|_| "Falha ao bloquear mutex do library_db")?;
226
227 let current_version = current_schema_version(&conn).unwrap_or(0) as i32;
228 let expected_version = expected_schema_version(&app) as i32;
229
230 if current_version == 0 {
231 return Ok(format!("Banco de dados novo criado (v{})", SCHEMA_VERSION));
232 }
233
234 if current_version != expected_version {
235 let app_data_dir = app
236 .path()
237 .app_data_dir()
238 .map_err(|e| format!("Falha ao obter app_data_dir: {}", e))?;
239
240 return Err(format!(
241 "Banco desatualizado: Schema atual: v{}, esperado: v{}. Faça backup, exclua o diretório da aplicação em: {:?} e reinicie para recriar o banco.",
242 current_version, expected_version, app_data_dir
243 ));
244 }
245
246 Ok(format!("Banco de dados OK (v{})", current_version))
247}
248
249pub fn serialize_tags(tags: &[crate::models::GameTag]) -> Result<String, String> {
251 serde_json::to_string(tags).map_err(|e| e.to_string())
252}
253
254pub fn deserialize_tags(tags_json: &str) -> Vec<crate::models::GameTag> {
256 use crate::utils::tag_utils::{TagCategory, TagRole};
257
258 if let Ok(tags) = serde_json::from_str::<Vec<crate::models::GameTag>>(tags_json) {
260 return tags;
261 }
262
263 tags_json
265 .split(',')
266 .map(|s| s.trim())
267 .filter(|s| !s.is_empty())
268 .map(|slug| crate::models::GameTag {
269 slug: slug.to_string(),
270 name: slug.to_string(),
271 category: TagCategory::Meta,
272 role: TagRole::Context,
273 relevance: 5.0,
274 })
275 .collect()
276}
277
278fn get_secrets_connection<'a>(
283 state: &'a State<AppState>,
284) -> Result<std::sync::MutexGuard<'a, Connection>, String> {
285 let conn = state
286 .secrets_db
287 .lock()
288 .map_err(|_| "Falha ao bloquear mutex do secrets_db".to_string())?;
289
290 conn.execute(
291 r#"
292 CREATE TABLE IF NOT EXISTS encrypted_keys (
293 key TEXT PRIMARY KEY,
294 value TEXT NOT NULL
295 )
296 "#,
297 [],
298 )
299 .map_err(|e: rusqlite::Error| e.to_string())?;
300
301 Ok(conn)
302}
303
304pub fn set_secret(app: &AppHandle, key_name: &str, value: &str) -> Result<(), AppError> {
306 let state: tauri::State<AppState> = app.state();
307 let conn = get_secrets_connection(&state)?;
308
309 let encrypted = security::encrypt(app, value)?;
310
311 conn.execute(
312 "INSERT OR REPLACE INTO encrypted_keys (key, value) VALUES (?1, ?2)",
313 params![key_name, encrypted],
314 )?;
315
316 Ok(())
317}
318
319pub fn get_secret(app: &AppHandle, key_name: &str) -> Result<String, AppError> {
321 let state: tauri::State<AppState> = app.state();
322 let conn = get_secrets_connection(&state)?;
323
324 let result: Result<String, rusqlite::Error> = conn.query_row(
325 "SELECT value FROM encrypted_keys WHERE key = ?1",
326 params![key_name],
327 |row| row.get::<_, String>(0),
328 );
329
330 match result {
331 Ok(encrypted) => {
332 let decrypted = security::decrypt(app, &encrypted)?;
333 Ok(decrypted)
334 }
335 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(String::new()),
336 Err(e) => Err(AppError::DatabaseError(e.to_string())),
337 }
338}
339
340pub fn delete_secret(app: &AppHandle, key_name: &str) -> Result<(), AppError> {
342 let state: tauri::State<AppState> = app.state();
343 let conn = get_secrets_connection(&state)?;
344
345 conn.execute(
346 "DELETE FROM encrypted_keys WHERE key = ?1",
347 params![key_name],
348 )?;
349
350 Ok(())
351}
352
353pub fn list_supported_keys() -> Vec<&'static str> {
355 vec![
356 "steam_id",
357 "steam_api_key",
358 "rawg_api_key",
359 "gemini_api_key",
360 ]
361}