mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 10:28:50 +01:00
Refactor: extract cache ops
This commit is contained in:
parent
78ab3a5793
commit
90004b8623
2 changed files with 306 additions and 277 deletions
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
|
@ -9,13 +8,12 @@ import 'package:nc_photos/api/api.dart';
|
|||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/file_cache_manager.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/webdav_response_parser.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/touch_token_manager.dart';
|
||||
import 'package:nc_photos/use_case/compat/v32.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
|
@ -331,30 +329,10 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
}
|
||||
|
||||
/// Remove a file/dir from database
|
||||
///
|
||||
/// If [f] is a dir, the dir and its sub-dirs will be removed from dirStore.
|
||||
/// The files inside any of these dirs will be removed from file2Store.
|
||||
///
|
||||
/// If [f] is a file, the file will be removed from file2Store, but no changes
|
||||
/// to dirStore.
|
||||
@override
|
||||
remove(Account account, File f) async {
|
||||
remove(Account account, File f) {
|
||||
_log.info("[remove] ${f.path}");
|
||||
await appDb.use((db) async {
|
||||
if (f.isCollection == true) {
|
||||
final transaction = db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await _removeDirFromAppDb(account, f,
|
||||
dirStore: dirStore, fileStore: fileStore);
|
||||
} else {
|
||||
final transaction =
|
||||
db.transaction(AppDb.file2StoreName, idbModeReadWrite);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await _removeFileFromAppDb(account, f, fileStore: fileStore);
|
||||
}
|
||||
});
|
||||
return FileCacheRemover(appDb)(account, f);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -435,37 +413,33 @@ class FileCachedDataSource implements FileDataSource {
|
|||
|
||||
@override
|
||||
list(Account account, File dir) async {
|
||||
final cacheManager = _CacheManager(
|
||||
final cacheLoader = FileCacheLoader(
|
||||
appDb: appDb,
|
||||
appDbSrc: _appDbSrc,
|
||||
remoteSrc: _remoteSrc,
|
||||
shouldCheckCache: shouldCheckCache,
|
||||
forwardCacheManager: forwardCacheManager,
|
||||
);
|
||||
final cache = await cacheManager.list(account, dir);
|
||||
if (cacheManager.isGood) {
|
||||
final cache = await cacheLoader(account, dir);
|
||||
if (cacheLoader.isGood) {
|
||||
return cache!;
|
||||
}
|
||||
|
||||
// no cache or outdated
|
||||
try {
|
||||
final remote = await _remoteSrc.list(account, dir);
|
||||
await _cacheResult(account, dir, remote);
|
||||
await FileCacheUpdater(appDb)(account, dir, remote: remote, cache: cache);
|
||||
if (shouldCheckCache) {
|
||||
// update our local touch token to match the remote one
|
||||
const tokenManager = TouchTokenManager();
|
||||
try {
|
||||
await tokenManager.setLocalToken(
|
||||
account, dir, cacheManager.remoteTouchToken);
|
||||
account, dir, cacheLoader.remoteTouchToken);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout("[list] Failed while setLocalToken", e, stacktrace);
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
if (cache != null) {
|
||||
await _cleanUpCacheWithRemote(account, remote, cache);
|
||||
}
|
||||
return remote;
|
||||
} on ApiException catch (e) {
|
||||
if (e.response.statusCode == 404) {
|
||||
|
@ -564,52 +538,6 @@ class FileCachedDataSource implements FileDataSource {
|
|||
await _remoteSrc.createDir(account, path);
|
||||
}
|
||||
|
||||
Future<void> _cacheResult(Account account, File f, List<File> result) {
|
||||
return appDb.use((db) async {
|
||||
final transaction = db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await _cacheListResults(account, f, result,
|
||||
fileStore: fileStore, dirStore: dirStore);
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove extra entries from local cache based on remote results
|
||||
Future<void> _cleanUpCacheWithRemote(
|
||||
Account account, List<File> remote, List<File> cache) async {
|
||||
final removed = cache
|
||||
.where((c) => !remote.any((r) => r.compareServerIdentity(c)))
|
||||
.toList();
|
||||
if (removed.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_log.info(
|
||||
"[_cleanUpCacheWithRemote] Removed: ${removed.map((f) => f.path).toReadableString()}");
|
||||
|
||||
await appDb.use((db) async {
|
||||
final transaction = db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
for (final f in removed) {
|
||||
try {
|
||||
if (f.isCollection == true) {
|
||||
await _removeDirFromAppDb(account, f,
|
||||
dirStore: dirStore, fileStore: fileStore);
|
||||
} else {
|
||||
await _removeFileFromAppDb(account, f, fileStore: fileStore);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_cleanUpCacheWithRemote] Failed while removing file: ${logFilename(f.path)}",
|
||||
e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final bool shouldCheckCache;
|
||||
final FileForwardCacheManager? forwardCacheManager;
|
||||
|
@ -714,204 +642,7 @@ class FileForwardCacheManager {
|
|||
static final _log = Logger("entity.file.data_source.FileForwardCacheManager");
|
||||
}
|
||||
|
||||
class _CacheManager {
|
||||
_CacheManager({
|
||||
required this.appDb,
|
||||
required this.appDbSrc,
|
||||
required this.remoteSrc,
|
||||
this.shouldCheckCache = false,
|
||||
this.forwardCacheManager,
|
||||
});
|
||||
|
||||
/// Return the cached results of listing a directory [dir]
|
||||
///
|
||||
/// Should check [isGood] before using the cache returning by this method
|
||||
Future<List<File>?> list(Account account, File dir) async {
|
||||
List<File>? cache;
|
||||
try {
|
||||
if (forwardCacheManager != null) {
|
||||
cache = await forwardCacheManager!.list(account, dir);
|
||||
} else {
|
||||
cache = await appDbSrc.list(account, dir);
|
||||
}
|
||||
// compare the cached root
|
||||
final cacheEtag =
|
||||
cache.firstWhere((f) => f.compareServerIdentity(dir)).etag!;
|
||||
// compare the etag to see if the content has been updated
|
||||
var remoteEtag = dir.etag;
|
||||
// if no etag supplied, we need to query it form remote
|
||||
remoteEtag ??= (await remoteSrc.list(account, dir, depth: 0)).first.etag;
|
||||
if (cacheEtag == remoteEtag) {
|
||||
_log.fine(
|
||||
"[list] etag matched for ${AppDbDirEntry.toPrimaryKeyForDir(account, dir)}");
|
||||
if (shouldCheckCache) {
|
||||
await _checkTouchToken(account, dir, cache);
|
||||
} else {
|
||||
_isGood = true;
|
||||
}
|
||||
} else {
|
||||
_log.info("[list] Remote content updated for ${dir.path}");
|
||||
}
|
||||
} on CacheNotFoundException catch (_) {
|
||||
// normal when there's no cache
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[list] Cache failure", e, stackTrace);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
bool get isGood => _isGood;
|
||||
String? get remoteTouchToken => _remoteToken;
|
||||
|
||||
Future<void> _checkTouchToken(
|
||||
Account account, File f, List<File> cache) async {
|
||||
final touchPath =
|
||||
"${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}";
|
||||
final fileRepo = FileRepo(FileCachedDataSource(appDb));
|
||||
const tokenManager = TouchTokenManager();
|
||||
String? remoteToken;
|
||||
try {
|
||||
remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_checkTouchToken] Failed getting remote token at '$touchPath'",
|
||||
e,
|
||||
stacktrace);
|
||||
}
|
||||
_remoteToken = remoteToken;
|
||||
|
||||
String? localToken;
|
||||
try {
|
||||
localToken = await tokenManager.getLocalToken(account, f);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_checkTouchToken] Failed getting local token at '$touchPath'",
|
||||
e,
|
||||
stacktrace);
|
||||
}
|
||||
|
||||
if (localToken != remoteToken) {
|
||||
_log.info(
|
||||
"[_checkTouchToken] Remote and local token differ, cache outdated");
|
||||
} else {
|
||||
_isGood = true;
|
||||
}
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final FileWebdavDataSource remoteSrc;
|
||||
final FileAppDbDataSource appDbSrc;
|
||||
final bool shouldCheckCache;
|
||||
final FileForwardCacheManager? forwardCacheManager;
|
||||
|
||||
var _isGood = false;
|
||||
String? _remoteToken;
|
||||
|
||||
static final _log = Logger("entity.file.data_source._CacheManager");
|
||||
}
|
||||
|
||||
Future<void> _cacheListResults(
|
||||
Account account,
|
||||
File dir,
|
||||
List<File> results, {
|
||||
required ObjectStore fileStore,
|
||||
required ObjectStore dirStore,
|
||||
}) async {
|
||||
// add files to db
|
||||
await Future.wait(results.map((f) => fileStore.put(
|
||||
AppDbFile2Entry.fromFile(account, f).toJson(),
|
||||
AppDbFile2Entry.toPrimaryKeyForFile(account, f))));
|
||||
|
||||
// results from remote also contain the dir itself
|
||||
final resultGroup = results.groupListsBy((f) => f.compareServerIdentity(dir));
|
||||
final remoteDir = resultGroup[true]!.first;
|
||||
final remoteChildren = resultGroup[false] ?? [];
|
||||
// add dir to db
|
||||
await dirStore.put(
|
||||
AppDbDirEntry.fromFiles(account, remoteDir, remoteChildren).toJson(),
|
||||
AppDbDirEntry.toPrimaryKeyForDir(account, remoteDir));
|
||||
}
|
||||
|
||||
Future<void> _removeFileFromAppDb(
|
||||
Account account,
|
||||
File file, {
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
assert(file.isCollection != true);
|
||||
try {
|
||||
await fileStore.delete(AppDbFile2Entry.toPrimaryKeyForFile(account, file));
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeFileFromAppDb] Failed removing fileStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a dir and all files inside from the database
|
||||
Future<void> _removeDirFromAppDb(
|
||||
Account account,
|
||||
File dir, {
|
||||
required ObjectStore dirStore,
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
assert(dir.isCollection == true);
|
||||
// delete the dir itself
|
||||
try {
|
||||
await AppDbDirEntry.toPrimaryKeyForDir(account, dir).runFuture((key) async {
|
||||
_log.fine("[_removeDirFromAppDb] Removing dirStore entry: $key");
|
||||
await dirStore.delete(key);
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_removeDirFromAppDb] Failed removing dirStore entry", e, stackTrace);
|
||||
}
|
||||
// then its children
|
||||
final childrenRange = KeyRange.bound(
|
||||
AppDbDirEntry.toPrimaryLowerKeyForSubDirs(account, dir),
|
||||
AppDbDirEntry.toPrimaryUpperKeyForSubDirs(account, dir),
|
||||
);
|
||||
for (final key in await dirStore.getAllKeys(childrenRange)) {
|
||||
_log.fine("[_removeDirFromAppDb] Removing dirStore entry: $key");
|
||||
try {
|
||||
await dirStore.delete(key);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeDirFromAppDb] Failed removing dirStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// delete files from fileStore
|
||||
// first the dir
|
||||
try {
|
||||
await AppDbFile2Entry.toPrimaryKeyForFile(account, dir)
|
||||
.runFuture((key) async {
|
||||
_log.fine("[_removeDirFromAppDb] Removing fileStore entry: $key");
|
||||
await fileStore.delete(key);
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_removeDirFromAppDb] Failed removing fileStore entry", e, stackTrace);
|
||||
}
|
||||
// then files under this dir and sub-dirs
|
||||
final range = KeyRange.bound(
|
||||
AppDbFile2Entry.toStrippedPathIndexLowerKeyForDir(account, dir),
|
||||
AppDbFile2Entry.toStrippedPathIndexUpperKeyForDir(account, dir),
|
||||
);
|
||||
final strippedPathIndex =
|
||||
fileStore.index(AppDbFile2Entry.strippedPathIndexName);
|
||||
for (final key in await strippedPathIndex.getAllKeys(range)) {
|
||||
_log.fine("[_removeDirFromAppDb] Removing fileStore entry: $key");
|
||||
try {
|
||||
await fileStore.delete(key);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeDirFromAppDb] Failed removing fileStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _validateFile(File f) {
|
||||
// See: https://gitlab.com/nkming2/nc-photos/-/issues/9
|
||||
return f.lastModified != null;
|
||||
}
|
||||
|
||||
final _log = Logger("entity.file.data_source");
|
||||
|
|
298
lib/entity/file/file_cache_manager.dart
Normal file
298
lib/entity/file/file_cache_manager.dart
Normal file
|
@ -0,0 +1,298 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/touch_token_manager.dart';
|
||||
|
||||
class FileCacheLoader {
|
||||
FileCacheLoader({
|
||||
required this.appDb,
|
||||
required this.appDbSrc,
|
||||
required this.remoteSrc,
|
||||
this.shouldCheckCache = false,
|
||||
this.forwardCacheManager,
|
||||
});
|
||||
|
||||
/// Return the cached results of listing a directory [dir]
|
||||
///
|
||||
/// Should check [isGood] before using the cache returning by this method
|
||||
Future<List<File>?> call(Account account, File dir) async {
|
||||
List<File>? cache;
|
||||
try {
|
||||
if (forwardCacheManager != null) {
|
||||
cache = await forwardCacheManager!.list(account, dir);
|
||||
} else {
|
||||
cache = await appDbSrc.list(account, dir);
|
||||
}
|
||||
// compare the cached root
|
||||
final cacheEtag =
|
||||
cache.firstWhere((f) => f.compareServerIdentity(dir)).etag!;
|
||||
// compare the etag to see if the content has been updated
|
||||
var remoteEtag = dir.etag;
|
||||
// if no etag supplied, we need to query it form remote
|
||||
remoteEtag ??= (await remoteSrc.list(account, dir, depth: 0)).first.etag;
|
||||
if (cacheEtag == remoteEtag) {
|
||||
_log.fine(
|
||||
"[list] etag matched for ${AppDbDirEntry.toPrimaryKeyForDir(account, dir)}");
|
||||
if (shouldCheckCache) {
|
||||
await _checkTouchToken(account, dir, cache);
|
||||
} else {
|
||||
_isGood = true;
|
||||
}
|
||||
} else {
|
||||
_log.info("[call] Remote content updated for ${dir.path}");
|
||||
}
|
||||
} on CacheNotFoundException catch (_) {
|
||||
// normal when there's no cache
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[call] Cache failure", e, stackTrace);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
bool get isGood => _isGood;
|
||||
String? get remoteTouchToken => _remoteToken;
|
||||
|
||||
Future<void> _checkTouchToken(
|
||||
Account account, File f, List<File> cache) async {
|
||||
final touchPath =
|
||||
"${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}";
|
||||
final fileRepo = FileRepo(FileCachedDataSource(appDb));
|
||||
const tokenManager = TouchTokenManager();
|
||||
String? remoteToken;
|
||||
try {
|
||||
remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_checkTouchToken] Failed getting remote token at '$touchPath'",
|
||||
e,
|
||||
stacktrace);
|
||||
}
|
||||
_remoteToken = remoteToken;
|
||||
|
||||
String? localToken;
|
||||
try {
|
||||
localToken = await tokenManager.getLocalToken(account, f);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_checkTouchToken] Failed getting local token at '$touchPath'",
|
||||
e,
|
||||
stacktrace);
|
||||
}
|
||||
|
||||
if (localToken != remoteToken) {
|
||||
_log.info(
|
||||
"[_checkTouchToken] Remote and local token differ, cache outdated");
|
||||
} else {
|
||||
_isGood = true;
|
||||
}
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
final FileWebdavDataSource remoteSrc;
|
||||
final FileAppDbDataSource appDbSrc;
|
||||
final bool shouldCheckCache;
|
||||
final FileForwardCacheManager? forwardCacheManager;
|
||||
|
||||
var _isGood = false;
|
||||
String? _remoteToken;
|
||||
|
||||
static final _log = Logger("entity.file.file_cache_manager.FileCacheLoader");
|
||||
}
|
||||
|
||||
class FileCacheUpdater {
|
||||
const FileCacheUpdater(this.appDb);
|
||||
|
||||
Future<void> call(
|
||||
Account account,
|
||||
File dir, {
|
||||
required List<File> remote,
|
||||
List<File>? cache,
|
||||
}) async {
|
||||
await _cacheRemote(account, dir, remote);
|
||||
if (cache != null) {
|
||||
await _cleanUpCache(account, remote, cache);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cacheRemote(Account account, File dir, List<File> remote) {
|
||||
return appDb.use((db) async {
|
||||
final transaction = db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
|
||||
// add files to db
|
||||
await Future.wait(remote.map((f) => fileStore.put(
|
||||
AppDbFile2Entry.fromFile(account, f).toJson(),
|
||||
AppDbFile2Entry.toPrimaryKeyForFile(account, f))));
|
||||
|
||||
// results from remote also contain the dir itself
|
||||
final resultGroup =
|
||||
remote.groupListsBy((f) => f.compareServerIdentity(dir));
|
||||
final remoteDir = resultGroup[true]!.first;
|
||||
final remoteChildren = resultGroup[false] ?? [];
|
||||
// add dir to db
|
||||
await dirStore.put(
|
||||
AppDbDirEntry.fromFiles(account, remoteDir, remoteChildren).toJson(),
|
||||
AppDbDirEntry.toPrimaryKeyForDir(account, remoteDir));
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove extra entries from local cache based on remote contents
|
||||
Future<void> _cleanUpCache(
|
||||
Account account, List<File> remote, List<File> cache) async {
|
||||
final removed = cache
|
||||
.where((c) => !remote.any((r) => r.compareServerIdentity(c)))
|
||||
.toList();
|
||||
if (removed.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_log.info(
|
||||
"[_cleanUpCache] Removed: ${removed.map((f) => f.path).toReadableString()}");
|
||||
|
||||
await appDb.use((db) async {
|
||||
final transaction = db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
for (final f in removed) {
|
||||
try {
|
||||
if (f.isCollection == true) {
|
||||
await _removeDirFromAppDb(account, f,
|
||||
dirStore: dirStore, fileStore: fileStore);
|
||||
} else {
|
||||
await _removeFileFromAppDb(account, f, fileStore: fileStore);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_cleanUpCache] Failed while removing file: ${logFilename(f.path)}",
|
||||
e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
|
||||
static final _log = Logger("entity.file.file_cache_manager.FileCacheUpdater");
|
||||
}
|
||||
|
||||
class FileCacheRemover {
|
||||
const FileCacheRemover(this.appDb);
|
||||
|
||||
/// Remove a file/dir from cache
|
||||
///
|
||||
/// If [f] is a dir, the dir and its sub-dirs will be removed from dirStore.
|
||||
/// The files inside any of these dirs will be removed from file2Store.
|
||||
///
|
||||
/// If [f] is a file, the file will be removed from file2Store, but no changes
|
||||
/// to dirStore.
|
||||
Future<void> call(Account account, File f) async {
|
||||
await appDb.use((db) async {
|
||||
if (f.isCollection == true) {
|
||||
final transaction = db.transaction(
|
||||
[AppDb.dirStoreName, AppDb.file2StoreName], idbModeReadWrite);
|
||||
final dirStore = transaction.objectStore(AppDb.dirStoreName);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await _removeDirFromAppDb(account, f,
|
||||
dirStore: dirStore, fileStore: fileStore);
|
||||
} else {
|
||||
final transaction =
|
||||
db.transaction(AppDb.file2StoreName, idbModeReadWrite);
|
||||
final fileStore = transaction.objectStore(AppDb.file2StoreName);
|
||||
await _removeFileFromAppDb(account, f, fileStore: fileStore);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final AppDb appDb;
|
||||
}
|
||||
|
||||
Future<void> _removeFileFromAppDb(
|
||||
Account account,
|
||||
File file, {
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
assert(file.isCollection != true);
|
||||
try {
|
||||
await fileStore.delete(AppDbFile2Entry.toPrimaryKeyForFile(account, file));
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeFileFromAppDb] Failed removing fileStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a dir and all files inside from the database
|
||||
Future<void> _removeDirFromAppDb(
|
||||
Account account,
|
||||
File dir, {
|
||||
required ObjectStore dirStore,
|
||||
required ObjectStore fileStore,
|
||||
}) async {
|
||||
assert(dir.isCollection == true);
|
||||
// delete the dir itself
|
||||
try {
|
||||
await AppDbDirEntry.toPrimaryKeyForDir(account, dir).runFuture((key) async {
|
||||
_log.fine("[_removeDirFromAppDb] Removing dirStore entry: $key");
|
||||
await dirStore.delete(key);
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_removeDirFromAppDb] Failed removing dirStore entry", e, stackTrace);
|
||||
}
|
||||
// then its children
|
||||
final childrenRange = KeyRange.bound(
|
||||
AppDbDirEntry.toPrimaryLowerKeyForSubDirs(account, dir),
|
||||
AppDbDirEntry.toPrimaryUpperKeyForSubDirs(account, dir),
|
||||
);
|
||||
for (final key in await dirStore.getAllKeys(childrenRange)) {
|
||||
_log.fine("[_removeDirFromAppDb] Removing dirStore entry: $key");
|
||||
try {
|
||||
await dirStore.delete(key);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeDirFromAppDb] Failed removing dirStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// delete files from fileStore
|
||||
// first the dir
|
||||
try {
|
||||
await AppDbFile2Entry.toPrimaryKeyForFile(account, dir)
|
||||
.runFuture((key) async {
|
||||
_log.fine("[_removeDirFromAppDb] Removing fileStore entry: $key");
|
||||
await fileStore.delete(key);
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_removeDirFromAppDb] Failed removing fileStore entry", e, stackTrace);
|
||||
}
|
||||
// then files under this dir and sub-dirs
|
||||
final range = KeyRange.bound(
|
||||
AppDbFile2Entry.toStrippedPathIndexLowerKeyForDir(account, dir),
|
||||
AppDbFile2Entry.toStrippedPathIndexUpperKeyForDir(account, dir),
|
||||
);
|
||||
final strippedPathIndex =
|
||||
fileStore.index(AppDbFile2Entry.strippedPathIndexName);
|
||||
for (final key in await strippedPathIndex.getAllKeys(range)) {
|
||||
_log.fine("[_removeDirFromAppDb] Removing fileStore entry: $key");
|
||||
try {
|
||||
await fileStore.delete(key);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_removeDirFromAppDb] Failed removing fileStore entry", e,
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _log = Logger("entity.file.file_cache_manager");
|
Loading…
Reference in a new issue