import 'package:flutter/rendering.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/sqlite/database.dart' as sql; import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/use_case/ls_single_file.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:tuple/tuple.dart'; part 'sync_dir.g.dart'; @npLog class SyncDir { SyncDir(this._c) : assert(require(_c)); static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepoRemote) && DiContainer.has(c, DiType.sqliteDb) && DiContainer.has(c, DiType.touchManager); /// Sync local SQLite DB with remote content /// /// Return true if some of the files have changed Future call( Account account, String dirPath, { bool isRecursive = true, ValueChanged? onProgressUpdate, }) async { final dirCache = await _queryAllDirEtags(account, dirPath); final remoteRoot = await LsSingleFile(_c.withRemoteFileRepo())(account, dirPath); return await _syncDir( account, remoteRoot, dirCache, isRecursive: isRecursive, onProgressUpdate: onProgressUpdate, ); } Future _syncDir( Account account, File remoteDir, Map dirCache, { required bool isRecursive, ValueChanged? onProgressUpdate, }) async { final status = await _checkContentUpdated(account, remoteDir, dirCache); if (!status.item1) { _log.finer("[_syncDir] Dir unchanged: ${remoteDir.path}"); return false; } _log.info("[_syncDir] Dir changed: ${remoteDir.path}"); final dataSrc = FileCachedDataSource(_c, shouldCheckCache: true); final syncState = await dataSrc.beginSync(account, remoteDir, remoteTouchEtag: status.item2); final children = syncState.files; if (!isRecursive) { await dataSrc.concludeSync(syncState); return true; } final subDirs = children .where((f) => f.isCollection == true && !remoteDir.compareServerIdentity(f) && !f.path.endsWith(remote_storage_util.getRemoteStorageDir(account))) .toList(); final progress = IntProgress(subDirs.length); for (final d in subDirs) { onProgressUpdate ?.call(Progress(progress.progress, d.strippedPathWithEmpty)); try { await _syncDir( account, d, dirCache, isRecursive: isRecursive, onProgressUpdate: (value) { final merged = progress.progress + progress.step * value.progress; onProgressUpdate?.call(Progress(merged, value.text)); }, ); } catch (e, stackTrace) { _log.severe("[_syncDir] Failed while _syncDir: ${logFilename(d.path)}", e, stackTrace); } progress.next(); } await dataSrc.concludeSync(syncState); return true; } Future> _checkContentUpdated( Account account, File remoteDir, Map dirCache) async { String? touchResult; try { touchResult = await _c.touchManager.checkTouchEtag(account, remoteDir); if (touchResult == null && dirCache[remoteDir.fileId!] == remoteDir.etag!) { return const Tuple2(false, null); } } catch (e, stackTrace) { _log.severe("[_isContentUpdated] Uncaught exception", e, stackTrace); } return Tuple2(true, touchResult); } Future> _queryAllDirEtags( Account account, String dirPath) async { final dir = File(path: dirPath); return await _c.sqliteDb.use((db) async { final query = db.queryFiles().run((q) { q ..setQueryMode(sql.FilesQueryMode.expression, expressions: [db.files.fileId, db.files.etag]) ..setAppAccount(account); if (dir.strippedPathWithEmpty.isNotEmpty) { q ..byOrRelativePath(dir.strippedPathWithEmpty) ..byOrRelativePathPattern("${dir.strippedPathWithEmpty}/%"); } return q.build(); }); query.where(db.files.isCollection.equals(true)); return Map.fromEntries(await query .map( (r) => MapEntry(r.read(db.files.fileId)!, r.read(db.files.etag)!)) .get()); }); } final DiContainer _c; }