ente/lib/utils/delete_file_util.dart

472 lines
15 KiB
Dart
Raw Normal View History

// @dart=2.9
import 'dart:async';
import 'dart:io' as io;
2021-06-28 12:54:57 +00:00
import 'dart:io';
import 'dart:math';
import 'package:device_info/device_info.dart';
2021-06-28 15:52:21 +00:00
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart';
2021-10-12 20:25:23 +00:00
import 'package:photos/models/trash_item_request.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/services/sync_service.dart';
2021-10-12 20:25:23 +00:00
import 'package:photos/services/trash_sync_service.dart';
import 'package:photos/ui/common/dialogs.dart';
import 'package:photos/ui/common/linear_progress_dialog.dart';
import 'package:photos/utils/dialog_util.dart';
2021-10-30 05:51:23 +00:00
import 'package:photos/utils/file_util.dart';
import 'package:photos/utils/toast_util.dart';
final _logger = Logger("DeleteFileUtil");
Future<void> deleteFilesFromEverywhere(
2022-06-11 08:23:52 +00:00
BuildContext context,
List<File> files,
) async {
2022-05-30 11:05:08 +00:00
final dialog = createProgressDialog(context, "Deleting...");
await dialog.show();
2022-09-12 11:04:48 +00:00
_logger.info("Trying to deleteFilesFromEverywhere " + files.toString());
final List<String> localAssetIDs = [];
final List<String> localSharedMediaIDs = [];
final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
bool hasLocalOnlyFiles = false;
for (final file in files) {
2021-09-15 20:50:13 +00:00
if (file.localID != null) {
if (!(await _localFileExist(file))) {
2021-06-13 20:09:06 +00:00
_logger.warning("Already deleted " + file.toString());
alreadyDeletedIDs.add(file.localID);
2022-09-23 01:48:25 +00:00
} else if (file.isSharedMediaToAppSandbox) {
localSharedMediaIDs.add(file.localID);
} else {
localAssetIDs.add(file.localID);
}
}
if (file.uploadedFileID == null) {
hasLocalOnlyFiles = true;
}
}
if (hasLocalOnlyFiles && Platform.isAndroid) {
final shouldProceed = await shouldProceedWithDeletion(context);
if (!shouldProceed) {
await dialog.hide();
return;
}
}
2021-09-15 20:44:52 +00:00
Set<String> deletedIDs = <String>{};
try {
2021-09-15 20:50:13 +00:00
deletedIDs =
(await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
} catch (e, s) {
_logger.severe("Could not delete file", e, s);
}
deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
2021-09-15 20:44:52 +00:00
final updatedCollectionIDs = <int>{};
2021-10-12 20:25:23 +00:00
final List<TrashRequest> uploadedFilesToBeTrashed = [];
final List<File> deletedFiles = [];
for (final file in files) {
if (file.localID != null) {
// Remove only those files that have already been removed from disk
if (deletedIDs.contains(file.localID) ||
alreadyDeletedIDs.contains(file.localID)) {
deletedFiles.add(file);
2021-10-21 12:08:12 +00:00
if (file.uploadedFileID != null) {
2021-10-26 13:32:33 +00:00
uploadedFilesToBeTrashed
.add(TrashRequest(file.uploadedFileID, file.collectionID));
updatedCollectionIDs.add(file.collectionID);
} else {
await FilesDB.instance.deleteLocalFile(file);
}
}
} else {
updatedCollectionIDs.add(file.collectionID);
deletedFiles.add(file);
2021-10-26 13:32:33 +00:00
uploadedFilesToBeTrashed
.add(TrashRequest(file.uploadedFileID, file.collectionID));
}
}
2021-10-12 20:25:23 +00:00
if (uploadedFilesToBeTrashed.isNotEmpty) {
try {
2021-10-26 13:32:33 +00:00
final fileIDs =
uploadedFilesToBeTrashed.map((item) => item.fileID).toList();
await TrashSyncService.instance
.trashFilesOnServer(uploadedFilesToBeTrashed);
2021-10-12 20:25:23 +00:00
// await SyncService.instance
// .deleteFilesOnServer(fileIDs);
2021-10-26 13:32:33 +00:00
await FilesDB.instance.deleteMultipleUploadedFiles(fileIDs);
} catch (e) {
_logger.severe(e);
await dialog.hide();
showGenericErrorDialog(context);
2021-09-15 20:44:52 +00:00
rethrow;
}
for (final collectionID in updatedCollectionIDs) {
2022-06-11 08:23:52 +00:00
Bus.instance.fire(
CollectionUpdatedEvent(
collectionID,
deletedFiles
.where((file) => file.collectionID == collectionID)
.toList(),
type: EventType.deletedFromEverywhere,
),
);
}
}
if (deletedFiles.isNotEmpty) {
2022-06-11 08:23:52 +00:00
Bus.instance.fire(
LocalPhotosUpdatedEvent(
deletedFiles,
type: EventType.deletedFromEverywhere,
),
);
if (hasLocalOnlyFiles && Platform.isAndroid) {
2022-06-10 14:29:56 +00:00
showShortToast(context, "Files deleted");
} else {
2022-06-10 14:29:56 +00:00
showShortToast(context, "Moved to trash");
}
}
await dialog.hide();
2021-10-12 20:25:23 +00:00
if (uploadedFilesToBeTrashed.isNotEmpty) {
RemoteSyncService.instance.sync(silently: true);
}
}
2021-09-15 20:50:13 +00:00
Future<void> deleteFilesFromRemoteOnly(
2022-06-11 08:23:52 +00:00
BuildContext context,
List<File> files,
) async {
files.removeWhere((element) => element.uploadedFileID == null);
2021-10-26 13:32:33 +00:00
if (files.isEmpty) {
2022-06-10 14:29:56 +00:00
showToast(context, "Selected files are not on ente");
return;
}
2022-05-30 11:05:08 +00:00
final dialog = createProgressDialog(context, "Deleting...");
2021-09-15 20:50:13 +00:00
await dialog.show();
2022-06-11 08:23:52 +00:00
_logger.info(
2022-09-12 11:04:48 +00:00
"Trying to deleteFilesFromRemoteOnly " +
files.map((f) => f.uploadedFileID).toString(),
2022-06-11 08:23:52 +00:00
);
2021-09-15 20:50:13 +00:00
final updatedCollectionIDs = <int>{};
final List<int> uploadedFileIDs = [];
final List<TrashRequest> trashRequests = [];
2021-09-15 20:50:13 +00:00
for (final file in files) {
updatedCollectionIDs.add(file.collectionID);
uploadedFileIDs.add(file.uploadedFileID);
trashRequests.add(TrashRequest(file.uploadedFileID, file.collectionID));
2021-09-15 20:50:13 +00:00
}
try {
await TrashSyncService.instance.trashFilesOnServer(trashRequests);
await FilesDB.instance.deleteMultipleUploadedFiles(uploadedFileIDs);
2021-09-23 07:03:13 +00:00
} catch (e, s) {
_logger.severe("Failed to delete files from remote", e, s);
2021-09-15 20:50:13 +00:00
await dialog.hide();
showGenericErrorDialog(context);
rethrow;
}
for (final collectionID in updatedCollectionIDs) {
2022-06-11 08:23:52 +00:00
Bus.instance.fire(
CollectionUpdatedEvent(
collectionID,
files.where((file) => file.collectionID == collectionID).toList(),
type: EventType.deletedFromRemote,
),
);
2021-09-15 20:50:13 +00:00
}
Bus.instance
.fire(LocalPhotosUpdatedEvent(files, type: EventType.deletedFromRemote));
SyncService.instance.sync();
2021-09-15 20:50:13 +00:00
await dialog.hide();
RemoteSyncService.instance.sync(silently: true);
}
Future<void> deleteFilesOnDeviceOnly(
2022-06-11 08:23:52 +00:00
BuildContext context,
List<File> files,
) async {
2022-05-30 11:05:08 +00:00
final dialog = createProgressDialog(context, "Deleting...");
await dialog.show();
2022-09-12 11:04:48 +00:00
_logger.info("Trying to deleteFilesOnDeviceOnly" + files.toString());
final List<String> localAssetIDs = [];
final List<String> localSharedMediaIDs = [];
final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
bool hasLocalOnlyFiles = false;
for (final file in files) {
if (file.localID != null) {
if (!(await _localFileExist(file))) {
2021-06-13 20:09:06 +00:00
_logger.warning("Already deleted " + file.toString());
alreadyDeletedIDs.add(file.localID);
2022-09-23 01:48:25 +00:00
} else if (file.isSharedMediaToAppSandbox) {
localSharedMediaIDs.add(file.localID);
} else {
localAssetIDs.add(file.localID);
}
}
if (file.uploadedFileID == null) {
hasLocalOnlyFiles = true;
}
}
if (hasLocalOnlyFiles && Platform.isAndroid) {
final shouldProceed = await shouldProceedWithDeletion(context);
if (!shouldProceed) {
await dialog.hide();
return;
}
}
2021-09-15 20:44:52 +00:00
Set<String> deletedIDs = <String>{};
2021-05-25 12:26:31 +00:00
try {
2021-09-15 20:50:13 +00:00
deletedIDs =
(await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
2021-05-25 12:26:31 +00:00
} catch (e, s) {
_logger.severe("Could not delete file", e, s);
}
deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
final List<File> deletedFiles = [];
for (final file in files) {
// Remove only those files that have been removed from disk
if (deletedIDs.contains(file.localID) ||
alreadyDeletedIDs.contains(file.localID)) {
deletedFiles.add(file);
file.localID = null;
FilesDB.instance.update(file);
}
}
if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
2022-06-11 08:23:52 +00:00
Bus.instance.fire(
LocalPhotosUpdatedEvent(
deletedFiles,
type: EventType.deletedFromDevice,
),
);
}
await dialog.hide();
}
2021-06-28 10:12:12 +00:00
2021-10-26 13:32:33 +00:00
Future<bool> deleteFromTrash(BuildContext context, List<File> files) async {
final result = await showChoiceDialog(
2022-06-11 08:23:52 +00:00
context,
"Delete permanently?",
"This action cannot be undone",
firstAction: "Delete",
actionType: ActionType.critical,
);
if (result != DialogUserChoice.firstChoice) {
return false;
}
2022-05-30 11:05:08 +00:00
final dialog = createProgressDialog(context, "Permanently deleting...");
await dialog.show();
try {
await TrashSyncService.instance.deleteFromTrash(files);
2022-06-10 14:29:56 +00:00
showShortToast(context, "Successfully deleted");
await dialog.hide();
Bus.instance
.fire(FilesUpdatedEvent(files, type: EventType.deletedFromEverywhere));
return true;
} catch (e, s) {
_logger.info("failed to delete from trash", e, s);
await dialog.hide();
await showGenericErrorDialog(context);
return false;
}
}
2021-10-25 14:57:41 +00:00
Future<bool> emptyTrash(BuildContext context) async {
2022-06-11 08:23:52 +00:00
final result = await showChoiceDialog(
context,
"Empty trash?",
"These files will be permanently removed from your ente account",
firstAction: "Empty",
actionType: ActionType.critical,
);
2021-10-25 14:57:41 +00:00
if (result != DialogUserChoice.firstChoice) {
return false;
}
2022-05-30 11:05:08 +00:00
final dialog = createProgressDialog(context, "Please wait...");
2021-10-25 14:57:41 +00:00
await dialog.show();
try {
await TrashSyncService.instance.emptyTrash();
2022-06-10 14:29:56 +00:00
showShortToast(context, "Trash emptied");
2021-10-25 14:57:41 +00:00
await dialog.hide();
return true;
} catch (e, s) {
_logger.info("failed empty trash", e, s);
await dialog.hide();
await showGenericErrorDialog(context);
return false;
}
}
2021-06-28 16:32:29 +00:00
Future<bool> deleteLocalFiles(
2022-06-11 08:23:52 +00:00
BuildContext context,
List<String> localIDs,
) async {
2021-06-28 16:32:29 +00:00
final List<String> deletedIDs = [];
final List<String> localAssetIDs = [];
final List<String> localSharedMediaIDs = [];
for (String id in localIDs) {
if (id.startsWith(oldSharedMediaIdentifier) ||
id.startsWith(sharedMediaIdentifier)) {
localSharedMediaIDs.add(id);
} else {
localAssetIDs.add(id);
}
}
deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
2021-06-28 12:54:57 +00:00
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt < android11SDKINT) {
2021-09-15 20:50:13 +00:00
deletedIDs
.addAll(await _deleteLocalFilesInBatches(context, localAssetIDs));
} else {
2021-09-15 20:50:13 +00:00
deletedIDs
.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
}
2021-06-28 12:54:57 +00:00
} else {
deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
2021-06-28 10:12:12 +00:00
}
if (deletedIDs.isNotEmpty) {
final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
await FilesDB.instance.deleteLocalFiles(deletedIDs);
_logger.info(deletedFiles.length.toString() + " files deleted locally");
2021-06-29 06:08:52 +00:00
Bus.instance.fire(LocalPhotosUpdatedEvent(deletedFiles));
2021-06-28 16:34:06 +00:00
return true;
2021-06-28 16:32:29 +00:00
} else {
2022-06-10 14:29:56 +00:00
showToast(context, "Could not free up space");
2021-06-28 16:32:29 +00:00
return false;
2021-06-28 10:12:12 +00:00
}
}
2021-06-28 15:52:21 +00:00
2021-06-28 18:32:09 +00:00
Future<List<String>> _deleteLocalFilesInOneShot(
2022-06-11 08:23:52 +00:00
BuildContext context,
List<String> localIDs,
) async {
2022-07-03 10:35:46 +00:00
_logger.info('starting _deleteLocalFilesInOneShot for ${localIDs.length}');
2021-06-28 16:32:29 +00:00
final List<String> deletedIDs = [];
2022-06-11 08:23:52 +00:00
final dialog = createProgressDialog(
context,
"Deleting " + localIDs.length.toString() + " backed up files...",
);
2021-06-28 15:52:21 +00:00
await dialog.show();
try {
deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
} catch (e, s) {
_logger.severe("Could not delete files ", e, s);
}
2022-07-04 06:02:17 +00:00
_logger.info(
'_deleteLocalFilesInOneShot deleted ${deletedIDs.length} out '
'of ${localIDs.length}',
);
2021-06-28 15:52:21 +00:00
await dialog.hide();
2021-06-28 16:32:29 +00:00
return deletedIDs;
2021-06-28 15:52:21 +00:00
}
2021-06-28 18:32:09 +00:00
Future<List<String>> _deleteLocalFilesInBatches(
2022-06-11 08:23:52 +00:00
BuildContext context,
List<String> localIDs,
) async {
2021-06-28 15:52:21 +00:00
final dialogKey = GlobalKey<LinearProgressDialogState>();
final dialog = LinearProgressDialog(
2022-05-30 11:05:08 +00:00
"Deleting " + localIDs.length.toString() + " backed up files...",
2021-06-28 15:52:21 +00:00
key: dialogKey,
);
showDialog(
context: context,
builder: (context) {
return dialog;
},
2021-06-29 04:26:20 +00:00
barrierColor: Colors.black.withOpacity(0.85),
2021-06-28 15:52:21 +00:00
);
2021-06-28 16:08:07 +00:00
const minimumParts = 10;
const minimumBatchSize = 1;
const maximumBatchSize = 100;
final batchSize = min(
2022-06-11 08:23:52 +00:00
max(minimumBatchSize, (localIDs.length / minimumParts).round()),
maximumBatchSize,
);
2021-06-28 16:32:29 +00:00
final List<String> deletedIDs = [];
2021-06-28 15:52:21 +00:00
for (int index = 0; index < localIDs.length; index += batchSize) {
if (dialogKey.currentState != null) {
dialogKey.currentState.setProgress(index / localIDs.length);
}
final ids = localIDs
.getRange(index, min(localIDs.length, index + batchSize))
.toList();
_logger.info("Trying to delete " + ids.toString());
try {
deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
_logger.info("Deleted " + ids.toString());
} catch (e, s) {
_logger.severe("Could not delete batch " + ids.toString(), e, s);
for (final id in ids) {
try {
deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
_logger.info("Deleted " + id);
} catch (e, s) {
_logger.severe("Could not delete file " + id, e, s);
}
}
}
}
Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
2021-06-28 16:32:29 +00:00
return deletedIDs;
2021-06-28 15:52:21 +00:00
}
Future<bool> _localFileExist(File file) {
2022-09-23 01:48:25 +00:00
if (file.isSharedMediaToAppSandbox) {
2022-08-29 14:43:31 +00:00
final localFile = io.File(getSharedMediaFilePath(file));
return localFile.exists();
} else {
2022-09-23 01:48:25 +00:00
return file.getAsset.then((asset) {
if (asset == null) {
return false;
}
return asset.exists;
});
}
}
Future<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
final List<String> actuallyDeletedIDs = [];
try {
return Future.forEach(localIDs, (id) async {
2022-08-29 14:43:31 +00:00
final String localPath = getSharedMediaPathFromLocalID(id);
try {
// verify the file exists as the OS may have already deleted it from cache
if (io.File(localPath).existsSync()) {
await io.File(localPath).delete();
}
actuallyDeletedIDs.add(id);
} catch (e, s) {
_logger.warning("Could not delete file " + id, e, s);
// server log shouldn't contain localId
_logger.severe("Could not delete file ", e, s);
}
}).then((ignore) {
return actuallyDeletedIDs;
});
} catch (e, s) {
_logger.severe("Unexpected error while deleting share media files", e, s);
return Future.value(actuallyDeletedIDs);
}
}
Future<bool> shouldProceedWithDeletion(BuildContext context) async {
final choice = await showChoiceDialog(
context,
2022-05-30 11:05:08 +00:00
"Are you sure?",
"Some of the files you are trying to delete are only available on your device and cannot be recovered if deleted",
firstAction: "Cancel",
secondAction: "Delete",
secondActionColor: Colors.red,
);
return choice == DialogUserChoice.secondChoice;
}