aves_mio1/lib/remote/run_remote_sync.dart.old
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

194 lines
6.5 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) Impedisci futuri duplicati e ripulisci quelli già presenti
await repo.ensureUniqueRemoteId();
final removed = await repo.deduplicateRemotes();
// 5.c) Paracadute: assicura che i remoti NON siano mostrati nella Collection Aves
//await db.rawUpdate('UPDATE entry SET trashed=1 WHERE origin=1 AND trashed=0;');
// 5.d) **CLEANUP LEGACY**: elimina righe remote "orfane" o doppioni su remotePath
// - Righe senza remoteId (NULL o vuoto): non deduplicabili via UNIQUE → vanno rimosse
final purgedNoId = await db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
);
// - Doppioni per remotePath: tieni solo la riga con id MAX
// (copre i casi in cui in passato siano state create due righe per lo stesso path)
final purgedByPath = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
// ignore: avoid_print
print('[remote-sync] cleanup: removed dup(remoteId)=$removed, purged(noId)=$purgedNoId, purged(byPath)=$purgedByPath');
// 6) Log sintetico
int? c;
try {
c = await repo.countRemote();
} catch (_) {
c = null;
}
// ignore: avoid_print
if (c == null) {
print('[remote-sync] import completato (conteggio non disponibile)');
} else {
print('[remote-sync] importati remoti: $c (base=$bUrl, index=$ip)');
}
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][ERROR] $e\n$st');
rethrow;
}
}