Skip to content

Commit 9b6b2d2

Browse files
committed
feat: Added a cache manager for easily handling cached images.
1 parent 6f61c3c commit 9b6b2d2

File tree

5 files changed

+289
-211
lines changed

5 files changed

+289
-211
lines changed
Lines changed: 4 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
import 'dart:io';
2-
31
import 'package:flutter_riverpod/flutter_riverpod.dart';
4-
import 'package:http/http.dart' as http;
52
import 'package:open_authenticator/model/settings/entry.dart';
6-
import 'package:open_authenticator/model/totp/decrypted.dart';
7-
import 'package:open_authenticator/model/totp/repository.dart';
8-
import 'package:open_authenticator/model/totp/totp.dart';
9-
import 'package:open_authenticator/utils/utils.dart';
10-
import 'package:path/path.dart';
11-
import 'package:path_provider/path_provider.dart';
3+
import 'package:open_authenticator/model/totp/image_cache.dart';
124

135
/// The cache TOTP pictures settings entry provider.
146
final cacheTotpPicturesSettingsEntryProvider = AsyncNotifierProvider.autoDispose<CacheTotpPicturesSettingsEntry, bool>(CacheTotpPicturesSettingsEntry.new);
@@ -26,73 +18,13 @@ class CacheTotpPicturesSettingsEntry extends SettingsEntry<bool> {
2618
Future<void> changeValue(bool value) async {
2719
if (value != state.valueOrNull) {
2820
state = const AsyncLoading();
21+
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
2922
if (value) {
30-
TotpList totps = await ref.read(totpRepositoryProvider.future);
31-
for (Totp totp in totps) {
32-
await totp.cacheImage();
33-
}
23+
totpImageCacheManager.fillCache();
3424
} else {
35-
Directory cache = await TotpImageCache._getTotpImagesDirectory();
36-
if (await cache.exists()) {
37-
await cache.delete(recursive: true);
38-
}
25+
totpImageCacheManager.clearCache();
3926
}
4027
}
4128
await super.changeValue(value);
4229
}
4330
}
44-
45-
/// Contains various methods for caching TOTP images.
46-
extension TotpImageCache on Totp {
47-
/// Caches the TOTP image.
48-
Future<void> cacheImage({String? previousImageUrl}) async {
49-
try {
50-
if (!isDecrypted) {
51-
return;
52-
}
53-
String? imageUrl = (this as DecryptedTotp).imageUrl;
54-
if (imageUrl == null) {
55-
File file = await getTotpCachedImage(uuid);
56-
if (await file.exists()) {
57-
await file.delete();
58-
}
59-
} else {
60-
previousImageUrl ??= imageUrl;
61-
File file = await getTotpCachedImage(uuid, createDirectory: true);
62-
if (previousImageUrl == imageUrl && file.existsSync()) {
63-
return;
64-
}
65-
http.Response response = await http.get(Uri.parse(imageUrl));
66-
await file.writeAsBytes(response.bodyBytes);
67-
}
68-
}
69-
catch (ex, stacktrace) {
70-
handleException(ex, stacktrace);
71-
}
72-
}
73-
74-
/// Deletes the cached image, if possible.
75-
Future<void> deleteCachedImage() async => (await getTotpCachedImage(uuid)).deleteIfExists();
76-
77-
/// Returns the TOTP cached image file.
78-
static Future<File> getTotpCachedImage(String uuid, {bool createDirectory = false}) async => File(join((await _getTotpImagesDirectory(create: createDirectory)).path, uuid));
79-
80-
/// Returns the totp images directory, creating it if doesn't exist yet.
81-
static Future<Directory> _getTotpImagesDirectory({bool create = false}) async {
82-
Directory directory = Directory(join((await getApplicationCacheDirectory()).path, 'totps_images'));
83-
if (create && !directory.existsSync()) {
84-
directory.createSync(recursive: true);
85-
}
86-
return directory;
87-
}
88-
}
89-
90-
/// Allows to easily delete a file without checking if it exists.
91-
extension DeleteIfExists on File {
92-
/// Deletes the current file if it exists.
93-
Future<void> deleteIfExists() async {
94-
if (await exists()) {
95-
await delete();
96-
}
97-
}
98-
}

lib/model/totp/image_cache.dart

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:io';
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_riverpod/flutter_riverpod.dart';
7+
import 'package:http/http.dart' as http;
8+
import 'package:open_authenticator/model/settings/cache_totp_pictures.dart';
9+
import 'package:open_authenticator/model/totp/decrypted.dart';
10+
import 'package:open_authenticator/model/totp/repository.dart';
11+
import 'package:open_authenticator/model/totp/totp.dart';
12+
import 'package:open_authenticator/utils/utils.dart';
13+
import 'package:path/path.dart';
14+
import 'package:path_provider/path_provider.dart';
15+
16+
/// The TOTP image cache manager provider.
17+
final totpImageCacheManagerProvider = AsyncNotifierProvider.autoDispose<TotpImageCacheManager, Map<String, String>>(TotpImageCacheManager.new);
18+
19+
/// Manages the cache of TOTPs images.
20+
class TotpImageCacheManager extends AutoDisposeAsyncNotifier<Map<String, String>> {
21+
@override
22+
FutureOr<Map<String, String>> build() async {
23+
File index = await _getIndexFile();
24+
return index.existsSync() ? jsonDecode(index.readAsStringSync()).cast<String, String>() : {};
25+
}
26+
27+
/// Caches the TOTP image.
28+
Future<void> cacheImage(Totp totp, {bool checkSettings = true}) async {
29+
try {
30+
if (!totp.isDecrypted) {
31+
return;
32+
}
33+
if (checkSettings) {
34+
bool cacheEnabled = await ref.read(cacheTotpPicturesSettingsEntryProvider.future);
35+
if (!cacheEnabled) {
36+
return;
37+
}
38+
}
39+
String? imageUrl = (totp as DecryptedTotp).imageUrl;
40+
if (imageUrl == null) {
41+
await deleteCachedImage(totp.uuid);
42+
} else {
43+
Map<String, String> cached = Map.from(await future);
44+
String? previousImageUrl = cached[totp.uuid];
45+
File file = await _getTotpCachedImageFile(totp.uuid, createDirectory: true);
46+
if (previousImageUrl == imageUrl && file.existsSync()) {
47+
return;
48+
}
49+
http.Response response = await http.get(Uri.parse(imageUrl));
50+
await file.writeAsBytes(response.bodyBytes);
51+
cached[totp.uuid] = imageUrl;
52+
state = AsyncData(cached);
53+
imageCache.clear();
54+
_saveIndex(content: cached);
55+
}
56+
} catch (ex, stacktrace) {
57+
handleException(ex, stacktrace);
58+
}
59+
}
60+
61+
/// Returns the cached image that corresponds to the TOTP UUID and current image URL.
62+
static Future<File?> getCachedImage(Map<String, String> cached, String uuid, String? imageUrl) async {
63+
if (!cached.containsKey(uuid)) {
64+
return null;
65+
}
66+
String? cachedImageUrl = cached[uuid];
67+
if (cachedImageUrl != imageUrl) {
68+
return null;
69+
}
70+
return _getTotpCachedImageFile(uuid);
71+
}
72+
73+
/// Deletes the cached image, if possible.
74+
Future<void> deleteCachedImage(String uuid) async {
75+
Map<String, String> cached = Map.from(await future);
76+
File file = await _getTotpCachedImageFile(uuid);
77+
await file.deleteIfExists();
78+
cached.remove(uuid);
79+
state = AsyncData(cached);
80+
_saveIndex(content: cached);
81+
}
82+
83+
/// Fills the cache with all TOTPs that can be read from the TOTP repository.
84+
Future<void> fillCache() async {
85+
TotpList totps = await ref.read(totpRepositoryProvider.future);
86+
for (Totp totp in totps) {
87+
await cacheImage(totp);
88+
}
89+
}
90+
91+
/// Clears the cache.
92+
Future<void> clearCache() async {
93+
Directory directory = await _getTotpImagesDirectory();
94+
if (directory.existsSync()) {
95+
directory.deleteSync(recursive: true);
96+
}
97+
state = const AsyncData({});
98+
}
99+
100+
/// Returns the cache index.
101+
Future<File> _getIndexFile() async => File(join((await _getTotpImagesDirectory()).path, 'index.json'));
102+
103+
/// Saves the content to the index.
104+
Future<void> _saveIndex({Map<String, String>? content}) async {
105+
content ??= await future;
106+
(await _getIndexFile()).writeAsStringSync(jsonEncode(content));
107+
}
108+
109+
/// Returns the TOTP cached image file.
110+
static Future<File> _getTotpCachedImageFile(String uuid, {bool createDirectory = false}) async => File(join((await _getTotpImagesDirectory(create: createDirectory)).path, uuid));
111+
112+
/// Returns the totp images directory, creating it if doesn't exist yet.
113+
static Future<Directory> _getTotpImagesDirectory({bool create = false}) async {
114+
Directory directory = Directory(join((await getApplicationCacheDirectory()).path, 'totps_images'));
115+
if (create && !directory.existsSync()) {
116+
directory.createSync(recursive: true);
117+
}
118+
return directory;
119+
}
120+
}
121+
122+
/// Allows to easily delete a file without checking if it exists.
123+
extension _DeleteIfExists on File {
124+
/// Deletes the current file if it exists.
125+
Future<void> deleteIfExists() async {
126+
if (await exists()) {
127+
await delete();
128+
}
129+
}
130+
}

lib/model/totp/repository.dart

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import 'package:open_authenticator/app.dart';
66
import 'package:open_authenticator/model/backup.dart';
77
import 'package:open_authenticator/model/crypto.dart';
88
import 'package:open_authenticator/model/purchases/contributor_plan.dart';
9-
import 'package:open_authenticator/model/settings/cache_totp_pictures.dart';
109
import 'package:open_authenticator/model/settings/storage_type.dart';
1110
import 'package:open_authenticator/model/storage/storage.dart';
1211
import 'package:open_authenticator/model/storage/type.dart';
1312
import 'package:open_authenticator/model/totp/decrypted.dart';
1413
import 'package:open_authenticator/model/totp/deleted_totps.dart';
14+
import 'package:open_authenticator/model/totp/image_cache.dart';
1515
import 'package:open_authenticator/model/totp/totp.dart';
1616
import 'package:open_authenticator/utils/result.dart';
1717
import 'package:open_authenticator/utils/utils.dart';
@@ -72,9 +72,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
7272
/// Queries TOTPs (and decrypt them) from storage.
7373
Future<TotpList> _queryTotpsFromStorage(Storage storage, CryptoStore? cryptoStore) async {
7474
List<Totp> totps = await storage.listTotps();
75-
for (Totp totp in totps) {
76-
totp.cacheImage();
77-
}
75+
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
76+
totpImageCacheManager.fillCache();
7877
return TotpList._fromListAndStorage(
7978
list: await totps.decrypt(cryptoStore),
8079
storage: storage,
@@ -88,9 +87,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
8887
await totpList.waitBeforeNextOperation();
8988
Storage storage = await ref.read(storageProvider.future);
9089
await storage.addTotp(totp);
91-
if (await ref.read(cacheTotpPicturesSettingsEntryProvider.future)) {
92-
totp.cacheImage();
93-
}
90+
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
91+
totpImageCacheManager.cacheImage(totp);
9492
CryptoStore? cryptoStore = await ref.read(cryptoStoreProvider.future);
9593
state = AsyncData(
9694
TotpList._fromListAndStorage(
@@ -116,16 +114,9 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
116114
await storage.replaceTotps(totps);
117115
CryptoStore? cryptoStore = await ref.read(cryptoStoreProvider.future);
118116
List<Totp> decrypted = await totps.decrypt(cryptoStore);
119-
if (await ref.read(cacheTotpPicturesSettingsEntryProvider.future)) {
120-
TotpList totpList = await future;
121-
Map<String, String> previousImages = {
122-
for (Totp currentTotp in totpList)
123-
if (currentTotp.isDecrypted && (currentTotp as DecryptedTotp).imageUrl != null)
124-
currentTotp.uuid: currentTotp.imageUrl!,
125-
};
126-
for (Totp updatedTotp in decrypted) {
127-
await updatedTotp.cacheImage(previousImageUrl: previousImages[updatedTotp.uuid]);
128-
}
117+
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
118+
for (Totp updatedTotp in decrypted) {
119+
await totpImageCacheManager.cacheImage(updatedTotp);
129120
}
130121

131122
state = AsyncData(
@@ -150,10 +141,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
150141
await totpList.waitBeforeNextOperation();
151142
Storage storage = await ref.read(storageProvider.future);
152143
await storage.updateTotp(uuid, totp);
153-
if (await ref.read(cacheTotpPicturesSettingsEntryProvider.future)) {
154-
DecryptedTotp? current = totpList._list.firstWhereOrNull((currentTotp) => currentTotp.uuid == totp.uuid && currentTotp.isDecrypted) as DecryptedTotp?;
155-
await totp.cacheImage(previousImageUrl: current?.imageUrl);
156-
}
144+
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
145+
await totpImageCacheManager.cacheImage(totp);
157146
state = AsyncData(
158147
TotpList._fromListAndStorage(
159148
list: _mergeToCurrentList(totpList, totp: totp),
@@ -177,7 +166,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
177166
Storage storage = await ref.read(storageProvider.future);
178167
await storage.deleteTotp(uuid);
179168
await ref.read(deletedTotpsProvider).markDeleted(uuid);
180-
(await TotpImageCache.getTotpCachedImage(uuid)).deleteIfExists();
169+
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
170+
totpImageCacheManager.deleteCachedImage(uuid);
181171
state = AsyncData(
182172
TotpList._fromListAndStorage(
183173
list: totpList._list..removeWhere((totp) => totp.uuid == uuid),

0 commit comments

Comments
 (0)