// lib/remote/run_remote_sync.dart // // Esegue un ciclo di sincronizzazione "pull": // 1) legge le impostazioni (server, path, user, password) da RemoteSettings // 2) login → Bearer token // 3) GET dell'indice JSON (array di oggetti foto) // 4) upsert nel DB 'entry' (e 'address' se presente) tramite RemoteRepository // // NOTE: // - La versione "managed" (runRemoteSyncOnceManaged) apre/chiude il DB ed evita run concorrenti. // - La versione "plain" (runRemoteSyncOnce) usa un Database già aperto (compatibilità). // - PRAGMA per concorrenza (WAL, busy_timeout, ...). // - Non logghiamo contenuti sensibili (password/token/body completi). import 'package:path/path.dart' as p; import 'package:sqflite/sqflite.dart'; import 'remote_settings.dart'; import 'auth_client.dart'; import 'remote_client.dart'; import 'remote_repository.dart'; // === Guardia anti-concorrenza (single-flight) per la run "managed" === bool _remoteSyncRunning = false; /// Helper: retry esponenziale breve per SQLITE_BUSY. Future _withRetryBusy(Future Function() fn) async { const maxAttempts = 3; var delay = const Duration(milliseconds: 250); for (var i = 0; i < maxAttempts; i++) { try { return await fn(); } catch (e) { final msg = e.toString(); final isBusy = msg.contains('SQLITE_BUSY') || msg.contains('database is locked'); if (!isBusy || i == maxAttempts - 1) rethrow; await Future.delayed(delay); delay *= 2; // 250 → 500 → 1000 ms } } // non dovrebbe arrivare qui return await fn(); } /// Versione "managed": /// - impedisce run concorrenti /// - apre/chiude da sola la connessione a `metadata.db` (istanza indipendente) /// - imposta PRAGMA per concorrenza /// - accetta override opzionali (utile in test) Future runRemoteSyncOnceManaged({ String? baseUrl, String? indexPath, String? email, String? password, }) async { if (_remoteSyncRunning) { // ignore: avoid_print print('[remote-sync] already running, skip'); return; } _remoteSyncRunning = true; Database? db; try { final dbDir = await getDatabasesPath(); final dbPath = p.join(dbDir, 'metadata.db'); db = await openDatabase( dbPath, singleInstance: false, // connessione indipendente (non chiude l’handle di Aves) onConfigure: (db) async { try { // Alcuni PRAGMA ritornano valori → usare SEMPRE rawQuery. await db.rawQuery('PRAGMA journal_mode=WAL'); await db.rawQuery('PRAGMA synchronous=NORMAL'); await db.rawQuery('PRAGMA busy_timeout=3000'); await db.rawQuery('PRAGMA wal_autocheckpoint=1000'); await db.rawQuery('PRAGMA foreign_keys=ON'); // (Opzionale) verifica del mode corrente final jm = await db.rawQuery('PRAGMA journal_mode'); final mode = jm.isNotEmpty ? jm.first.values.first : null; // ignore: avoid_print print('[remote-sync] journal_mode=$mode'); // atteso: wal } catch (e, st) { // ignore: avoid_print print('[remote-sync][WARN] PRAGMA setup failed: $e\n$st'); // Non rilanciare: in estremo, continueremo con journaling di default } }, ); await runRemoteSyncOnce( db: db, baseUrl: baseUrl, indexPath: indexPath, email: email, password: password, ); } finally { try { await db?.close(); } catch (_) { // In caso di close doppio/già chiuso, ignoro. } _remoteSyncRunning = false; } } /// Versione "plain": /// Esegue login, scarica /photos e fa upsert nel DB usando una connessione /// SQLite **già aperta** (non viene chiusa qui). /// /// Gli optional [baseUrl], [indexPath], [email], [password] permettono override /// delle impostazioni salvate in `RemoteSettings` (comodo per test / debug). Future runRemoteSyncOnce({ required Database db, String? baseUrl, String? indexPath, String? email, String? password, }) async { try { // 1) Carica impostazioni sicure (secure storage) final s = await RemoteSettings.load(); final bUrl = (baseUrl ?? s.baseUrl).trim(); final ip = (indexPath ?? s.indexPath).trim(); final em = (email ?? s.email).trim(); final pw = (password ?? s.password); if (bUrl.isEmpty || ip.isEmpty) { throw StateError('Impostazioni remote incomplete: baseUrl/indexPath mancanti'); } // 2) Autenticazione (Bearer) final auth = RemoteAuth(baseUrl: bUrl, email: em, password: pw); await auth.login(); // Se necessario, RemoteJsonClient può riloggare su 401 // 3) Client JSON (segue anche redirect 301/302/307/308) final client = RemoteJsonClient(bUrl, ip, auth: auth); // 4) Scarica l’elenco di elementi remoti (array top-level) final items = await client.fetchAll(); // 5) Upsert nel DB (con retry se incappiamo in SQLITE_BUSY) final repo = RemoteRepository(db); await _withRetryBusy(() => repo.upsertAll(items)); // 5.b) Pulizia + indici (copre sia remoteId sia remotePath) await repo.sanitizeRemotes(); // 5.c) **Paracadute visibilità remoti**: deve restare DISABILITATO // (se lo riattivi, i remoti spariscono dalla galleria) // await db.rawUpdate('UPDATE entry SET trashed=1 WHERE origin=1 AND trashed=0;'); // 5.d) (Opzionale) CLEANUP LEGACY: elimina righe remote senza `remoteId` // – utilissimo se hai record vecchi non deduplicabili final purgedNoId = await db.rawDelete( "DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')", ); // 6) Log sintetico final count = await repo.countRemote().catchError((_) => null); // ignore: avoid_print print('[remote-sync] import completato: remoti=${count ?? 'n/a'} (base=$bUrl, index=$ip, purged(noId)=$purgedNoId)'); } catch (e, st) { // ignore: avoid_print print('[remote-sync][ERROR] $e\n$st'); rethrow; } }