From 12ee23252f18c80ebb63c2ce6e4ffcd6fab3b05a Mon Sep 17 00:00:00 2001 From: luckyrat Date: Tue, 19 Mar 2024 12:23:22 +0000 Subject: [PATCH] Allow user to delete staged update file if corrupt --- lib/cubit/vault_cubit.dart | 8 +++ lib/local_vault_repository.dart | 22 +++++++ lib/widgets/help.dart | 104 ++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) diff --git a/lib/cubit/vault_cubit.dart b/lib/cubit/vault_cubit.dart index 1cae684..60f4460 100644 --- a/lib/cubit/vault_cubit.dart +++ b/lib/cubit/vault_cubit.dart @@ -262,6 +262,14 @@ class VaultCubit extends Cubit { l.d('vault cubit started'); } + Future hasPendingUpdateFile(User user) async { + return await _localVaultRepo.hasStagedUpdate(user); + } + + Future deletePendingUpdateFile(User user) async { + return await _localVaultRepo.removeStagedUpdate(user); + } + Future emitVaultLoaded(LocalVaultFile vault, User? user, {bool immediateRemoteRefresh = true, required bool safe}) async { if (user?.subscriptionStatus == AccountSubscriptionStatus.expired || diff --git a/lib/local_vault_repository.dart b/lib/local_vault_repository.dart index 4077c0b..7f44281 100644 --- a/lib/local_vault_repository.dart +++ b/lib/local_vault_repository.dart @@ -240,6 +240,28 @@ class LocalVaultRepository { ); } + Future removeStagedUpdate(User user) async { + final directory = await getStorageDirectory(); + final stagedFile = File('${directory.path}/${user.idB64url}/staged.kdbx'); + try { + await stagedFile.delete(); + } on Exception { + // can fail if OS/hardware failure has caused the file to be already deleted but that's OK + } + } + + Future hasStagedUpdate(User user) async { + final directory = await getStorageDirectory(); + final stagedFile = File('${directory.path}/${user.idB64url}/staged.kdbx'); + try { + final exists = await stagedFile.exists(); + return exists; + } on Exception { + // maybe can fail but that's OK, just assume does not exist since are unlikely to be able to do anything with it anyway + } + return false; + } + remove(User user) async { final directory = await getStorageDirectory(); final file = File('${directory.path}/${user.idB64url}/current.kdbx'); diff --git a/lib/widgets/help.dart b/lib/widgets/help.dart index adf0f26..74026f2 100644 --- a/lib/widgets/help.dart +++ b/lib/widgets/help.dart @@ -1,16 +1,20 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:keevault/widgets/bottom.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../config/app.dart'; import '../config/routes.dart'; +import '../cubit/account_cubit.dart'; +import '../cubit/vault_cubit.dart'; import '../generated/l10n.dart'; import 'coloured_safe_area_widget.dart'; import 'dialog_utils.dart'; +import '../logging/logger.dart'; class HelpWidget extends StatefulWidget { const HelpWidget({ @@ -124,6 +128,7 @@ class _HelpWidgetState extends State with TraceableClientMixin { child: Text('Share / view logs'), ), ), + PendingUpdateErrorRecoveryWidget(theme: theme), ], ), ), @@ -134,3 +139,102 @@ class _HelpWidgetState extends State with TraceableClientMixin { ); } } + +class PendingUpdateErrorRecoveryWidget extends StatefulWidget { + const PendingUpdateErrorRecoveryWidget({ + super.key, + required this.theme, + }); + + final ThemeData theme; + + @override + State createState() => _PendingUpdateErrorRecoveryWidgetState(); +} + +class _PendingUpdateErrorRecoveryWidgetState extends State { + bool? _errorAndPendingUpdateKdbxExists; + + @override + void initState() { + super.initState(); + unawaited(_detectPendingUpdateKdbx()); + } + + Future _detectPendingUpdateKdbx() async { + final accountCubit = BlocProvider.of(context); + final currentUser = accountCubit.currentUserIfKnown; + // Only bother if user is signed in account holder, not a free local only user + // and if we might care whether the file exists or not + final vaultCubit = BlocProvider.of(context); + if (vaultCubit.state is! VaultError || accountCubit.state is AccountLocalOnly || currentUser == null) { + setState(() { + _errorAndPendingUpdateKdbxExists = false; + }); + return; + } + final pendingUpdateKdbxExists = await vaultCubit.hasPendingUpdateFile(currentUser); + setState(() { + _errorAndPendingUpdateKdbxExists = pendingUpdateKdbxExists; + }); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final vc = BlocProvider.of(context); + final ac = BlocProvider.of(context); + final currentUser = ac.currentUserIfKnown; + return Visibility( + visible: _errorAndPendingUpdateKdbxExists ?? false, + child: Column( + children: [ + Divider(), + Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 16.0), + child: Text( + 'Error recovery', + style: widget.theme.textTheme.titleLarge, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + 'There is a version of your Kee Vault pending merge with your current local version. This is a rare but expected behaviour in some cases of a failed download from the internet but once any fault with your device has been resolved, Kee Vault should automatically complete the merge when you sign-in to your Kee Vault.'), + ), + Text( + 'If you are experiencing an error that prevents the opening of your Kee Vault because this downloaded data is in some way corrupt or incompatible with your Kee Vault on this device, the button below may help. If the underlying fault with the previous download attempt has not been resolved, the error may re-appear and in very rare circumstances you may lose data (e.g. during certain offline modifications across multiple devices).'), + Text( + 'Ensure you understand the cause of your error situation and the implications of clicking the button - we recommend discussing the problem in the community forum before taking any action.'), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: () async { + final proceed = await DialogUtils.showConfirmDialog( + context: context, + params: ConfirmDialogParams( + content: + 'If you delete this data we will attempt to re-download the latest version shortly after you next sign-in. After deleting this data, you must use your task manager to kill Kee Vault and then start it again (or restart your device if you are unsure how to do this). Ensure that you have resolved the underlying fault before proceeding (e.g. made more disk space available on your device or repaired the hardware fault on the device) and that you have a good network connection. Are you sure you want to delete the data?', + negativeButtonText: 'Keep', + positiveButtonText: 'Delete', + title: 'Delete version of your Kee Vault data that is pending merge', + )); + if (proceed) { + l.w('deletePendingUpdateFile'); + await vc.deletePendingUpdateFile(currentUser!); + await _detectPendingUpdateKdbx(); + } + l.i('deletePendingUpdateFile skipped by user'); + }, + style: ElevatedButton.styleFrom(backgroundColor: widget.theme.buttonTheme.colorScheme!.error), + child: Text('Delete pending Kee Vault data'), + ), + ), + ], + ), + ); + }, + ); + } +}