Skip to content

Commit

Permalink
feat: Added a cache manager for easily handling cached images.
Browse files Browse the repository at this point in the history
  • Loading branch information
Skyost committed Jul 11, 2024
1 parent 6f61c3c commit 9b6b2d2
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 211 deletions.
76 changes: 4 additions & 72 deletions lib/model/settings/cache_totp_pictures.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:open_authenticator/model/settings/entry.dart';
import 'package:open_authenticator/model/totp/decrypted.dart';
import 'package:open_authenticator/model/totp/repository.dart';
import 'package:open_authenticator/model/totp/totp.dart';
import 'package:open_authenticator/utils/utils.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:open_authenticator/model/totp/image_cache.dart';

/// The cache TOTP pictures settings entry provider.
final cacheTotpPicturesSettingsEntryProvider = AsyncNotifierProvider.autoDispose<CacheTotpPicturesSettingsEntry, bool>(CacheTotpPicturesSettingsEntry.new);
Expand All @@ -26,73 +18,13 @@ class CacheTotpPicturesSettingsEntry extends SettingsEntry<bool> {
Future<void> changeValue(bool value) async {
if (value != state.valueOrNull) {
state = const AsyncLoading();
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
if (value) {
TotpList totps = await ref.read(totpRepositoryProvider.future);
for (Totp totp in totps) {
await totp.cacheImage();
}
totpImageCacheManager.fillCache();
} else {
Directory cache = await TotpImageCache._getTotpImagesDirectory();
if (await cache.exists()) {
await cache.delete(recursive: true);
}
totpImageCacheManager.clearCache();
}
}
await super.changeValue(value);
}
}

/// Contains various methods for caching TOTP images.
extension TotpImageCache on Totp {
/// Caches the TOTP image.
Future<void> cacheImage({String? previousImageUrl}) async {
try {
if (!isDecrypted) {
return;
}
String? imageUrl = (this as DecryptedTotp).imageUrl;
if (imageUrl == null) {
File file = await getTotpCachedImage(uuid);
if (await file.exists()) {
await file.delete();
}
} else {
previousImageUrl ??= imageUrl;
File file = await getTotpCachedImage(uuid, createDirectory: true);
if (previousImageUrl == imageUrl && file.existsSync()) {
return;
}
http.Response response = await http.get(Uri.parse(imageUrl));
await file.writeAsBytes(response.bodyBytes);
}
}
catch (ex, stacktrace) {
handleException(ex, stacktrace);
}
}

/// Deletes the cached image, if possible.
Future<void> deleteCachedImage() async => (await getTotpCachedImage(uuid)).deleteIfExists();

/// Returns the TOTP cached image file.
static Future<File> getTotpCachedImage(String uuid, {bool createDirectory = false}) async => File(join((await _getTotpImagesDirectory(create: createDirectory)).path, uuid));

/// Returns the totp images directory, creating it if doesn't exist yet.
static Future<Directory> _getTotpImagesDirectory({bool create = false}) async {
Directory directory = Directory(join((await getApplicationCacheDirectory()).path, 'totps_images'));
if (create && !directory.existsSync()) {
directory.createSync(recursive: true);
}
return directory;
}
}

/// Allows to easily delete a file without checking if it exists.
extension DeleteIfExists on File {
/// Deletes the current file if it exists.
Future<void> deleteIfExists() async {
if (await exists()) {
await delete();
}
}
}
130 changes: 130 additions & 0 deletions lib/model/totp/image_cache.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:open_authenticator/model/settings/cache_totp_pictures.dart';
import 'package:open_authenticator/model/totp/decrypted.dart';
import 'package:open_authenticator/model/totp/repository.dart';
import 'package:open_authenticator/model/totp/totp.dart';
import 'package:open_authenticator/utils/utils.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

/// The TOTP image cache manager provider.
final totpImageCacheManagerProvider = AsyncNotifierProvider.autoDispose<TotpImageCacheManager, Map<String, String>>(TotpImageCacheManager.new);

/// Manages the cache of TOTPs images.
class TotpImageCacheManager extends AutoDisposeAsyncNotifier<Map<String, String>> {
@override
FutureOr<Map<String, String>> build() async {
File index = await _getIndexFile();
return index.existsSync() ? jsonDecode(index.readAsStringSync()).cast<String, String>() : {};
}

/// Caches the TOTP image.
Future<void> cacheImage(Totp totp, {bool checkSettings = true}) async {
try {
if (!totp.isDecrypted) {
return;
}
if (checkSettings) {
bool cacheEnabled = await ref.read(cacheTotpPicturesSettingsEntryProvider.future);
if (!cacheEnabled) {
return;
}
}
String? imageUrl = (totp as DecryptedTotp).imageUrl;
if (imageUrl == null) {
await deleteCachedImage(totp.uuid);
} else {
Map<String, String> cached = Map.from(await future);
String? previousImageUrl = cached[totp.uuid];
File file = await _getTotpCachedImageFile(totp.uuid, createDirectory: true);
if (previousImageUrl == imageUrl && file.existsSync()) {
return;
}
http.Response response = await http.get(Uri.parse(imageUrl));
await file.writeAsBytes(response.bodyBytes);
cached[totp.uuid] = imageUrl;
state = AsyncData(cached);
imageCache.clear();
_saveIndex(content: cached);
}
} catch (ex, stacktrace) {
handleException(ex, stacktrace);
}
}

/// Returns the cached image that corresponds to the TOTP UUID and current image URL.
static Future<File?> getCachedImage(Map<String, String> cached, String uuid, String? imageUrl) async {
if (!cached.containsKey(uuid)) {
return null;
}
String? cachedImageUrl = cached[uuid];
if (cachedImageUrl != imageUrl) {
return null;
}
return _getTotpCachedImageFile(uuid);
}

/// Deletes the cached image, if possible.
Future<void> deleteCachedImage(String uuid) async {
Map<String, String> cached = Map.from(await future);
File file = await _getTotpCachedImageFile(uuid);
await file.deleteIfExists();
cached.remove(uuid);
state = AsyncData(cached);
_saveIndex(content: cached);
}

/// Fills the cache with all TOTPs that can be read from the TOTP repository.
Future<void> fillCache() async {
TotpList totps = await ref.read(totpRepositoryProvider.future);
for (Totp totp in totps) {
await cacheImage(totp);
}
}

/// Clears the cache.
Future<void> clearCache() async {
Directory directory = await _getTotpImagesDirectory();
if (directory.existsSync()) {
directory.deleteSync(recursive: true);
}
state = const AsyncData({});
}

/// Returns the cache index.
Future<File> _getIndexFile() async => File(join((await _getTotpImagesDirectory()).path, 'index.json'));

/// Saves the content to the index.
Future<void> _saveIndex({Map<String, String>? content}) async {
content ??= await future;
(await _getIndexFile()).writeAsStringSync(jsonEncode(content));
}

/// Returns the TOTP cached image file.
static Future<File> _getTotpCachedImageFile(String uuid, {bool createDirectory = false}) async => File(join((await _getTotpImagesDirectory(create: createDirectory)).path, uuid));

/// Returns the totp images directory, creating it if doesn't exist yet.
static Future<Directory> _getTotpImagesDirectory({bool create = false}) async {
Directory directory = Directory(join((await getApplicationCacheDirectory()).path, 'totps_images'));
if (create && !directory.existsSync()) {
directory.createSync(recursive: true);
}
return directory;
}
}

/// Allows to easily delete a file without checking if it exists.
extension _DeleteIfExists on File {
/// Deletes the current file if it exists.
Future<void> deleteIfExists() async {
if (await exists()) {
await delete();
}
}
}
34 changes: 12 additions & 22 deletions lib/model/totp/repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import 'package:open_authenticator/app.dart';
import 'package:open_authenticator/model/backup.dart';
import 'package:open_authenticator/model/crypto.dart';
import 'package:open_authenticator/model/purchases/contributor_plan.dart';
import 'package:open_authenticator/model/settings/cache_totp_pictures.dart';
import 'package:open_authenticator/model/settings/storage_type.dart';
import 'package:open_authenticator/model/storage/storage.dart';
import 'package:open_authenticator/model/storage/type.dart';
import 'package:open_authenticator/model/totp/decrypted.dart';
import 'package:open_authenticator/model/totp/deleted_totps.dart';
import 'package:open_authenticator/model/totp/image_cache.dart';
import 'package:open_authenticator/model/totp/totp.dart';
import 'package:open_authenticator/utils/result.dart';
import 'package:open_authenticator/utils/utils.dart';
Expand Down Expand Up @@ -72,9 +72,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
/// Queries TOTPs (and decrypt them) from storage.
Future<TotpList> _queryTotpsFromStorage(Storage storage, CryptoStore? cryptoStore) async {
List<Totp> totps = await storage.listTotps();
for (Totp totp in totps) {
totp.cacheImage();
}
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
totpImageCacheManager.fillCache();
return TotpList._fromListAndStorage(
list: await totps.decrypt(cryptoStore),
storage: storage,
Expand All @@ -88,9 +87,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
await totpList.waitBeforeNextOperation();
Storage storage = await ref.read(storageProvider.future);
await storage.addTotp(totp);
if (await ref.read(cacheTotpPicturesSettingsEntryProvider.future)) {
totp.cacheImage();
}
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
totpImageCacheManager.cacheImage(totp);
CryptoStore? cryptoStore = await ref.read(cryptoStoreProvider.future);
state = AsyncData(
TotpList._fromListAndStorage(
Expand All @@ -116,16 +114,9 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
await storage.replaceTotps(totps);
CryptoStore? cryptoStore = await ref.read(cryptoStoreProvider.future);
List<Totp> decrypted = await totps.decrypt(cryptoStore);
if (await ref.read(cacheTotpPicturesSettingsEntryProvider.future)) {
TotpList totpList = await future;
Map<String, String> previousImages = {
for (Totp currentTotp in totpList)
if (currentTotp.isDecrypted && (currentTotp as DecryptedTotp).imageUrl != null)
currentTotp.uuid: currentTotp.imageUrl!,
};
for (Totp updatedTotp in decrypted) {
await updatedTotp.cacheImage(previousImageUrl: previousImages[updatedTotp.uuid]);
}
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
for (Totp updatedTotp in decrypted) {
await totpImageCacheManager.cacheImage(updatedTotp);
}

state = AsyncData(
Expand All @@ -150,10 +141,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
await totpList.waitBeforeNextOperation();
Storage storage = await ref.read(storageProvider.future);
await storage.updateTotp(uuid, totp);
if (await ref.read(cacheTotpPicturesSettingsEntryProvider.future)) {
DecryptedTotp? current = totpList._list.firstWhereOrNull((currentTotp) => currentTotp.uuid == totp.uuid && currentTotp.isDecrypted) as DecryptedTotp?;
await totp.cacheImage(previousImageUrl: current?.imageUrl);
}
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
await totpImageCacheManager.cacheImage(totp);
state = AsyncData(
TotpList._fromListAndStorage(
list: _mergeToCurrentList(totpList, totp: totp),
Expand All @@ -177,7 +166,8 @@ class TotpRepository extends AutoDisposeAsyncNotifier<TotpList> {
Storage storage = await ref.read(storageProvider.future);
await storage.deleteTotp(uuid);
await ref.read(deletedTotpsProvider).markDeleted(uuid);
(await TotpImageCache.getTotpCachedImage(uuid)).deleteIfExists();
TotpImageCacheManager totpImageCacheManager = ref.read(totpImageCacheManagerProvider.notifier);
totpImageCacheManager.deleteCachedImage(uuid);
state = AsyncData(
TotpList._fromListAndStorage(
list: totpList._list..removeWhere((totp) => totp.uuid == uuid),
Expand Down
Loading

0 comments on commit 9b6b2d2

Please sign in to comment.