immich_cache_info_repository.dart 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import 'dart:io';
  2. import 'dart:math';
  3. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  4. import 'package:flutter_cache_manager/src/storage/cache_object.dart';
  5. import 'package:hive/hive.dart';
  6. import 'package:path/path.dart' as p;
  7. import 'package:path_provider/path_provider.dart';
  8. // Implementation of a CacheInfoRepository based on Hive
  9. abstract class ImmichCacheRepository extends CacheInfoRepository {
  10. int getNumberOfCachedObjects();
  11. int getCacheSize();
  12. }
  13. class ImmichCacheInfoRepository extends ImmichCacheRepository {
  14. final String hiveBoxName;
  15. final String keyLookupHiveBoxName;
  16. // To circumvent some of the limitations of a non-relational key-value database,
  17. // we use two hive boxes per cache.
  18. // [cacheObjectLookupBox] maps ids to cache objects.
  19. // [keyLookupHiveBox] maps keys to ids.
  20. // The lookup of a cache object by key therefore involves two steps:
  21. // id = keyLookupHiveBox[key]
  22. // object = cacheObjectLookupBox[id]
  23. late Box<Map<dynamic, dynamic>> cacheObjectLookupBox;
  24. late Box<int> keyLookupHiveBox;
  25. ImmichCacheInfoRepository(this.hiveBoxName, this.keyLookupHiveBoxName);
  26. @override
  27. Future<bool> close() async {
  28. await cacheObjectLookupBox.close();
  29. return true;
  30. }
  31. @override
  32. Future<int> delete(int id) async {
  33. if (cacheObjectLookupBox.containsKey(id)) {
  34. await cacheObjectLookupBox.delete(id);
  35. return 1;
  36. }
  37. return 0;
  38. }
  39. @override
  40. Future<int> deleteAll(Iterable<int> ids) async {
  41. int deleted = 0;
  42. for (var id in ids) {
  43. if (cacheObjectLookupBox.containsKey(id)) {
  44. deleted++;
  45. await cacheObjectLookupBox.delete(id);
  46. }
  47. }
  48. return deleted;
  49. }
  50. @override
  51. Future<void> deleteDataFile() async {
  52. await cacheObjectLookupBox.clear();
  53. await keyLookupHiveBox.clear();
  54. }
  55. @override
  56. Future<bool> exists() async {
  57. return cacheObjectLookupBox.isNotEmpty && keyLookupHiveBox.isNotEmpty;
  58. }
  59. @override
  60. Future<CacheObject?> get(String key) async {
  61. if (!keyLookupHiveBox.containsKey(key)) {
  62. return null;
  63. }
  64. int id = keyLookupHiveBox.get(key)!;
  65. if (!cacheObjectLookupBox.containsKey(id)) {
  66. keyLookupHiveBox.delete(key);
  67. return null;
  68. }
  69. return _deserialize(cacheObjectLookupBox.get(id)!);
  70. }
  71. @override
  72. Future<List<CacheObject>> getAllObjects() async {
  73. return cacheObjectLookupBox.values.map(_deserialize).toList();
  74. }
  75. @override
  76. Future<List<CacheObject>> getObjectsOverCapacity(int capacity) async {
  77. if (cacheObjectLookupBox.length <= capacity) {
  78. return List.empty();
  79. }
  80. var values = cacheObjectLookupBox.values.map(_deserialize).toList();
  81. values.sort((CacheObject a, CacheObject b) {
  82. final aTouched = a.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
  83. final bTouched = b.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
  84. return aTouched.compareTo(bTouched);
  85. });
  86. return values.skip(capacity).take(10).toList();
  87. }
  88. @override
  89. Future<List<CacheObject>> getOldObjects(Duration maxAge) async {
  90. return cacheObjectLookupBox.values
  91. .map(_deserialize)
  92. .where((CacheObject element) {
  93. DateTime touched =
  94. element.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
  95. return touched.isBefore(DateTime.now().subtract(maxAge));
  96. }).toList();
  97. }
  98. @override
  99. Future<CacheObject> insert(
  100. CacheObject cacheObject, {
  101. bool setTouchedToNow = true,
  102. }) async {
  103. int newId = keyLookupHiveBox.length == 0
  104. ? 0
  105. : keyLookupHiveBox.values.reduce(max) + 1;
  106. cacheObject = cacheObject.copyWith(id: newId);
  107. keyLookupHiveBox.put(cacheObject.key, newId);
  108. cacheObjectLookupBox.put(newId, cacheObject.toMap());
  109. return cacheObject;
  110. }
  111. @override
  112. Future<bool> open() async {
  113. cacheObjectLookupBox = await Hive.openBox(hiveBoxName);
  114. keyLookupHiveBox = await Hive.openBox(keyLookupHiveBoxName);
  115. // The cache might have cleared by the operating system.
  116. // This could create inconsistencies between the file system cache and database.
  117. // To check whether the cache was cleared, a file within the cache directory
  118. // is created for each database. If the file is absent, the cache was cleared and therefore
  119. // the database has to be cleared as well.
  120. if (!await _checkAndCreateAnchorFile()) {
  121. await cacheObjectLookupBox.clear();
  122. await keyLookupHiveBox.clear();
  123. }
  124. return cacheObjectLookupBox.isOpen;
  125. }
  126. @override
  127. Future<int> update(
  128. CacheObject cacheObject, {
  129. bool setTouchedToNow = true,
  130. }) async {
  131. if (cacheObject.id != null) {
  132. cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap());
  133. return 1;
  134. }
  135. return 0;
  136. }
  137. @override
  138. Future updateOrInsert(CacheObject cacheObject) {
  139. if (cacheObject.id == null) {
  140. return insert(cacheObject);
  141. } else {
  142. return update(cacheObject);
  143. }
  144. }
  145. @override
  146. int getNumberOfCachedObjects() {
  147. return cacheObjectLookupBox.length;
  148. }
  149. @override
  150. int getCacheSize() {
  151. final cacheElementsWithSize =
  152. cacheObjectLookupBox.values.map(_deserialize).map((e) => e.length ?? 0);
  153. if (cacheElementsWithSize.isEmpty) {
  154. return 0;
  155. }
  156. return cacheElementsWithSize.reduce((value, element) => value + element);
  157. }
  158. CacheObject _deserialize(Map serData) {
  159. Map<String, dynamic> converted = {};
  160. serData.forEach((key, value) {
  161. converted[key.toString()] = value;
  162. });
  163. return CacheObject.fromMap(converted);
  164. }
  165. Future<bool> _checkAndCreateAnchorFile() async {
  166. final tmpDir = await getTemporaryDirectory();
  167. final cacheFile = File(p.join(tmpDir.path, "$hiveBoxName.tmp"));
  168. if (await cacheFile.exists()) {
  169. return true;
  170. }
  171. await cacheFile.create();
  172. return false;
  173. }
  174. }