1use crate::database;
10use crate::database::{current_schema_version, AppState};
11use crate::errors::AppError;
12use crate::models::{
13 Game, GameDataPath, GameDetails, GameExtras, Platform, SystemRequirements, WishlistGame,
14};
15use chrono::Utc;
16use rusqlite::params;
17use std::fs;
18use std::path::PathBuf;
19use tauri::{AppHandle, Manager, State};
20
21type BackupDataTuple = (
23 Vec<Game>,
24 Vec<GameDetails>,
25 Vec<WishlistGame>,
26 Vec<GameExtras>,
27 Vec<SystemRequirements>,
28 Vec<GameDataPath>,
29 u32,
30);
31
32#[derive(serde::Serialize, serde::Deserialize)]
36pub struct BackupData {
37 pub version: u32, pub app_version: String,
39 pub date: String,
40 pub games: Vec<Game>,
41 pub game_details: Vec<GameDetails>,
42 pub wishlist_game: Vec<WishlistGame>,
43 #[serde(default)]
46 pub game_extras: Vec<GameExtras>,
47 #[serde(default)]
49 pub system_requirements: Vec<SystemRequirements>,
50 #[serde(default)]
52 pub game_data_paths: Vec<GameDataPath>,
53}
54
55fn fetch_backup_data(state: &State<AppState>) -> Result<BackupDataTuple, AppError> {
59 let conn = state.games_db.lock()?;
60
61 conn.execute("BEGIN TRANSACTION", [])?;
63
64 let games = fetch_games(&conn)?;
65 let game_details = fetch_game_details(&conn)?;
66 let wishlist_game = fetch_wishlist(&conn)?;
67 let game_extras = fetch_game_extras(&conn)?;
68 let system_requirements = fetch_system_requirements(&conn)?;
69 let game_data_paths = fetch_game_data_paths(&conn)?;
70 let schema_version = current_schema_version(&conn)?;
71
72 conn.execute("COMMIT", [])?;
73
74 Ok((
75 games,
76 game_details,
77 wishlist_game,
78 game_extras,
79 system_requirements,
80 game_data_paths,
81 schema_version,
82 ))
83}
84
85#[tauri::command]
90pub async fn export_database(
91 app: AppHandle,
92 state: State<'_, AppState>,
93 file_path: String,
94) -> Result<(), AppError> {
95 let (
97 games,
98 game_details,
99 wishlist_game,
100 game_extras,
101 system_requirements,
102 game_data_paths,
103 schema_version,
104 ) = fetch_backup_data(&state)?;
105
106 let backup = BackupData {
107 version: schema_version,
108 app_version: app.package_info().version.to_string(),
109 date: chrono::Local::now().to_rfc3339(),
110 games,
111 game_details,
112 wishlist_game,
113 game_extras,
114 system_requirements,
115 game_data_paths,
116 };
117
118 let json = serde_json::to_string_pretty(&backup)?;
119 fs::write(file_path, json)?;
120
121 let cache_conn = state.cache_db.lock().map_err(|_| AppError::MutexError)?;
123 let now = Utc::now().to_rfc3339();
124 database::configs::set_config(&cache_conn, "last_backup_at", &now)?;
125
126 Ok(())
127}
128
129pub fn backup_if_major_update(
130 app: &AppHandle,
131 previous_version: &str,
132 current_version: &str,
133) -> Result<Option<PathBuf>, AppError> {
134 let parse_version = |v: &str| -> (u32, u32, u32) {
136 let parts: Vec<&str> = v.split('.').collect();
137 let major = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
138 let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
139 let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
140 (major, minor, patch)
141 };
142
143 let (prev_major, _, _) = parse_version(previous_version);
144 let (curr_major, _, _) = parse_version(current_version);
145
146 if prev_major != curr_major && prev_major > 0 {
148 tracing::info!(
149 "Mudança de versão major detectada: v{} -> v{}",
150 previous_version,
151 current_version
152 );
153 let backup_path = backup_before_update(app, previous_version)?;
154 Ok(Some(backup_path))
155 } else {
156 Ok(None)
157 }
158}
159
160pub fn backup_before_update(app: &AppHandle, previous_version: &str) -> Result<PathBuf, AppError> {
164 tracing::info!("Criando backup automático antes da atualização...");
165
166 let app_data_dir = app
167 .path()
168 .app_data_dir()
169 .map_err(|e| AppError::IoError(format!("Falha ao obter app_data_dir: {}", e)))?;
170
171 let backups_dir = app_data_dir.join("backups");
172 std::fs::create_dir_all(&backups_dir)?;
173
174 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
176 let backup_filename = format!("auto_backup_v{}_{}.json", previous_version, timestamp);
177 let backup_path = backups_dir.join(backup_filename);
178
179 let state: tauri::State<AppState> = app.state();
181 let (
182 games,
183 game_details,
184 wishlist_game,
185 game_extras,
186 system_requirements,
187 game_data_paths,
188 schema_version,
189 ) = fetch_backup_data(&state)?;
190
191 let backup = BackupData {
192 version: schema_version,
193 app_version: previous_version.to_string(),
194 date: chrono::Local::now().to_rfc3339(),
195 games,
196 game_details,
197 wishlist_game,
198 game_extras,
199 system_requirements,
200 game_data_paths,
201 };
202
203 let json = serde_json::to_string_pretty(&backup)?;
204 fs::write(&backup_path, json)?;
205
206 let cache_conn = state.cache_db.lock().map_err(|_| AppError::MutexError)?;
208 let now = Utc::now().to_rfc3339();
209 database::configs::set_config(&cache_conn, "last_auto_backup_at", &now)?;
210
211 tracing::info!("Backup automático criado: {:?}", backup_path);
212 Ok(backup_path)
213}
214
215#[tauri::command]
223pub async fn import_database(
224 _app: AppHandle,
225 state: State<'_, AppState>,
226 file_path: String,
227) -> Result<String, AppError> {
228 let content = fs::read_to_string(file_path)?;
229 let backup: BackupData = serde_json::from_str(&content)
230 .map_err(|_| AppError::ValidationError("Arquivo de backup inválido".to_string()))?;
231
232 let current_version = {
234 let conn = state.games_db.lock()?;
235 current_schema_version(&conn)?
236 };
237
238 if backup.version != current_version {
239 return Err(AppError::ValidationError(format!(
240 "Backup incompatível. Backup v{}, app espera v{}",
241 backup.version, current_version
242 )));
243 }
244
245 let conn = state.games_db.lock()?;
246
247 conn.execute("BEGIN IMMEDIATE TRANSACTION", [])?;
249
250 let mut game_stmt = conn.prepare(
252 "INSERT OR REPLACE INTO games (id, name, cover_url, platform, platform_game_id, installed, import_confidence, install_path, executable_path, launch_args, user_rating, favorite, status, playtime, last_played, added_at)
253 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)"
254 )?;
255
256 let mut details_stmt = conn.prepare(
258 "INSERT OR REPLACE INTO game_details (
259 game_id, steam_app_id, developer, publisher, release_date, genres, tags, series,
260 description_raw, description_ptbr, background_image, critic_score, steam_review_label,
261 steam_review_count, steam_review_score, steam_review_updated_at, esrb_rating, is_adult,
262 adult_tags, external_links, median_playtime, estimated_playtime
263 )
264 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22)"
265 )?;
266
267 let mut wishlist_stmt = conn.prepare(
268 "INSERT OR REPLACE INTO wishlist (id, name, cover_url, store_url, store_platform, current_price, normal_price, lowest_price, currency, on_sale, voucher, added_at, itad_id)
269 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)"
270 )?;
271
272 for game in &backup.games {
273 game_stmt.execute(rusqlite::params![
274 game.id,
275 game.name,
276 game.cover_url,
277 game.platform.to_string(),
278 game.platform_game_id,
279 game.installed,
280 game.import_confidence.as_ref().map(|ic| ic.to_string()),
281 game.install_path,
282 game.executable_path,
283 game.launch_args,
284 game.user_rating,
285 game.favorite,
286 game.status,
287 game.playtime,
288 game.last_played,
289 game.added_at
290 ])?;
291 }
292
293 for detail in &backup.game_details {
295 let links_json = detail
297 .external_links
298 .as_ref()
299 .and_then(|links| serde_json::to_string(links).ok());
300
301 let tags_json = detail
302 .tags
303 .as_ref()
304 .and_then(|tags| crate::database::serialize_tags(tags).ok());
305
306 details_stmt.execute(params![
307 detail.game_id,
308 detail.steam_app_id,
309 detail.developer,
310 detail.publisher,
311 detail.release_date,
312 detail.genres,
313 tags_json,
314 detail.series,
315 detail.description_raw,
316 detail.description_ptbr,
317 detail.background_image,
318 detail.critic_score,
319 detail.steam_review_label,
320 detail.steam_review_count,
321 detail.steam_review_score,
322 detail.steam_review_updated_at,
323 detail.esrb_rating,
324 detail.is_adult,
325 detail.adult_tags,
326 links_json, detail.median_playtime,
328 detail.estimated_playtime
329 ])?;
330 }
331
332 for item in &backup.wishlist_game {
333 wishlist_stmt.execute(rusqlite::params![
334 item.id,
335 item.name,
336 item.cover_url,
337 item.store_url,
338 item.store_platform,
339 item.current_price,
340 item.normal_price,
341 item.lowest_price,
342 item.currency,
343 item.on_sale,
344 item.voucher,
345 item.added_at,
346 item.itad_id
347 ])?;
348 }
349
350 let mut extras_stmt = conn.prepare(
352 "INSERT OR REPLACE INTO game_extras (
353 steam_app_id, pcgw_page_id, pcgw_page_name, engine,
354 available_on,
355 dx_versions, vulkan_versions, opengl_versions,
356 win64, linux64, macos_arm, macos_intel64,
357 ray_tracing, upscaling, frame_gen,
358 ultrawidescreen, four_k_support, hdr, high_fps, fov, borderless_windowed, color_blind,
359 controller_support, full_controller, playstation_controllers, xinput_controllers,
360 surround_sound, subtitles, closed_captions,
361 has_save_data, has_config_data,
362 languages_interface, languages_audio, languages_subtitles,
363 fetched_at
364 ) VALUES (
365 ?1, ?2, ?3, ?4,
366 ?5,
367 ?6, ?7, ?8,
368 ?9, ?10, ?11, ?12,
369 ?13, ?14, ?15,
370 ?16, ?17, ?18, ?19, ?20, ?21, ?22,
371 ?23, ?24, ?25, ?26,
372 ?27, ?28, ?29,
373 ?30, ?31,
374 ?32, ?33, ?34,
375 ?35
376 )",
377 )?;
378
379 let mut sysreq_stmt = conn.prepare(
380 "INSERT INTO system_requirements (
381 steam_app_id, os_family, tier_title, target,
382 min_os, min_cpu, min_cpu2, min_ram, min_gpu, min_gpu2, min_vram, min_dx, min_storage,
383 rec_os, rec_cpu, rec_cpu2, rec_ram, rec_gpu, rec_gpu2, rec_vram, rec_dx, rec_storage,
384 fetched_at
385 ) VALUES (
386 ?1, ?2, ?3, ?4,
387 ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13,
388 ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22,
389 ?23
390 )",
391 )?;
392
393 let mut paths_stmt = conn.prepare(
394 "INSERT INTO game_data_paths (steam_app_id, kind, os, raw_path, fetched_at)
395 VALUES (?1, ?2, ?3, ?4, ?5)",
396 )?;
397
398 let serialize_vec = |v: &Option<Vec<String>>| -> Option<String> {
399 v.as_ref().and_then(|list| serde_json::to_string(list).ok())
400 };
401
402 let now = Utc::now().to_rfc3339();
403
404 conn.execute("DELETE FROM system_requirements", [])?;
406 conn.execute("DELETE FROM game_data_paths", [])?;
407
408 for extras in &backup.game_extras {
409 extras_stmt.execute(params![
410 extras.steam_app_id,
411 extras.pcgw_page_id,
412 extras.pcgw_page_name,
413 extras.engine,
414 extras.available_on,
415 extras.dx_versions,
416 extras.vulkan_versions,
417 extras.opengl_versions,
418 extras.win64,
419 extras.linux64,
420 extras.macos_arm,
421 extras.macos_intel64,
422 extras.ray_tracing,
423 extras.upscaling,
424 extras.frame_gen,
425 extras.ultrawidescreen,
426 extras.four_k_support,
427 extras.hdr,
428 extras.high_fps,
429 extras.fov,
430 extras.borderless_windowed,
431 extras.color_blind,
432 extras.controller_support,
433 extras.full_controller,
434 extras.playstation_controllers,
435 extras.xinput_controllers,
436 extras.surround_sound,
437 extras.subtitles,
438 extras.closed_captions,
439 extras.has_save_data,
440 extras.has_config_data,
441 serialize_vec(&extras.languages_interface),
442 serialize_vec(&extras.languages_audio),
443 serialize_vec(&extras.languages_subtitles),
444 extras.fetched_at,
445 ])?;
446 }
447
448 for req in &backup.system_requirements {
449 sysreq_stmt.execute(params![
450 req.steam_app_id,
451 req.os_family,
452 req.tier_title,
453 req.target,
454 req.min_os,
455 req.min_cpu,
456 req.min_cpu2,
457 req.min_ram,
458 req.min_gpu,
459 req.min_gpu2,
460 req.min_vram,
461 req.min_dx,
462 req.min_storage,
463 req.rec_os,
464 req.rec_cpu,
465 req.rec_cpu2,
466 req.rec_ram,
467 req.rec_gpu,
468 req.rec_gpu2,
469 req.rec_vram,
470 req.rec_dx,
471 req.rec_storage,
472 now,
473 ])?;
474 }
475
476 for path in &backup.game_data_paths {
477 paths_stmt.execute(params![
478 path.steam_app_id,
479 path.kind,
480 path.os,
481 path.raw_path,
482 now,
483 ])?;
484 }
485
486 conn.execute("COMMIT", [])?;
487
488 Ok(format!(
489 "Backup restaurado! {} jogos, {} detalhes, {} itens da wishlist, {} dados técnicos, {} requisitos de sistema e {} caminhos.",
490 backup.games.len(),
491 backup.game_details.len(),
492 backup.wishlist_game.len(),
493 backup.game_extras.len(),
494 backup.system_requirements.len(),
495 backup.game_data_paths.len(),
496 ))
497}
498
499fn fetch_games(conn: &rusqlite::Connection) -> Result<Vec<Game>, AppError> {
503 let mut stmt = conn.prepare(
504 "SELECT id, name, cover_url, platform, platform_game_id, installed, import_confidence, install_path, executable_path, launch_args, user_rating, favorite, status, playtime, last_played, added_at FROM games"
505 )?;
506
507 let game_iter = stmt.query_map([], |row| {
508 Ok(Game {
509 id: row.get(0)?,
510 name: row.get(1)?,
511 cover_url: row.get(2)?,
512 genres: None,
513 developer: None,
514 platform: row.get::<_, String>(3)?.parse().unwrap_or(Platform::Outra),
515 platform_game_id: row.get(4)?,
516 installed: row.get(5)?,
517 import_confidence: row
518 .get::<_, Option<String>>(6)?
519 .and_then(|s| s.parse().ok()),
520 install_path: row.get(7)?,
521 executable_path: row.get(8)?,
522 launch_args: row.get(9)?,
523 user_rating: row.get(10)?,
524 favorite: row.get(11)?,
525 status: row.get(12)?,
526 playtime: row.get(13)?,
527 last_played: row.get(14)?,
528 added_at: row.get(15)?,
529 is_adult: false,
530 })
531 })?;
532
533 Ok(game_iter.collect::<Result<Vec<_>, _>>()?)
534}
535
536fn fetch_game_details(conn: &rusqlite::Connection) -> Result<Vec<GameDetails>, AppError> {
540 let mut stmt = conn.prepare(
541 "SELECT
542 game_id, steam_app_id, developer, publisher, release_date, genres, tags, series,
543 description_raw, description_ptbr, background_image, critic_score,
544 steam_review_label, steam_review_count, steam_review_score, steam_review_updated_at,
545 esrb_rating, is_adult, adult_tags, external_links, median_playtime,
546 estimated_playtime
547 FROM game_details",
548 )?;
549
550 let details_iter = stmt.query_map([], |row| {
551 let links_json: Option<String> = row.get(19)?;
553 let external_links = links_json.and_then(|s| serde_json::from_str(&s).ok());
554
555 let tags_json: Option<String> = row.get(6)?;
556 let tags = tags_json.map(|s| crate::database::deserialize_tags(&s));
557
558 Ok(GameDetails {
559 game_id: row.get(0)?,
560 steam_app_id: row.get(1)?,
561 developer: row.get(2)?,
562 publisher: row.get(3)?,
563 release_date: row.get(4)?,
564 genres: row.get(5)?,
565 tags,
566 series: row.get(7)?,
567 description_raw: row.get(8)?,
568 description_ptbr: row.get(9)?,
569 background_image: row.get(10)?,
570 critic_score: row.get(11)?,
571 steam_review_label: row.get(12)?,
572 steam_review_count: row.get(13)?,
573 steam_review_score: row.get(14)?,
574 steam_review_updated_at: row.get(15)?,
575 esrb_rating: row.get(16)?,
576 is_adult: row.get(17).unwrap_or(false),
577 adult_tags: row.get(18)?,
578 external_links,
579 median_playtime: row.get(20)?,
580 estimated_playtime: row.get(21)?,
581 })
582 })?;
583
584 Ok(details_iter.collect::<Result<Vec<_>, _>>()?)
585}
586
587fn fetch_wishlist(conn: &rusqlite::Connection) -> Result<Vec<WishlistGame>, AppError> {
591 let mut stmt = conn.prepare(
592 "SELECT id, name, cover_url, store_url, store_platform, itad_id, current_price, normal_price, lowest_price, currency, on_sale, voucher, added_at FROM wishlist"
593 )?;
594
595 let wishlist_iter = stmt.query_map([], |row| {
596 Ok(WishlistGame {
597 id: row.get(0)?,
598 name: row.get(1)?,
599 cover_url: row.get(2)?,
600 store_url: row.get(3)?,
601 store_platform: row.get(4)?,
602 itad_id: row.get(5)?,
603 current_price: row.get(6)?,
604 normal_price: row.get(7)?,
605 lowest_price: row.get(8)?,
606 currency: row.get(9)?,
607 on_sale: row.get(10)?,
608 voucher: row.get(11)?,
609 added_at: row.get(12)?,
610 })
611 })?;
612
613 Ok(wishlist_iter.collect::<Result<Vec<_>, _>>()?)
614}
615
616fn fetch_game_extras(conn: &rusqlite::Connection) -> Result<Vec<GameExtras>, AppError> {
620 let mut stmt = conn.prepare(
621 "SELECT
622 steam_app_id, pcgw_page_id, pcgw_page_name, engine,
623 available_on,
624 dx_versions, vulkan_versions, opengl_versions,
625 win64, linux64, macos_arm, macos_intel64,
626 ray_tracing, upscaling, frame_gen,
627 ultrawidescreen, four_k_support, hdr, high_fps, fov, borderless_windowed, color_blind,
628 controller_support, full_controller, playstation_controllers, xinput_controllers,
629 surround_sound, subtitles, closed_captions,
630 has_save_data, has_config_data,
631 languages_interface, languages_audio, languages_subtitles,
632 fetched_at
633 FROM game_extras
634 WHERE fetched_at IS NOT NULL",
635 )?;
636
637 let parse_json_vec = |s: Option<String>| -> Option<Vec<String>> {
638 s.and_then(|v| serde_json::from_str(&v).ok())
639 };
640
641 let iter = stmt.query_map([], |row| {
642 Ok(GameExtras {
643 steam_app_id: row.get(0)?,
644 pcgw_page_id: row.get(1)?,
645 pcgw_page_name: row.get(2)?,
646 engine: row.get(3)?,
647 available_on: row.get(4)?,
648 dx_versions: row.get(5)?,
649 vulkan_versions: row.get(6)?,
650 opengl_versions: row.get(7)?,
651 win64: row.get(8)?,
652 linux64: row.get(9)?,
653 macos_arm: row.get(10)?,
654 macos_intel64: row.get(11)?,
655 ray_tracing: row.get(12)?,
656 upscaling: row.get(13)?,
657 frame_gen: row.get(14)?,
658 ultrawidescreen: row.get(15)?,
659 four_k_support: row.get(16)?,
660 hdr: row.get(17)?,
661 high_fps: row.get(18)?,
662 fov: row.get(19)?,
663 borderless_windowed: row.get(20)?,
664 color_blind: row.get(21)?,
665 controller_support: row.get(22)?,
666 full_controller: row.get(23)?,
667 playstation_controllers: row.get(24)?,
668 xinput_controllers: row.get(25)?,
669 surround_sound: row.get(26)?,
670 subtitles: row.get(27)?,
671 closed_captions: row.get(28)?,
672 has_save_data: row.get(29)?,
673 has_config_data: row.get(30)?,
674 languages_interface: parse_json_vec(row.get(31)?),
675 languages_audio: parse_json_vec(row.get(32)?),
676 languages_subtitles: parse_json_vec(row.get(33)?),
677 fetched_at: row.get(34)?,
678 })
679 })?;
680
681 Ok(iter.collect::<Result<Vec<_>, _>>()?)
682}
683
684fn fetch_system_requirements(
686 conn: &rusqlite::Connection,
687) -> Result<Vec<SystemRequirements>, AppError> {
688 let mut stmt = conn.prepare(
689 "SELECT
690 steam_app_id, os_family, tier_title, target,
691 min_os, min_cpu, min_cpu2, min_ram, min_gpu, min_gpu2, min_vram, min_dx, min_storage,
692 rec_os, rec_cpu, rec_cpu2, rec_ram, rec_gpu, rec_gpu2, rec_vram, rec_dx, rec_storage
693 FROM system_requirements
694 ORDER BY steam_app_id, id ASC",
695 )?;
696
697 let iter = stmt.query_map([], |row| {
698 Ok(SystemRequirements {
699 steam_app_id: row.get(0)?,
700 os_family: row.get(1)?,
701 tier_title: row.get(2)?,
702 target: row.get(3)?,
703 min_os: row.get(4)?,
704 min_cpu: row.get(5)?,
705 min_cpu2: row.get(6)?,
706 min_ram: row.get(7)?,
707 min_gpu: row.get(8)?,
708 min_gpu2: row.get(9)?,
709 min_vram: row.get(10)?,
710 min_dx: row.get(11)?,
711 min_storage: row.get(12)?,
712 rec_os: row.get(13)?,
713 rec_cpu: row.get(14)?,
714 rec_cpu2: row.get(15)?,
715 rec_ram: row.get(16)?,
716 rec_gpu: row.get(17)?,
717 rec_gpu2: row.get(18)?,
718 rec_vram: row.get(19)?,
719 rec_dx: row.get(20)?,
720 rec_storage: row.get(21)?,
721 })
722 })?;
723
724 Ok(iter.collect::<Result<Vec<_>, _>>()?)
725}
726
727fn fetch_game_data_paths(conn: &rusqlite::Connection) -> Result<Vec<GameDataPath>, AppError> {
729 let mut stmt = conn.prepare(
730 "SELECT steam_app_id, kind, os, raw_path
731 FROM game_data_paths
732 ORDER BY steam_app_id, id ASC",
733 )?;
734
735 let iter = stmt.query_map([], |row| {
736 Ok(GameDataPath {
737 steam_app_id: row.get(0)?,
738 kind: row.get(1)?,
739 os: row.get(2)?,
740 raw_path: row.get(3)?,
741 expanded_path: None,
742 })
743 })?;
744
745 Ok(iter.collect::<Result<Vec<_>, _>>()?)
746}