aves_mio1/lib/remote/run_remote_sync.dart
FabioMich66 507c131502
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
ok2
2026-03-07 23:53:27 +01:00

171 lines
No EOL
5.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<T> _withRetryBusy<T>(Future<T> 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<void> 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 lhandle 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<void> 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 lelenco 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;
}
}