mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			211 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
// ignore_for_file: depend_on_referenced_packages, implementation_imports
 | 
						|
 | 
						|
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;
 | 
						|
  }
 | 
						|
}
 |