171 lines
No EOL
5.8 KiB
Dart
171 lines
No EOL
5.8 KiB
Dart
// 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 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<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 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;
|
||
}
|
||
} |