delete_file_util.dart 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/widgets.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photo_manager/photo_manager.dart';
  8. import 'package:photos/core/event_bus.dart';
  9. import 'package:photos/db/files_db.dart';
  10. import 'package:photos/events/collection_updated_event.dart';
  11. import 'package:photos/events/files_updated_event.dart';
  12. import 'package:photos/events/local_photos_updated_event.dart';
  13. import 'package:photos/models/file.dart';
  14. import 'package:photos/services/remote_sync_service.dart';
  15. import 'package:photos/services/sync_service.dart';
  16. import 'package:photos/ui/linear_progress_dialog.dart';
  17. import 'package:photos/utils/dialog_util.dart';
  18. import 'package:photos/utils/toast_util.dart';
  19. final _logger = Logger("DeleteFileUtil");
  20. Future<void> deleteFilesFromEverywhere(
  21. BuildContext context, List<File> files) async {
  22. final dialog = createProgressDialog(context, "deleting...");
  23. await dialog.show();
  24. _logger.info("Trying to delete files " + files.toString());
  25. final List<String> localIDs = [];
  26. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  27. for (final file in files) {
  28. if (file.localID != null) {
  29. final asset = await file.getAsset();
  30. if (asset == null || !(await asset.exists)) {
  31. _logger.warning("Already deleted " + file.toString());
  32. alreadyDeletedIDs.add(file.localID);
  33. } else {
  34. localIDs.add(file.localID);
  35. }
  36. }
  37. }
  38. Set<String> deletedIDs = Set<String>();
  39. try {
  40. deletedIDs = (await PhotoManager.editor.deleteWithIds(localIDs)).toSet();
  41. } catch (e, s) {
  42. _logger.severe("Could not delete file", e, s);
  43. }
  44. final updatedCollectionIDs = Set<int>();
  45. final List<int> uploadedFileIDsToBeDeleted = [];
  46. final List<File> deletedFiles = [];
  47. for (final file in files) {
  48. if (file.localID != null) {
  49. // Remove only those files that have already been removed from disk
  50. if (deletedIDs.contains(file.localID) ||
  51. alreadyDeletedIDs.contains(file.localID)) {
  52. deletedFiles.add(file);
  53. if (file.uploadedFileID != null) {
  54. uploadedFileIDsToBeDeleted.add(file.uploadedFileID);
  55. updatedCollectionIDs.add(file.collectionID);
  56. } else {
  57. await FilesDB.instance.deleteLocalFile(file.localID);
  58. }
  59. }
  60. } else {
  61. updatedCollectionIDs.add(file.collectionID);
  62. deletedFiles.add(file);
  63. uploadedFileIDsToBeDeleted.add(file.uploadedFileID);
  64. }
  65. }
  66. if (uploadedFileIDsToBeDeleted.isNotEmpty) {
  67. try {
  68. await SyncService.instance
  69. .deleteFilesOnServer(uploadedFileIDsToBeDeleted);
  70. await FilesDB.instance
  71. .deleteMultipleUploadedFiles(uploadedFileIDsToBeDeleted);
  72. } catch (e) {
  73. _logger.severe(e);
  74. await dialog.hide();
  75. showGenericErrorDialog(context);
  76. throw e;
  77. }
  78. for (final collectionID in updatedCollectionIDs) {
  79. Bus.instance.fire(CollectionUpdatedEvent(
  80. collectionID,
  81. deletedFiles
  82. .where((file) => file.collectionID == collectionID)
  83. .toList(),
  84. type: EventType.deleted,
  85. ));
  86. }
  87. }
  88. if (deletedFiles.isNotEmpty) {
  89. Bus.instance
  90. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  91. }
  92. await dialog.hide();
  93. showToast("deleted from everywhere");
  94. if (uploadedFileIDsToBeDeleted.isNotEmpty) {
  95. RemoteSyncService.instance.sync(silently: true);
  96. }
  97. }
  98. Future<void> deleteFilesOnDeviceOnly(
  99. BuildContext context, List<File> files) async {
  100. final dialog = createProgressDialog(context, "deleting...");
  101. await dialog.show();
  102. _logger.info("Trying to delete files " + files.toString());
  103. final List<String> localIDs = [];
  104. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  105. for (final file in files) {
  106. if (file.localID != null) {
  107. final asset = await file.getAsset();
  108. if (asset == null || !(await asset.exists)) {
  109. _logger.warning("Already deleted " + file.toString());
  110. alreadyDeletedIDs.add(file.localID);
  111. } else {
  112. localIDs.add(file.localID);
  113. }
  114. }
  115. }
  116. Set<String> deletedIDs = Set<String>();
  117. try {
  118. deletedIDs = (await PhotoManager.editor.deleteWithIds(localIDs)).toSet();
  119. } catch (e, s) {
  120. _logger.severe("Could not delete file", e, s);
  121. }
  122. final List<File> deletedFiles = [];
  123. for (final file in files) {
  124. // Remove only those files that have been removed from disk
  125. if (deletedIDs.contains(file.localID) ||
  126. alreadyDeletedIDs.contains(file.localID)) {
  127. deletedFiles.add(file);
  128. file.localID = null;
  129. FilesDB.instance.update(file);
  130. }
  131. }
  132. if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
  133. Bus.instance
  134. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  135. }
  136. await dialog.hide();
  137. }
  138. Future<bool> deleteLocalFiles(
  139. BuildContext context, List<String> localIDs) async {
  140. final List<String> deletedIDs = [];
  141. if (Platform.isAndroid) {
  142. deletedIDs.addAll(await _deleteLocalFilesInBatches(context, localIDs));
  143. } else {
  144. deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localIDs));
  145. }
  146. if (deletedIDs.isNotEmpty) {
  147. final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
  148. await FilesDB.instance.deleteLocalFiles(deletedIDs);
  149. _logger.info(deletedFiles.length.toString() + " files deleted locally");
  150. Bus.instance
  151. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  152. if (Platform.isIOS) {
  153. showToast(
  154. "also empty \"Recently Deleted\" from Settings to claim the freed up space");
  155. }
  156. return true;
  157. } else {
  158. showToast("could not free up space");
  159. return false;
  160. }
  161. }
  162. Future<List<String>> _deleteLocalFilesInOneShot(
  163. BuildContext context, List<String> localIDs) async {
  164. final List<String> deletedIDs = [];
  165. final dialog = createProgressDialog(context,
  166. "deleting " + localIDs.length.toString() + " backed up files...");
  167. await dialog.show();
  168. try {
  169. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
  170. } catch (e, s) {
  171. _logger.severe("Could not delete files ", e, s);
  172. }
  173. await dialog.hide();
  174. return deletedIDs;
  175. }
  176. Future<List<String>> _deleteLocalFilesInBatches(
  177. BuildContext context, List<String> localIDs) async {
  178. final dialogKey = GlobalKey<LinearProgressDialogState>();
  179. final dialog = LinearProgressDialog(
  180. "deleting " + localIDs.length.toString() + " backed up files...",
  181. key: dialogKey,
  182. );
  183. showDialog(
  184. context: context,
  185. builder: (context) {
  186. return dialog;
  187. },
  188. );
  189. const minimumParts = 10;
  190. const minimumBatchSize = 1;
  191. const maximumBatchSize = 100;
  192. final batchSize = min(
  193. max(minimumBatchSize, (localIDs.length / minimumParts).round()),
  194. maximumBatchSize);
  195. final List<String> deletedIDs = [];
  196. for (int index = 0; index < localIDs.length; index += batchSize) {
  197. if (dialogKey.currentState != null) {
  198. dialogKey.currentState.setProgress(index / localIDs.length);
  199. }
  200. final ids = localIDs
  201. .getRange(index, min(localIDs.length, index + batchSize))
  202. .toList();
  203. _logger.info("Trying to delete " + ids.toString());
  204. try {
  205. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
  206. _logger.info("Deleted " + ids.toString());
  207. } catch (e, s) {
  208. _logger.severe("Could not delete batch " + ids.toString(), e, s);
  209. for (final id in ids) {
  210. try {
  211. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
  212. _logger.info("Deleted " + id);
  213. } catch (e, s) {
  214. _logger.severe("Could not delete file " + id, e, s);
  215. }
  216. }
  217. }
  218. }
  219. Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
  220. return deletedIDs;
  221. }