mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 18:08:48 -07:00
209 lines
5.7 KiB
Dart
209 lines
5.7 KiB
Dart
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<Map<dynamic, dynamic>> cacheObjectLookupBox;
|
|
late Box<int> keyLookupHiveBox;
|
|
|
|
ImmichCacheInfoRepository(this.hiveBoxName, this.keyLookupHiveBoxName);
|
|
|
|
@override
|
|
Future<bool> close() async {
|
|
await cacheObjectLookupBox.close();
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<int> delete(int id) async {
|
|
if (cacheObjectLookupBox.containsKey(id)) {
|
|
await cacheObjectLookupBox.delete(id);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
@override
|
|
Future<int> deleteAll(Iterable<int> ids) async {
|
|
int deleted = 0;
|
|
for (var id in ids) {
|
|
if (cacheObjectLookupBox.containsKey(id)) {
|
|
deleted++;
|
|
await cacheObjectLookupBox.delete(id);
|
|
}
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteDataFile() async {
|
|
await cacheObjectLookupBox.clear();
|
|
await keyLookupHiveBox.clear();
|
|
}
|
|
|
|
@override
|
|
Future<bool> exists() async {
|
|
return cacheObjectLookupBox.isNotEmpty && keyLookupHiveBox.isNotEmpty;
|
|
}
|
|
|
|
@override
|
|
Future<CacheObject?> 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<List<CacheObject>> getAllObjects() async {
|
|
return cacheObjectLookupBox.values.map(_deserialize).toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<CacheObject>> 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<List<CacheObject>> 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<CacheObject> 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<bool> 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<int> 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<String, dynamic> converted = {};
|
|
|
|
serData.forEach((key, value) {
|
|
converted[key.toString()] = value;
|
|
});
|
|
|
|
return CacheObject.fromMap(converted);
|
|
}
|
|
|
|
Future<bool> _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;
|
|
}
|
|
}
|