2021-05-03 21:14:44 +00:00
|
|
|
import 'dart:async';
|
2021-07-24 14:41:28 +00:00
|
|
|
import 'dart:io' as io;
|
2021-06-28 12:54:57 +00:00
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:math';
|
2021-05-03 21:14:44 +00:00
|
|
|
|
2021-06-28 19:03:53 +00:00
|
|
|
import 'package:device_info/device_info.dart';
|
2021-06-28 15:52:21 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2021-05-03 21:14:44 +00:00
|
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:photo_manager/photo_manager.dart';
|
2021-07-24 14:41:28 +00:00
|
|
|
import 'package:photos/core/configuration.dart';
|
2021-06-28 19:03:53 +00:00
|
|
|
import 'package:photos/core/constants.dart';
|
2021-05-03 21:14:44 +00:00
|
|
|
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-06-14 16:24:15 +00:00
|
|
|
import 'package:photos/services/remote_sync_service.dart';
|
2021-05-03 21:14:44 +00:00
|
|
|
import 'package:photos/services/sync_service.dart';
|
2021-06-28 15:52:21 +00:00
|
|
|
import 'package:photos/ui/linear_progress_dialog.dart';
|
2021-05-03 21:14:44 +00:00
|
|
|
import 'package:photos/utils/dialog_util.dart';
|
|
|
|
import 'package:photos/utils/toast_util.dart';
|
|
|
|
|
2021-07-24 14:41:28 +00:00
|
|
|
import 'file_util.dart';
|
|
|
|
|
2021-05-03 21:14:44 +00:00
|
|
|
final _logger = Logger("DeleteFileUtil");
|
|
|
|
|
|
|
|
Future<void> deleteFilesFromEverywhere(
|
|
|
|
BuildContext context, List<File> files) async {
|
|
|
|
final dialog = createProgressDialog(context, "deleting...");
|
|
|
|
await dialog.show();
|
2021-06-13 20:09:06 +00:00
|
|
|
_logger.info("Trying to delete files " + files.toString());
|
2021-07-24 14:41:28 +00:00
|
|
|
final List<String> localAssetIDs = [];
|
|
|
|
final List<String> localSharedMediaIDs = [];
|
2021-06-13 19:49:25 +00:00
|
|
|
final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
|
2021-05-03 21:14:44 +00:00
|
|
|
for (final file in files) {
|
2021-09-15 20:50:13 +00:00
|
|
|
if (file.localID != null) {
|
2021-07-24 14:41:28 +00:00
|
|
|
if (!(await _localFileExist(file))) {
|
2021-06-13 20:09:06 +00:00
|
|
|
_logger.warning("Already deleted " + file.toString());
|
2021-06-13 19:49:25 +00:00
|
|
|
alreadyDeletedIDs.add(file.localID);
|
2021-09-15 20:50:13 +00:00
|
|
|
} else if (file.isSharedMediaToAppSandbox()) {
|
2021-07-24 14:41:28 +00:00
|
|
|
localSharedMediaIDs.add(file.localID);
|
2021-06-13 19:49:25 +00:00
|
|
|
} else {
|
2021-07-24 14:41:28 +00:00
|
|
|
localAssetIDs.add(file.localID);
|
2021-06-13 19:49:25 +00:00
|
|
|
}
|
2021-05-03 21:14:44 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-15 20:44:52 +00:00
|
|
|
Set<String> deletedIDs = <String>{};
|
2021-05-03 21:14:44 +00:00
|
|
|
try {
|
2021-09-15 20:50:13 +00:00
|
|
|
deletedIDs =
|
|
|
|
(await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
|
2021-05-03 21:14:44 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
_logger.severe("Could not delete file", e, s);
|
|
|
|
}
|
2021-07-24 14:41:28 +00:00
|
|
|
deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
|
2021-09-15 20:44:52 +00:00
|
|
|
final updatedCollectionIDs = <int>{};
|
2021-05-03 21:14:44 +00:00
|
|
|
final List<int> uploadedFileIDsToBeDeleted = [];
|
|
|
|
final List<File> deletedFiles = [];
|
|
|
|
for (final file in files) {
|
|
|
|
if (file.localID != null) {
|
2021-06-13 19:49:25 +00:00
|
|
|
// Remove only those files that have already been removed from disk
|
|
|
|
if (deletedIDs.contains(file.localID) ||
|
|
|
|
alreadyDeletedIDs.contains(file.localID)) {
|
2021-05-03 21:14:44 +00:00
|
|
|
deletedFiles.add(file);
|
|
|
|
if (file.uploadedFileID != null) {
|
|
|
|
uploadedFileIDsToBeDeleted.add(file.uploadedFileID);
|
|
|
|
updatedCollectionIDs.add(file.collectionID);
|
|
|
|
} else {
|
2021-07-24 17:22:46 +00:00
|
|
|
await FilesDB.instance.deleteLocalFile(file);
|
2021-05-03 21:14:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2021-05-03 21:26:23 +00:00
|
|
|
updatedCollectionIDs.add(file.collectionID);
|
|
|
|
deletedFiles.add(file);
|
2021-05-03 21:14:44 +00:00
|
|
|
uploadedFileIDsToBeDeleted.add(file.uploadedFileID);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (uploadedFileIDsToBeDeleted.isNotEmpty) {
|
|
|
|
try {
|
|
|
|
await SyncService.instance
|
|
|
|
.deleteFilesOnServer(uploadedFileIDsToBeDeleted);
|
|
|
|
await FilesDB.instance
|
|
|
|
.deleteMultipleUploadedFiles(uploadedFileIDsToBeDeleted);
|
|
|
|
} catch (e) {
|
|
|
|
_logger.severe(e);
|
|
|
|
await dialog.hide();
|
|
|
|
showGenericErrorDialog(context);
|
2021-09-15 20:44:52 +00:00
|
|
|
rethrow;
|
2021-05-03 21:14:44 +00:00
|
|
|
}
|
|
|
|
for (final collectionID in updatedCollectionIDs) {
|
|
|
|
Bus.instance.fire(CollectionUpdatedEvent(
|
2021-05-03 21:26:23 +00:00
|
|
|
collectionID,
|
|
|
|
deletedFiles
|
|
|
|
.where((file) => file.collectionID == collectionID)
|
|
|
|
.toList(),
|
|
|
|
type: EventType.deleted,
|
|
|
|
));
|
2021-05-03 21:14:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (deletedFiles.isNotEmpty) {
|
|
|
|
Bus.instance
|
|
|
|
.fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
|
|
|
|
}
|
|
|
|
await dialog.hide();
|
|
|
|
showToast("deleted from everywhere");
|
|
|
|
if (uploadedFileIDsToBeDeleted.isNotEmpty) {
|
2021-06-14 16:24:15 +00:00
|
|
|
RemoteSyncService.instance.sync(silently: true);
|
2021-05-03 21:14:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-15 20:50:13 +00:00
|
|
|
Future<void> deleteFilesFromRemoteOnly(
|
|
|
|
BuildContext context, List<File> files) async {
|
|
|
|
final dialog = createProgressDialog(context, "deleting...");
|
|
|
|
await dialog.show();
|
2021-09-30 08:16:54 +00:00
|
|
|
_logger.info("Trying to delete files " +
|
|
|
|
files.map((f) => f.uploadedFileID).toString());
|
2021-09-15 20:50:13 +00:00
|
|
|
final updatedCollectionIDs = <int>{};
|
|
|
|
final List<int> ids = [];
|
|
|
|
for (final file in files) {
|
|
|
|
updatedCollectionIDs.add(file.collectionID);
|
|
|
|
ids.add(file.uploadedFileID);
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
await SyncService.instance.deleteFilesOnServer(ids);
|
|
|
|
await FilesDB.instance.deleteMultipleUploadedFiles(ids);
|
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) {
|
|
|
|
Bus.instance.fire(CollectionUpdatedEvent(
|
|
|
|
collectionID,
|
|
|
|
files.where((file) => file.collectionID == collectionID).toList(),
|
|
|
|
type: EventType.deleted,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
await dialog.hide();
|
|
|
|
RemoteSyncService.instance.sync(silently: true);
|
|
|
|
}
|
|
|
|
|
2021-05-03 21:14:44 +00:00
|
|
|
Future<void> deleteFilesOnDeviceOnly(
|
|
|
|
BuildContext context, List<File> files) async {
|
|
|
|
final dialog = createProgressDialog(context, "deleting...");
|
|
|
|
await dialog.show();
|
2021-06-13 20:09:06 +00:00
|
|
|
_logger.info("Trying to delete files " + files.toString());
|
2021-07-24 14:41:28 +00:00
|
|
|
final List<String> localAssetIDs = [];
|
|
|
|
final List<String> localSharedMediaIDs = [];
|
2021-06-13 19:49:25 +00:00
|
|
|
final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
|
2021-05-03 21:14:44 +00:00
|
|
|
for (final file in files) {
|
|
|
|
if (file.localID != null) {
|
2021-07-24 14:41:28 +00:00
|
|
|
if (!(await _localFileExist(file))) {
|
2021-06-13 20:09:06 +00:00
|
|
|
_logger.warning("Already deleted " + file.toString());
|
2021-06-13 19:49:25 +00:00
|
|
|
alreadyDeletedIDs.add(file.localID);
|
2021-09-15 20:50:13 +00:00
|
|
|
} else if (file.isSharedMediaToAppSandbox()) {
|
2021-07-24 14:41:28 +00:00
|
|
|
localSharedMediaIDs.add(file.localID);
|
2021-06-13 19:49:25 +00:00
|
|
|
} else {
|
2021-07-24 14:41:28 +00:00
|
|
|
localAssetIDs.add(file.localID);
|
2021-06-13 19:49:25 +00:00
|
|
|
}
|
2021-05-03 21:14:44 +00:00
|
|
|
}
|
|
|
|
}
|
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);
|
|
|
|
}
|
2021-07-24 14:41:28 +00:00
|
|
|
deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
|
2021-05-03 21:14:44 +00:00
|
|
|
final List<File> deletedFiles = [];
|
|
|
|
for (final file in files) {
|
|
|
|
// Remove only those files that have been removed from disk
|
2021-06-13 19:49:25 +00:00
|
|
|
if (deletedIDs.contains(file.localID) ||
|
|
|
|
alreadyDeletedIDs.contains(file.localID)) {
|
2021-05-03 21:14:44 +00:00
|
|
|
deletedFiles.add(file);
|
|
|
|
file.localID = null;
|
|
|
|
FilesDB.instance.update(file);
|
|
|
|
}
|
|
|
|
}
|
2021-06-13 19:49:25 +00:00
|
|
|
if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
|
2021-05-03 21:14:44 +00:00
|
|
|
Bus.instance
|
|
|
|
.fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
|
|
|
|
}
|
|
|
|
await dialog.hide();
|
|
|
|
}
|
2021-06-28 10:12:12 +00:00
|
|
|
|
2021-06-28 16:32:29 +00:00
|
|
|
Future<bool> deleteLocalFiles(
|
2021-06-28 15:52:21 +00:00
|
|
|
BuildContext context, List<String> localIDs) async {
|
2021-06-28 16:32:29 +00:00
|
|
|
final List<String> deletedIDs = [];
|
2021-07-24 14:41:28 +00:00
|
|
|
final List<String> localAssetIDs = [];
|
|
|
|
final List<String> localSharedMediaIDs = [];
|
|
|
|
for (String id in localIDs) {
|
|
|
|
if (id.startsWith(kSharedMediaIdentifier)) {
|
|
|
|
localSharedMediaIDs.add(id);
|
|
|
|
} else {
|
|
|
|
localAssetIDs.add(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
|
|
|
|
|
2021-06-28 12:54:57 +00:00
|
|
|
if (Platform.isAndroid) {
|
2021-06-28 19:03:53 +00:00
|
|
|
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
2021-07-21 20:47:43 +00:00
|
|
|
if (androidInfo.version.sdkInt < kAndroid11SDKINT) {
|
2021-09-15 20:50:13 +00:00
|
|
|
deletedIDs
|
|
|
|
.addAll(await _deleteLocalFilesInBatches(context, localAssetIDs));
|
2021-06-28 19:03:53 +00:00
|
|
|
} else {
|
2021-09-15 20:50:13 +00:00
|
|
|
deletedIDs
|
|
|
|
.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
|
2021-06-28 19:03:53 +00:00
|
|
|
}
|
2021-06-28 12:54:57 +00:00
|
|
|
} else {
|
2021-07-24 14:41:28 +00:00
|
|
|
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 {
|
2021-06-28 16:51:52 +00:00
|
|
|
showToast("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(
|
|
|
|
BuildContext context, List<String> localIDs) async {
|
2021-06-28 16:32:29 +00:00
|
|
|
final List<String> deletedIDs = [];
|
2021-06-28 16:08:07 +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);
|
|
|
|
}
|
|
|
|
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(
|
|
|
|
BuildContext context, List<String> localIDs) async {
|
2021-06-28 15:52:21 +00:00
|
|
|
final dialogKey = GlobalKey<LinearProgressDialogState>();
|
|
|
|
final dialog = LinearProgressDialog(
|
2021-06-28 16:08:07 +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(
|
|
|
|
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
|
|
|
}
|
2021-07-24 14:41:28 +00:00
|
|
|
|
|
|
|
Future<bool> _localFileExist(File file) {
|
2021-07-24 17:33:59 +00:00
|
|
|
if (file.isSharedMediaToAppSandbox()) {
|
2021-07-24 14:41:28 +00:00
|
|
|
var localFile = io.File(getSharedMediaFilePath(file));
|
|
|
|
return localFile.exists();
|
|
|
|
} else {
|
|
|
|
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 {
|
|
|
|
String localPath = Configuration.instance.getSharedMediaCacheDirectory() +
|
|
|
|
"/" +
|
|
|
|
id.replaceAll(kSharedMediaIdentifier, '');
|
|
|
|
try {
|
2021-09-09 05:50:55 +00:00
|
|
|
// verify the file exists as the OS may have already deleted it from cache
|
|
|
|
if (io.File(localPath).existsSync()) {
|
|
|
|
await io.File(localPath).delete();
|
|
|
|
}
|
2021-07-24 14:41:28 +00:00
|
|
|
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);
|
|
|
|
}
|
2021-07-24 17:22:46 +00:00
|
|
|
}
|