game_manager_lib\database/
core.rs

1//! Módulo de gerenciamento do banco de dados da aplicação.
2//!
3//! Gerencia a criação e inicialização do banco SQLite para a biblioteca de jogos e wishlist,
4//! além do armazenamento seguro de secrets (API keys, tokens) com criptografia.
5//!
6//! **Bancos de Dados:**
7//! - library.db: armazena a biblioteca de jogos e wishlist do usuário.
8//! - secrets.db: armazena secrets encriptados com AES-256-GCM.
9//! - metadata.db: cache para respostas de APIs externas (RAWG, Steam).
10//!
11//! **Versão do Schema:** v3
12
13use 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
23/// Define o estado global da aplicação com ambas as conexões
24pub 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
32/// Retorna a versão atual do schema armazenada no banco
33pub 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
41/// Retorna a versão do schema esperada para esta versão do app
42pub fn expected_schema_version(app: &AppHandle) -> u32 {
43    // Usa o MAJOR da versão do app
44    let version = app.package_info().version.clone();
45    version.major as u32
46}
47
48// === INICIALIZAÇÃO CENTRALIZADA ===
49
50/// Inicializa ambos os bancos de dados e retorna o estado da aplicação
51///
52/// **Erros:**
53/// - Se não conseguir criar os diretórios
54/// - Se não conseguir abrir as conexões
55/// - Se falhar ao configurar WAL mode
56pub 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    // Conexão para library.db
66    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    // Executa migrations
75    crate::database::migrations::run_migrations(app, &library_conn)?;
76
77    // Cria schema completo
78    create_schema(&library_conn)?;
79
80    // Conexão para secrets.db
81    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    // Conexão para metadata.db (cache)
90    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    // Inicializa schema do cache
99    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
108// === BANCO DE DADOS DE GERENCIAMENTO DE BIBLIOTECAS E WISHLIST  ===
109
110/// Cria o schema completo do banco de dados (versão v3)
111///
112/// **Schema v3:**
113/// - Campos HLTB removidos
114/// - URLs legadas removidas (agora em external_links JSON)
115/// - users_score removido (substituído por steam_review_*)
116fn 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    // Índices
189    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    // Marca versão do schema
211    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/// Inicializa o banco de dados e verifica a versão do schema.
218///
219/// Se o banco estiver desatualizado (< v3), retorna erro com instruções para o usuário.
220#[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
249/// Serializa tags para salvar no banco
250pub fn serialize_tags(tags: &[crate::models::GameTag]) -> Result<String, String> {
251    serde_json::to_string(tags).map_err(|e| e.to_string())
252}
253
254/// Deserializa tags do banco (com fallback para formato antigo)
255pub fn deserialize_tags(tags_json: &str) -> Vec<crate::models::GameTag> {
256    use crate::utils::tag_utils::{TagCategory, TagRole};
257
258    // Tenta deserializar como novo formato
259    if let Ok(tags) = serde_json::from_str::<Vec<crate::models::GameTag>>(tags_json) {
260        return tags;
261    }
262
263    // Fallback: formato antigo (string separada por vírgulas)
264    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
278// === BANCO DE DADOS PARA GERENCIAMENTO DE API KEYS (secrets.db) ===
279
280/// Obtém conexão com o banco de secrets a partir do AppState.
281/// Cria automaticamente a tabela `encrypted_keys` se não existir.
282fn 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
304/// Salva um secret encriptado no banco. Se a chave já existir, o valor é substituído (upsert).
305pub 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
319/// Recupera e decripta um secret do banco. Se a chave não existir, retorna string vazia ao invés de erro.
320pub 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
340/// Remove um secret do banco permanentemente.
341pub 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
353/// Retorna lista de chaves de secrets suportadas pela aplicação.
354pub 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}