import 'dart:convert'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/exception.dart'; import 'package:nc_photos/mobile/platform.dart' if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/use_case/get_file_binary.dart'; import 'package:nc_photos/use_case/ls_single_file.dart'; import 'package:nc_photos/use_case/put_file_binary.dart'; import 'package:nc_photos/use_case/remove.dart'; /// Manage touch token for files /// /// Touch tokens are used to broadcast file changes that don't trigger an ETag /// update to other devices. Such changes include custom properties like /// metadata. In order to detect these hidden changes, you should get both /// local and remote tokens and compare them. Beware that getting the remote /// token requires downloading a file from the server so you may want to avoid /// doing it on every query class TouchTokenManager { TouchTokenManager(this._c) : assert(require(_c)); static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepo) && DiContainer.has(c, DiType.fileRepoRemote); Future getRemoteRootEtag(Account account) async { try { // we use the remote repo here to prevent it caching the result final touchDir = await LsSingleFile(_c.withRemoteFileRepo())( account, remote_storage_util.getRemoteTouchDir(account)); return touchDir.etag!; } catch (_) { // dir not found on server return null; } } Future setLocalRootEtag(Account account, String? etag) async { if (etag == null) { await AccountPref.of(account).removeTouchRootEtag(); } else { await AccountPref.of(account).setTouchRootEtag(etag); } } Future getLocalRootEtag(Account account) async { return AccountPref.of(account).getTouchRootEtag(); } Future setRemoteToken(Account account, File file, String? token) async { _log.info( "[setRemoteToken] Set remote token for file '${file.path}': $token"); final path = _getRemotePath(account, file); if (token == null) { return Remove(_c)(account, [file], shouldCleanUp: false); } else { return PutFileBinary(_c.fileRepo)( account, path, const Utf8Encoder().convert(token), shouldCreateMissingDir: true); } } /// Return the touch token for [file] from remote source, or null if no such /// file Future getRemoteToken(Account account, File file) async { final path = _getRemotePath(account, file); try { final content = await GetFileBinary(_c.fileRepo)(account, File(path: path)); return const Utf8Decoder().convert(content); } on ApiException catch (e) { if (e.response.statusCode == 404) { return null; } else { rethrow; } } } Future setLocalToken(Account account, File file, String? token) { _log.info( "[setLocalToken] Set local token for file '${file.path}': $token"); final name = _getLocalStorageName(account, file); if (token == null) { return platform.UniversalStorage().remove(name); } else { return platform.UniversalStorage().putString(name, token); } } Future getLocalToken(Account account, File file) async { final name = _getLocalStorageName(account, file); return platform.UniversalStorage().getString(name); } String _getRemotePath(Account account, File file) { final strippedPath = file.strippedPath; if (strippedPath == ".") { return "${remote_storage_util.getRemoteTouchDir(account)}/token.txt"; } else { return "${remote_storage_util.getRemoteTouchDir(account)}/${file.strippedPath}/token.txt"; } } String _getLocalStorageName(Account account, File file) { final strippedPath = file.strippedPath; if (strippedPath == ".") { return "touch/${account.url.replaceFirst('://', '_')}/${account.userId}/token"; } else { return "touch/${account.url.replaceFirst('://', '_')}/${account.userId}/${file.strippedPath}/token"; } } final DiContainer _c; static final _log = Logger("touch_token_manager.TouchTokenManager"); }