delete_file_util.dart 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import 'dart:async';
  2. import 'dart:io' as io;
  3. import 'dart:io';
  4. import 'dart:math';
  5. import 'package:device_info/device_info.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/widgets.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:photo_manager/photo_manager.dart';
  10. import 'package:photos/core/configuration.dart';
  11. import 'package:photos/core/constants.dart';
  12. import 'package:photos/core/event_bus.dart';
  13. import 'package:photos/db/files_db.dart';
  14. import 'package:photos/events/collection_updated_event.dart';
  15. import 'package:photos/events/files_updated_event.dart';
  16. import 'package:photos/events/local_photos_updated_event.dart';
  17. import 'package:photos/models/file.dart';
  18. import 'package:photos/services/remote_sync_service.dart';
  19. import 'package:photos/services/sync_service.dart';
  20. import 'package:photos/ui/linear_progress_dialog.dart';
  21. import 'package:photos/utils/dialog_util.dart';
  22. import 'package:photos/utils/toast_util.dart';
  23. import 'file_util.dart';
  24. final _logger = Logger("DeleteFileUtil");
  25. Future<void> deleteFilesFromEverywhere(
  26. BuildContext context, List<File> files) async {
  27. final dialog = createProgressDialog(context, "deleting...");
  28. await dialog.show();
  29. _logger.info("Trying to delete files " + files.toString());
  30. final List<String> localAssetIDs = [];
  31. final List<String> localSharedMediaIDs = [];
  32. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  33. for (final file in files) {
  34. if (file.localID != null) {
  35. if (!(await _localFileExist(file))) {
  36. _logger.warning("Already deleted " + file.toString());
  37. alreadyDeletedIDs.add(file.localID);
  38. } else if(file.isSharedMediaToAppSandbox()) {
  39. localSharedMediaIDs.add(file.localID);
  40. } else {
  41. localAssetIDs.add(file.localID);
  42. }
  43. }
  44. }
  45. Set<String> deletedIDs = <String>{};
  46. try {
  47. deletedIDs = (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  48. } catch (e, s) {
  49. _logger.severe("Could not delete file", e, s);
  50. }
  51. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  52. final updatedCollectionIDs = <int>{};
  53. final List<int> uploadedFileIDsToBeDeleted = [];
  54. final List<File> deletedFiles = [];
  55. for (final file in files) {
  56. if (file.localID != null) {
  57. // Remove only those files that have already been removed from disk
  58. if (deletedIDs.contains(file.localID) ||
  59. alreadyDeletedIDs.contains(file.localID)) {
  60. deletedFiles.add(file);
  61. if (file.uploadedFileID != null) {
  62. uploadedFileIDsToBeDeleted.add(file.uploadedFileID);
  63. updatedCollectionIDs.add(file.collectionID);
  64. } else {
  65. await FilesDB.instance.deleteLocalFile(file);
  66. }
  67. }
  68. } else {
  69. updatedCollectionIDs.add(file.collectionID);
  70. deletedFiles.add(file);
  71. uploadedFileIDsToBeDeleted.add(file.uploadedFileID);
  72. }
  73. }
  74. if (uploadedFileIDsToBeDeleted.isNotEmpty) {
  75. try {
  76. await SyncService.instance
  77. .deleteFilesOnServer(uploadedFileIDsToBeDeleted);
  78. await FilesDB.instance
  79. .deleteMultipleUploadedFiles(uploadedFileIDsToBeDeleted);
  80. } catch (e) {
  81. _logger.severe(e);
  82. await dialog.hide();
  83. showGenericErrorDialog(context);
  84. rethrow;
  85. }
  86. for (final collectionID in updatedCollectionIDs) {
  87. Bus.instance.fire(CollectionUpdatedEvent(
  88. collectionID,
  89. deletedFiles
  90. .where((file) => file.collectionID == collectionID)
  91. .toList(),
  92. type: EventType.deleted,
  93. ));
  94. }
  95. }
  96. if (deletedFiles.isNotEmpty) {
  97. Bus.instance
  98. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  99. }
  100. await dialog.hide();
  101. showToast("deleted from everywhere");
  102. if (uploadedFileIDsToBeDeleted.isNotEmpty) {
  103. RemoteSyncService.instance.sync(silently: true);
  104. }
  105. }
  106. Future<void> deleteFilesOnDeviceOnly(
  107. BuildContext context, List<File> files) async {
  108. final dialog = createProgressDialog(context, "deleting...");
  109. await dialog.show();
  110. _logger.info("Trying to delete files " + files.toString());
  111. final List<String> localAssetIDs = [];
  112. final List<String> localSharedMediaIDs = [];
  113. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  114. for (final file in files) {
  115. if (file.localID != null) {
  116. if (!(await _localFileExist(file))) {
  117. _logger.warning("Already deleted " + file.toString());
  118. alreadyDeletedIDs.add(file.localID);
  119. } else if(file.isSharedMediaToAppSandbox()) {
  120. localSharedMediaIDs.add(file.localID);
  121. } else {
  122. localAssetIDs.add(file.localID);
  123. }
  124. }
  125. }
  126. Set<String> deletedIDs = <String>{};
  127. try {
  128. deletedIDs = (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  129. } catch (e, s) {
  130. _logger.severe("Could not delete file", e, s);
  131. }
  132. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  133. final List<File> deletedFiles = [];
  134. for (final file in files) {
  135. // Remove only those files that have been removed from disk
  136. if (deletedIDs.contains(file.localID) ||
  137. alreadyDeletedIDs.contains(file.localID)) {
  138. deletedFiles.add(file);
  139. file.localID = null;
  140. FilesDB.instance.update(file);
  141. }
  142. }
  143. if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
  144. Bus.instance
  145. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  146. }
  147. await dialog.hide();
  148. }
  149. Future<bool> deleteLocalFiles(
  150. BuildContext context, List<String> localIDs) async {
  151. final List<String> deletedIDs = [];
  152. final List<String> localAssetIDs = [];
  153. final List<String> localSharedMediaIDs = [];
  154. for (String id in localIDs) {
  155. if (id.startsWith(kSharedMediaIdentifier)) {
  156. localSharedMediaIDs.add(id);
  157. } else {
  158. localAssetIDs.add(id);
  159. }
  160. }
  161. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  162. if (Platform.isAndroid) {
  163. final androidInfo = await DeviceInfoPlugin().androidInfo;
  164. if (androidInfo.version.sdkInt < kAndroid11SDKINT) {
  165. deletedIDs.addAll(await _deleteLocalFilesInBatches(context, localAssetIDs));
  166. } else {
  167. deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  168. }
  169. } else {
  170. deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  171. }
  172. if (deletedIDs.isNotEmpty) {
  173. final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
  174. await FilesDB.instance.deleteLocalFiles(deletedIDs);
  175. _logger.info(deletedFiles.length.toString() + " files deleted locally");
  176. Bus.instance.fire(LocalPhotosUpdatedEvent(deletedFiles));
  177. return true;
  178. } else {
  179. showToast("could not free up space");
  180. return false;
  181. }
  182. }
  183. Future<List<String>> _deleteLocalFilesInOneShot(
  184. BuildContext context, List<String> localIDs) async {
  185. final List<String> deletedIDs = [];
  186. final dialog = createProgressDialog(context,
  187. "deleting " + localIDs.length.toString() + " backed up files...");
  188. await dialog.show();
  189. try {
  190. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
  191. } catch (e, s) {
  192. _logger.severe("Could not delete files ", e, s);
  193. }
  194. await dialog.hide();
  195. return deletedIDs;
  196. }
  197. Future<List<String>> _deleteLocalFilesInBatches(
  198. BuildContext context, List<String> localIDs) async {
  199. final dialogKey = GlobalKey<LinearProgressDialogState>();
  200. final dialog = LinearProgressDialog(
  201. "deleting " + localIDs.length.toString() + " backed up files...",
  202. key: dialogKey,
  203. );
  204. showDialog(
  205. context: context,
  206. builder: (context) {
  207. return dialog;
  208. },
  209. barrierColor: Colors.black.withOpacity(0.85),
  210. );
  211. const minimumParts = 10;
  212. const minimumBatchSize = 1;
  213. const maximumBatchSize = 100;
  214. final batchSize = min(
  215. max(minimumBatchSize, (localIDs.length / minimumParts).round()),
  216. maximumBatchSize);
  217. final List<String> deletedIDs = [];
  218. for (int index = 0; index < localIDs.length; index += batchSize) {
  219. if (dialogKey.currentState != null) {
  220. dialogKey.currentState.setProgress(index / localIDs.length);
  221. }
  222. final ids = localIDs
  223. .getRange(index, min(localIDs.length, index + batchSize))
  224. .toList();
  225. _logger.info("Trying to delete " + ids.toString());
  226. try {
  227. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
  228. _logger.info("Deleted " + ids.toString());
  229. } catch (e, s) {
  230. _logger.severe("Could not delete batch " + ids.toString(), e, s);
  231. for (final id in ids) {
  232. try {
  233. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
  234. _logger.info("Deleted " + id);
  235. } catch (e, s) {
  236. _logger.severe("Could not delete file " + id, e, s);
  237. }
  238. }
  239. }
  240. }
  241. Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
  242. return deletedIDs;
  243. }
  244. Future<bool> _localFileExist(File file) {
  245. if (file.isSharedMediaToAppSandbox()) {
  246. var localFile = io.File(getSharedMediaFilePath(file));
  247. return localFile.exists();
  248. } else {
  249. return file.getAsset().then((asset) {
  250. if (asset == null) {
  251. return false;
  252. }
  253. return asset.exists;
  254. });
  255. }
  256. }
  257. Future<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
  258. final List<String> actuallyDeletedIDs = [];
  259. try {
  260. return Future.forEach(localIDs, (id) async {
  261. String localPath = Configuration.instance.getSharedMediaCacheDirectory() +
  262. "/" +
  263. id.replaceAll(kSharedMediaIdentifier, '');
  264. try {
  265. // verify the file exists as the OS may have already deleted it from cache
  266. if (io.File(localPath).existsSync()) {
  267. await io.File(localPath).delete();
  268. }
  269. actuallyDeletedIDs.add(id);
  270. } catch (e, s) {
  271. _logger.warning("Could not delete file " + id, e, s);
  272. // server log shouldn't contain localId
  273. _logger.severe("Could not delete file ", e, s);
  274. }
  275. }).then((ignore) {
  276. return actuallyDeletedIDs;
  277. });
  278. } catch (e, s) {
  279. _logger.severe("Unexpected error while deleting share media files", e, s);
  280. return Future.value(actuallyDeletedIDs);
  281. }
  282. }