import 'dart:io'; import 'dart:math'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/src/storage/cache_object.dart'; import 'package:hive/hive.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; // Implementation of a CacheInfoRepository based on Hive abstract class ImmichCacheRepository extends CacheInfoRepository { int getNumberOfCachedObjects(); int getCacheSize(); } class ImmichCacheInfoRepository extends ImmichCacheRepository { final String hiveBoxName; final String keyLookupHiveBoxName; // To circumvent some of the limitations of a non-relational key-value database, // we use two hive boxes per cache. // [cacheObjectLookupBox] maps ids to cache objects. // [keyLookupHiveBox] maps keys to ids. // The lookup of a cache object by key therefore involves two steps: // id = keyLookupHiveBox[key] // object = cacheObjectLookupBox[id] late Box> cacheObjectLookupBox; late Box keyLookupHiveBox; ImmichCacheInfoRepository(this.hiveBoxName, this.keyLookupHiveBoxName); @override Future close() async { await cacheObjectLookupBox.close(); return true; } @override Future delete(int id) async { if (cacheObjectLookupBox.containsKey(id)) { await cacheObjectLookupBox.delete(id); return 1; } return 0; } @override Future deleteAll(Iterable ids) async { int deleted = 0; for (var id in ids) { if (cacheObjectLookupBox.containsKey(id)) { deleted++; await cacheObjectLookupBox.delete(id); } } return deleted; } @override Future deleteDataFile() async { await cacheObjectLookupBox.clear(); await keyLookupHiveBox.clear(); } @override Future exists() async { return cacheObjectLookupBox.isNotEmpty && keyLookupHiveBox.isNotEmpty; } @override Future get(String key) async { if (!keyLookupHiveBox.containsKey(key)) { return null; } int id = keyLookupHiveBox.get(key)!; if (!cacheObjectLookupBox.containsKey(id)) { keyLookupHiveBox.delete(key); return null; } return _deserialize(cacheObjectLookupBox.get(id)!); } @override Future> getAllObjects() async { return cacheObjectLookupBox.values.map(_deserialize).toList(); } @override Future> getObjectsOverCapacity(int capacity) async { if (cacheObjectLookupBox.length <= capacity) { return List.empty(); } var values = cacheObjectLookupBox.values.map(_deserialize).toList(); values.sort((CacheObject a, CacheObject b) { final aTouched = a.touched ?? DateTime.fromMicrosecondsSinceEpoch(0); final bTouched = b.touched ?? DateTime.fromMicrosecondsSinceEpoch(0); return aTouched.compareTo(bTouched); }); return values.skip(capacity).take(10).toList(); } @override Future> getOldObjects(Duration maxAge) async { return cacheObjectLookupBox.values .map(_deserialize) .where((CacheObject element) { DateTime touched = element.touched ?? DateTime.fromMicrosecondsSinceEpoch(0); return touched.isBefore(DateTime.now().subtract(maxAge)); }).toList(); } @override Future insert( CacheObject cacheObject, { bool setTouchedToNow = true, }) async { int newId = keyLookupHiveBox.length == 0 ? 0 : keyLookupHiveBox.values.reduce(max) + 1; cacheObject = cacheObject.copyWith(id: newId); keyLookupHiveBox.put(cacheObject.key, newId); cacheObjectLookupBox.put(newId, cacheObject.toMap()); return cacheObject; } @override Future open() async { cacheObjectLookupBox = await Hive.openBox(hiveBoxName); keyLookupHiveBox = await Hive.openBox(keyLookupHiveBoxName); // The cache might have cleared by the operating system. // This could create inconsistencies between the file system cache and database. // To check whether the cache was cleared, a file within the cache directory // is created for each database. If the file is absent, the cache was cleared and therefore // the database has to be cleared as well. if (!await _checkAndCreateAnchorFile()) { await cacheObjectLookupBox.clear(); await keyLookupHiveBox.clear(); } return cacheObjectLookupBox.isOpen; } @override Future update( CacheObject cacheObject, { bool setTouchedToNow = true, }) async { if (cacheObject.id != null) { cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap()); return 1; } return 0; } @override Future updateOrInsert(CacheObject cacheObject) { if (cacheObject.id == null) { return insert(cacheObject); } else { return update(cacheObject); } } @override int getNumberOfCachedObjects() { return cacheObjectLookupBox.length; } @override int getCacheSize() { final cacheElementsWithSize = cacheObjectLookupBox.values.map(_deserialize).map((e) => e.length ?? 0); if (cacheElementsWithSize.isEmpty) { return 0; } return cacheElementsWithSize.reduce((value, element) => value + element); } CacheObject _deserialize(Map serData) { Map converted = {}; serData.forEach((key, value) { converted[key.toString()] = value; }); return CacheObject.fromMap(converted); } Future _checkAndCreateAnchorFile() async { final tmpDir = await getTemporaryDirectory(); final cacheFile = File(p.join(tmpDir.path, "$hiveBoxName.tmp")); if (await cacheFile.exists()) { return true; } await cacheFile.create(); return false; } }