1use crate::database;
10use crate::database::{current_schema_version, AppState, SCHEMA_VERSION};
11use crate::errors::AppError;
12use crate::models::{Game, GameDetails, WishlistGame};
13use chrono::Utc;
14use rusqlite::params;
15use std::fs;
16use std::path::PathBuf;
17use tauri::{AppHandle, Manager, State};
18
19#[derive(serde::Serialize, serde::Deserialize)]
23pub struct BackupData {
24 pub version: u32, pub app_version: String,
26 pub date: String,
27 pub games: Vec<Game>,
28 pub game_details: Vec<GameDetails>,
29 pub wishlist_game: Vec<WishlistGame>,
30}
31
32fn fetch_backup_data(
36 state: &State<AppState>,
37) -> Result<(Vec<Game>, Vec<GameDetails>, Vec<WishlistGame>, u32), AppError> {
38 let conn = state.library_db.lock()?;
39
40 conn.execute("BEGIN TRANSACTION", [])?;
42
43 let games = fetch_games(&conn)?;
44 let game_details = fetch_game_details(&conn)?;
45 let wishlist_game = fetch_wishlist(&conn)?;
46 let schema_version = current_schema_version(&conn)?;
47
48 conn.execute("COMMIT", [])?;
49
50 Ok((games, game_details, wishlist_game, schema_version))
51}
52
53#[tauri::command]
58pub async fn export_database(
59 app: AppHandle,
60 state: State<'_, AppState>,
61 file_path: String,
62) -> Result<(), AppError> {
63 let (games, game_details, wishlist_game, schema_version) = fetch_backup_data(&state)?;
65
66 let backup = BackupData {
67 version: schema_version,
68 app_version: app.package_info().version.to_string(),
69 date: chrono::Local::now().to_rfc3339(),
70 games,
71 game_details,
72 wishlist_game,
73 };
74
75 let json = serde_json::to_string_pretty(&backup)?;
76 fs::write(file_path, json)?;
77
78 let metadata_conn = state.metadata_db.lock().map_err(|_| AppError::MutexError)?;
80 let now = Utc::now().to_rfc3339();
81 database::configs::set_config(&metadata_conn, "last_backup_at", &now)?;
82
83 Ok(())
84}
85
86pub fn backup_if_major_update(
87 app: &AppHandle,
88 previous_version: &str,
89 current_version: &str,
90) -> Result<Option<PathBuf>, AppError> {
91 let parse_version = |v: &str| -> (u32, u32, u32) {
93 let parts: Vec<&str> = v.split('.').collect();
94 let major = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
95 let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
96 let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
97 (major, minor, patch)
98 };
99
100 let (prev_major, _, _) = parse_version(previous_version);
101 let (curr_major, _, _) = parse_version(current_version);
102
103 if prev_major != curr_major && prev_major > 0 {
105 tracing::info!(
106 "Mudança de versão major detectada: v{} -> v{}",
107 previous_version,
108 current_version
109 );
110 let backup_path = backup_before_update(app, previous_version)?;
111 Ok(Some(backup_path))
112 } else {
113 Ok(None)
114 }
115}
116
117pub fn backup_before_update(app: &AppHandle, previous_version: &str) -> Result<PathBuf, AppError> {
121 tracing::info!("Criando backup automático antes da atualização...");
122
123 let app_data_dir = app
124 .path()
125 .app_data_dir()
126 .map_err(|e| AppError::IoError(format!("Falha ao obter app_data_dir: {}", e)))?;
127
128 let backups_dir = app_data_dir.join("backups");
129 std::fs::create_dir_all(&backups_dir)?;
130
131 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
133 let backup_filename = format!("auto_backup_v{}_{}.json", previous_version, timestamp);
134 let backup_path = backups_dir.join(backup_filename);
135
136 let state: tauri::State<AppState> = app.state();
138 let (games, game_details, wishlist_game, schema_version) = fetch_backup_data(&state)?;
139
140 let backup = BackupData {
141 version: schema_version,
142 app_version: previous_version.to_string(),
143 date: chrono::Local::now().to_rfc3339(),
144 games,
145 game_details,
146 wishlist_game,
147 };
148
149 let json = serde_json::to_string_pretty(&backup)?;
150 fs::write(&backup_path, json)?;
151
152 let metadata_conn = state.metadata_db.lock().map_err(|_| AppError::MutexError)?;
154 let now = Utc::now().to_rfc3339();
155 database::configs::set_config(&metadata_conn, "last_auto_backup_at", &now)?;
156
157 tracing::info!("Backup automático criado: {:?}", backup_path);
158 Ok(backup_path)
159}
160
161#[tauri::command]
169pub async fn import_database(
170 _app: AppHandle,
171 state: State<'_, AppState>,
172 file_path: String,
173) -> Result<String, AppError> {
174 let content = fs::read_to_string(file_path)?;
175 let backup: BackupData = serde_json::from_str(&content)
176 .map_err(|_| AppError::ValidationError("Arquivo de backup inválido".to_string()))?;
177
178 if backup.version != SCHEMA_VERSION {
180 return Err(AppError::ValidationError(format!(
181 "Backup incompatível. Backup v{}, app espera v{}",
182 backup.version, SCHEMA_VERSION
183 )));
184 }
185
186 let conn = state.library_db.lock()?;
187
188 conn.execute("BEGIN IMMEDIATE TRANSACTION", [])?;
190
191 let mut game_stmt = conn.prepare(
193 "INSERT OR REPLACE INTO games (id, name, cover_url, platform, platform_id, install_path, executable_path, launch_args, user_rating, favorite, status, playtime, last_played, added_at)
194 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)"
195 )?;
196
197 let mut details_stmt = conn.prepare(
199 "INSERT OR REPLACE INTO game_details (
200 game_id, steam_app_id, developer, publisher, release_date, genres, tags, series,
201 description_raw, description_ptbr, background_image, critic_score, steam_review_label,
202 steam_review_count, steam_review_score, steam_review_updated_at, esrb_rating, is_adult,
203 adult_tags, external_links, median_playtime
204 )
205 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21)"
206 )?;
207
208 let mut wishlist_stmt = conn.prepare(
209 "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)
210 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)"
211 )?;
212
213 for game in &backup.games {
214 game_stmt.execute(rusqlite::params![
215 game.id,
216 game.name,
217 game.cover_url,
218 game.platform,
219 game.platform_id,
220 game.install_path,
221 game.executable_path,
222 game.launch_args,
223 game.user_rating,
224 game.favorite,
225 game.status,
226 game.playtime,
227 game.last_played,
228 game.added_at
229 ])?;
230 }
231
232 for detail in &backup.game_details {
234 let links_json = detail
236 .external_links
237 .as_ref()
238 .and_then(|links| serde_json::to_string(links).ok());
239
240 let tags_json = detail
241 .tags
242 .as_ref()
243 .and_then(|tags| crate::database::serialize_tags(tags).ok());
244
245 details_stmt.execute(params![
246 detail.game_id,
247 detail.steam_app_id,
248 detail.developer,
249 detail.publisher,
250 detail.release_date,
251 detail.genres,
252 tags_json,
253 detail.series,
254 detail.description_raw,
255 detail.description_ptbr,
256 detail.background_image,
257 detail.critic_score,
258 detail.steam_review_label,
259 detail.steam_review_count,
260 detail.steam_review_score,
261 detail.steam_review_updated_at,
262 detail.esrb_rating,
263 detail.is_adult,
264 detail.adult_tags,
265 links_json, detail.median_playtime
267 ])?;
268 }
269
270 for item in &backup.wishlist_game {
271 wishlist_stmt.execute(rusqlite::params![
272 item.id,
273 item.name,
274 item.cover_url,
275 item.store_url,
276 item.store_platform,
277 item.current_price,
278 item.normal_price,
279 item.lowest_price,
280 item.currency,
281 item.on_sale,
282 item.voucher,
283 item.added_at,
284 item.itad_id
285 ])?;
286 }
287
288 conn.execute("COMMIT", [])?;
289
290 Ok(format!(
291 "Backup restaurado! {} jogos, {} detalhes de jogos e {} itens da lista de desejos.",
292 backup.games.len(),
293 backup.game_details.len(),
294 backup.wishlist_game.len()
295 ))
296}
297
298fn fetch_games(conn: &rusqlite::Connection) -> Result<Vec<Game>, AppError> {
302 let mut stmt = conn.prepare(
303 "SELECT id, name, cover_url, platform, platform_id, install_path, executable_path, launch_args, user_rating, favorite, status, playtime, last_played, added_at FROM games"
304 )?;
305
306 let game_iter = stmt.query_map([], |row| {
307 Ok(Game {
308 id: row.get(0)?,
309 name: row.get(1)?,
310 cover_url: row.get(2)?,
311 genres: None,
312 developer: None,
313 platform: row.get(3)?,
314 platform_id: row.get(4)?,
315 install_path: row.get(5)?,
316 executable_path: row.get(6)?,
317 launch_args: row.get(7)?,
318 user_rating: row.get(8)?,
319 favorite: row.get(9)?,
320 status: row.get(10)?,
321 playtime: row.get(11)?,
322 last_played: row.get(12)?,
323 added_at: row.get(13)?,
324 is_adult: false,
325 })
326 })?;
327
328 Ok(game_iter.collect::<Result<Vec<_>, _>>()?)
329}
330
331fn fetch_game_details(conn: &rusqlite::Connection) -> Result<Vec<GameDetails>, AppError> {
335 let mut stmt = conn.prepare(
336 "SELECT
337 game_id, steam_app_id, developer, publisher, release_date, genres, tags, series,
338 description_raw, description_ptbr, background_image, critic_score,
339 steam_review_label, steam_review_count, steam_review_score, steam_review_updated_at,
340 esrb_rating, is_adult, adult_tags, external_links, median_playtime,
341 estimated_playtime
342 FROM game_details",
343 )?;
344
345 let details_iter = stmt.query_map([], |row| {
346 let links_json: Option<String> = row.get(19)?;
348 let external_links = links_json.and_then(|s| serde_json::from_str(&s).ok());
349
350 let tags_json: Option<String> = row.get(6)?;
351 let tags = tags_json.map(|s| crate::database::deserialize_tags(&s));
352
353 Ok(GameDetails {
354 game_id: row.get(0)?,
355 steam_app_id: row.get(1)?,
356 developer: row.get(2)?,
357 publisher: row.get(3)?,
358 release_date: row.get(4)?,
359 genres: row.get(5)?,
360 tags,
361 series: row.get(7)?,
362 description_raw: row.get(8)?,
363 description_ptbr: row.get(9)?,
364 background_image: row.get(10)?,
365 critic_score: row.get(11)?,
366 steam_review_label: row.get(12)?,
367 steam_review_count: row.get(13)?,
368 steam_review_score: row.get(14)?,
369 steam_review_updated_at: row.get(15)?,
370 esrb_rating: row.get(16)?,
371 is_adult: row.get(17).unwrap_or(false),
372 adult_tags: row.get(18)?,
373 external_links,
374 median_playtime: row.get(20)?,
375 estimated_playtime: row.get(21)?,
376 })
377 })?;
378
379 Ok(details_iter.collect::<Result<Vec<_>, _>>()?)
380}
381
382fn fetch_wishlist(conn: &rusqlite::Connection) -> Result<Vec<WishlistGame>, AppError> {
386 let mut stmt = conn.prepare(
387 "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"
388 )?;
389
390 let wishlist_iter = stmt.query_map([], |row| {
391 Ok(WishlistGame {
392 id: row.get(0)?,
393 name: row.get(1)?,
394 cover_url: row.get(2)?,
395 store_url: row.get(3)?,
396 store_platform: row.get(4)?,
397 itad_id: row.get(5)?,
398 current_price: row.get(6)?,
399 normal_price: row.get(7)?,
400 lowest_price: row.get(8)?,
401 currency: row.get(9)?,
402 on_sale: row.get(10)?,
403 voucher: row.get(11)?,
404 added_at: row.get(12)?,
405 })
406 })?;
407
408 Ok(wishlist_iter.collect::<Result<Vec<_>, _>>()?)
409}