#913 recover untracked vault items

This commit is contained in:
Thibault Deckers 2024-02-22 20:14:31 +01:00
parent a0925273bf
commit 413794cb74
9 changed files with 97 additions and 22 deletions

View file

@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- untracked binned items recovery
- untracked vault items recovery
## <a id="v1.10.4"></a>[v1.10.4] - 2024-02-07

View file

@ -30,6 +30,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
"getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) }
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
"getUntrackedTrashPaths" -> ioScope.launch { safe(call, result, ::getUntrackedTrashPaths) }
"getUntrackedVaultPaths" -> ioScope.launch { safe(call, result, ::getUntrackedVaultPaths) }
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
@ -129,13 +130,28 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun getUntrackedTrashPaths(call: MethodCall, result: MethodChannel.Result) {
val knownPaths = call.argument<List<String>>("knownPaths")
if (knownPaths == null) {
result.error("getUntrackedBinPaths-args", "missing arguments", null)
result.error("getUntrackedTrashPaths-args", "missing arguments", null)
return
}
val trashDirs = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }
val trashPaths = trashDirs.flatMap { dir -> dir.listFiles()?.map { file -> file.path } ?: listOf() }
val untrackedPaths = trashPaths.filter { !knownPaths.contains(it) }.toList()
val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.map { file -> file.path } ?: listOf() }
val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()
result.success(untrackedPaths)
}
private fun getUntrackedVaultPaths(call: MethodCall, result: MethodChannel.Result) {
val vault = call.argument<String>("vault")
val knownPaths = call.argument<List<String>>("knownPaths")
if (vault == null || knownPaths == null) {
result.error("getUntrackedVaultPaths-args", "missing arguments", null)
return
}
val vaultDir = File(StorageUtils.getVaultRoot(context), vault)
val vaultItemPaths = vaultDir.listFiles()?.map { file -> file.path } ?: listOf()
val untrackedPaths = vaultItemPaths.filterNot(knownPaths::contains).toList()
result.success(untrackedPaths)
}

View file

@ -159,7 +159,7 @@ object SafePngMetadataReader {
// Only compression method allowed by the spec is zero: deflate
if (compressionMethod.toInt() == 0) {
// bytes left for compressed text is:
// total bytes length - (profilenamebytes length + null byte + compression method byte)
// total bytes length - (profileNameBytes length + null byte + compression method byte)
val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1)
val compressedProfile = reader.getBytes(bytesLeft)
try {

View file

@ -154,7 +154,7 @@ class MediaStoreSource extends CollectionSource {
// recover untracked trash items
debugPrint('$runtimeType refresh ${stopwatch.elapsed} recover untracked entries');
if (directory == null) {
pendingNewEntries.addAll(await recoverLostTrashItems());
pendingNewEntries.addAll(await recoverUntrackedTrashItems());
}
// fetch new & modified entries

View file

@ -38,10 +38,11 @@ mixin TrashMixin on SourceBase {
return await completer.future;
}
Future<Set<AvesEntry>> recoverLostTrashItems() async {
Future<Set<AvesEntry>> recoverUntrackedTrashItems() async {
final newEntries = <AvesEntry>{};
final knownPaths = allEntries.map((v) => v.trashDetails?.path).whereNotNull().toSet();
final untrackedPaths = await storageService.getUntrackedTrashPaths(knownPaths);
final newEntries = <AvesEntry>{};
if (untrackedPaths.isNotEmpty) {
debugPrint('Recovering ${untrackedPaths.length} untracked bin items');
final recoveryPath = pContext.join(androidFileUtils.picturesPath, AndroidFileUtils.recoveryDir);
@ -75,7 +76,7 @@ mixin TrashMixin on SourceBase {
sourceEntry.trashDetails = _buildTrashDetails(id);
newEntries.add(sourceEntry);
} else {
debugPrint('Failed to recover untracked bin item at uri=$uri');
await reportService.recordError('Failed to recover untracked bin item at uri=$uri', null);
}
}
});

View file

@ -1,11 +1,15 @@
import 'dart:async';
import 'dart:io';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/origins.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/details.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves_screen_state/aves_screen_state.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
final Vaults vaults = Vaults._private();
@ -14,6 +18,8 @@ class Vaults extends ChangeNotifier {
Set<VaultDetails> _rows = {};
final Set<String> _unlockedDirPaths = {};
static const _fileScheme = 'file';
Vaults._private();
Future<void> init() async {
@ -118,7 +124,7 @@ class Vaults extends ChangeNotifier {
bool isVaultEntryUri(String uriString) {
final uri = Uri.parse(uriString);
if (uri.scheme != 'file') return false;
if (uri.scheme != _fileScheme) return false;
final path = uri.pathSegments.fold('', (prev, v) => '$prev${pContext.separator}$v');
return vaultDirectories.any(path.startsWith);
@ -132,13 +138,47 @@ class Vaults extends ChangeNotifier {
_onLockStateChanged();
}
void unlock(String dirPath) {
Future<void> unlock(BuildContext context, String dirPath) async {
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return;
// recover untracked vault items
final source = context.read<CollectionSource>();
final newEntries = await recoverUntrackedItems(source, dirPath);
if (newEntries.isNotEmpty) {
source.addEntries(newEntries);
await metadataDb.saveEntries(newEntries);
unawaited(source.analyze(null, entries: newEntries));
}
_unlockedDirPaths.add(dirPath);
_onLockStateChanged();
}
Future<Set<AvesEntry>> recoverUntrackedItems(CollectionSource source, String dirPath) async {
final newEntries = <AvesEntry>{};
final vaultName = detailsForPath(dirPath)?.name;
if (vaultName == null) return newEntries;
final knownPaths = source.allEntries.where((v) => v.origin == EntryOrigins.vault && v.directory == dirPath).map((v) => v.path).whereNotNull().toSet();
final untrackedPaths = await storageService.getUntrackedVaultPaths(vaultName, knownPaths);
if (untrackedPaths.isNotEmpty) {
debugPrint('Recovering ${untrackedPaths.length} untracked vault items');
await Future.forEach(untrackedPaths, (untrackedPath) async {
final uri = Uri.file(untrackedPath).toString();
final sourceEntry = await mediaFetchService.getEntry(uri, null);
if (sourceEntry != null) {
sourceEntry.id = metadataDb.nextId;
sourceEntry.origin = EntryOrigins.vault;
newEntries.add(sourceEntry);
} else {
await reportService.recordError('Failed to recover untracked vault item at uri=$uri', null);
}
});
}
return newEntries;
}
void _onScreenOff() => lock(all.where((v) => v.autoLockScreenOff).map((v) => v.path).toSet());
void _onLockStateChanged() {

View file

@ -14,6 +14,8 @@ abstract class StorageService {
Future<Set<String>> getUntrackedTrashPaths(Iterable<String> knownPaths);
Future<Set<String>> getUntrackedVaultPaths(String vaultName, Iterable<String> knownPaths);
Future<String> getVaultRoot();
Future<int?> getFreeSpace(StorageVolume volume);
@ -86,6 +88,20 @@ class PlatformStorageService implements StorageService {
return {};
}
@override
Future<Set<String>> getUntrackedVaultPaths(String vaultName, Iterable<String> knownPaths) async {
try {
final result = await _platform.invokeMethod('getUntrackedVaultPaths', <String, dynamic>{
'vault': vaultName,
'knownPaths': knownPaths.toList(),
});
return (result as List).cast<String>().toSet();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return {};
}
@override
Future<String> getVaultRoot() async {
try {

View file

@ -16,7 +16,7 @@ import 'package:local_auth/error_codes.dart' as auth_error;
import 'package:local_auth/local_auth.dart';
mixin VaultAwareMixin on FeedbackMixin {
Future<bool> _tryUnlock(String dirPath, BuildContext context) async {
Future<bool> _tryUnlock(BuildContext context, String dirPath) async {
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return true;
final details = vaults.detailsForPath(dirPath);
@ -67,12 +67,12 @@ mixin VaultAwareMixin on FeedbackMixin {
if (confirmed == null || !confirmed) return false;
vaults.unlock(dirPath);
await vaults.unlock(context, dirPath);
return true;
}
Future<bool> unlockAlbum(BuildContext context, String dirPath) async {
final success = await _tryUnlock(dirPath, context);
final success = await _tryUnlock(context, dirPath);
if (!success) {
showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
}

View file

@ -85,7 +85,14 @@ class _DbTabState extends State<DbTab> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('DB entry:${data == null ? ' no row' : ''}'),
if (data != null)
if (data != null) ...[
ElevatedButton(
onPressed: () async {
final source = context.read<CollectionSource>();
await source.removeEntries({entry.uri}, includeTrash: true);
},
child: const Text('Untrack entry'),
),
InfoRowGroup(
info: {
'uri': data.uri,
@ -103,6 +110,7 @@ class _DbTabState extends State<DbTab> {
'trashed': '${data.trashed}',
},
),
],
],
);
},
@ -181,13 +189,6 @@ class _DbTabState extends State<DbTab> {
},
child: const Text('Remove details'),
),
ElevatedButton(
onPressed: () async {
final source = context.read<CollectionSource>();
await source.removeEntries({entry.uri}, includeTrash: true);
},
child: const Text('Untrack entry'),
),
InfoRowGroup(
info: {
'dateMillis': '${data.dateMillis}',