game_manager_lib\database/
backup.rs

1//! Módulo de ‘backup’ e restauração de dados.
2//!
3//! Fornece funcionalidades para exportar e importar a base de dados completa
4//! em formato JSON, incluindo biblioteca de jogos e lista de desejos.
5//!
6//! **Nota:**
7//! Todas as operações usam transações ACID para garantir consistência dos dados.
8
9use 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/// Estrutura do arquivo de ‘backup’.
20///
21/// Contém metadados e todos os dados exportados da aplicação.
22#[derive(serde::Serialize, serde::Deserialize)]
23pub struct BackupData {
24    pub version: u32, // schema == backup
25    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
32/// Função auxiliar interna para buscar dados do backup com transação ACID
33///
34/// Retorna tupla com (games, game_details, wishlist_game, schema_version)
35fn 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    // Inicia transação READ para consistência
41    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/// Exporta toda a base de dados para um arquivo JSON.
54///
55/// Cria um snapshot completo e consistente de todos os dados da aplicação,
56/// incluindo jogos e wishlist, num único arquivo JSON formatado.
57#[tauri::command]
58pub async fn export_database(
59    app: AppHandle,
60    state: State<'_, AppState>,
61    file_path: String,
62) -> Result<(), AppError> {
63    // Buscar dados com transação ACID
64    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    // Atualiza timestamp do último backup manual
79    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    // Parse das versões
92    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    // Se versão major mudou, faz backup
104    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
117/// Cria backup automático antes de atualização de versão
118///
119/// Chamado automaticamente quando detecta mudança de versão major
120pub 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    // Nome do backup com timestamp e versão anterior
132    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    // Reutiliza a função auxiliar de fetch
137    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    // Atualiza timestamp do último backup automático
153    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/// Importa e restaura dados de um arquivo de 'backup'.
162///
163/// Lê um arquivo JSON de 'backup' restaura todos os dados no banco,
164/// substituindo registros existentes (INSERT OR REPLACE).
165///
166/// **Nota:**
167/// Esta operação pode sobrescrever dados existentes. Considere criar um 'backup' antes de importar.
168#[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    // Validação de versão
179    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    // Transação única para todas as operações
189    conn.execute("BEGIN IMMEDIATE TRANSACTION", [])?;
190
191    // Usa prepared statements para melhor desempenho
192    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    // Prepared Statement para Details (ATUALIZADO COM NOVOS CAMPOS)
198    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    // Loop Details
233    for detail in &backup.game_details {
234        // Serializa o HashMap de links e as tags para JSON String antes de salvar
235        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, // JSON String
266            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
298/// Busca todos os jogos na biblioteca
299///
300/// Retorna um vetor de `Game` ou um erro AppError.
301fn 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
331/// Busca todos os detalhes dos jogos na biblioteca
332///
333/// Retorna um vetor de `GameDetails` ou um erro AppError.
334fn 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        // Deserializa JSON strings
347        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
382/// Busca todos os jogos da wishlist
383///
384/// Retorna um vetor de `WishlistGame` ou um erro AppError.
385fn 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}