delete_file_util.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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:logging/logging.dart';
  8. import 'package:photo_manager/photo_manager.dart';
  9. import 'package:photos/core/configuration.dart';
  10. import 'package:photos/core/constants.dart';
  11. import 'package:photos/core/event_bus.dart';
  12. import 'package:photos/db/files_db.dart';
  13. import 'package:photos/events/collection_updated_event.dart';
  14. import 'package:photos/events/files_updated_event.dart';
  15. import 'package:photos/events/local_photos_updated_event.dart';
  16. import 'package:photos/models/file.dart';
  17. import 'package:photos/models/trash_item_request.dart';
  18. import 'package:photos/services/remote_sync_service.dart';
  19. import 'package:photos/services/sync_service.dart';
  20. import 'package:photos/services/trash_sync_service.dart';
  21. import 'package:photos/ui/common/dialogs.dart';
  22. import 'package:photos/ui/common/linear_progress_dialog.dart';
  23. import 'package:photos/utils/dialog_util.dart';
  24. import 'package:photos/utils/file_util.dart';
  25. import 'package:photos/utils/toast_util.dart';
  26. final _logger = Logger("DeleteFileUtil");
  27. Future<void> deleteFilesFromEverywhere(
  28. BuildContext context,
  29. List<File> files,
  30. ) async {
  31. final dialog = createProgressDialog(context, "Deleting...");
  32. await dialog.show();
  33. _logger.info("Trying to delete files " + files.toString());
  34. final List<String> localAssetIDs = [];
  35. final List<String> localSharedMediaIDs = [];
  36. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  37. bool hasLocalOnlyFiles = false;
  38. for (final file in files) {
  39. if (file.localID != null) {
  40. if (!(await _localFileExist(file))) {
  41. _logger.warning("Already deleted " + file.toString());
  42. alreadyDeletedIDs.add(file.localID);
  43. } else if (file.isSharedMediaToAppSandbox()) {
  44. localSharedMediaIDs.add(file.localID);
  45. } else {
  46. localAssetIDs.add(file.localID);
  47. }
  48. }
  49. if (file.uploadedFileID == null) {
  50. hasLocalOnlyFiles = true;
  51. }
  52. }
  53. if (hasLocalOnlyFiles && Platform.isAndroid) {
  54. final shouldProceed = await shouldProceedWithDeletion(context);
  55. if (!shouldProceed) {
  56. await dialog.hide();
  57. return;
  58. }
  59. }
  60. Set<String> deletedIDs = <String>{};
  61. try {
  62. deletedIDs =
  63. (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  64. } catch (e, s) {
  65. _logger.severe("Could not delete file", e, s);
  66. }
  67. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  68. final updatedCollectionIDs = <int>{};
  69. final List<TrashRequest> uploadedFilesToBeTrashed = [];
  70. final List<File> deletedFiles = [];
  71. for (final file in files) {
  72. if (file.localID != null) {
  73. // Remove only those files that have already been removed from disk
  74. if (deletedIDs.contains(file.localID) ||
  75. alreadyDeletedIDs.contains(file.localID)) {
  76. deletedFiles.add(file);
  77. if (file.uploadedFileID != null) {
  78. uploadedFilesToBeTrashed
  79. .add(TrashRequest(file.uploadedFileID, file.collectionID));
  80. updatedCollectionIDs.add(file.collectionID);
  81. } else {
  82. await FilesDB.instance.deleteLocalFile(file);
  83. }
  84. }
  85. } else {
  86. updatedCollectionIDs.add(file.collectionID);
  87. deletedFiles.add(file);
  88. uploadedFilesToBeTrashed
  89. .add(TrashRequest(file.uploadedFileID, file.collectionID));
  90. }
  91. }
  92. if (uploadedFilesToBeTrashed.isNotEmpty) {
  93. try {
  94. final fileIDs =
  95. uploadedFilesToBeTrashed.map((item) => item.fileID).toList();
  96. await TrashSyncService.instance
  97. .trashFilesOnServer(uploadedFilesToBeTrashed);
  98. // await SyncService.instance
  99. // .deleteFilesOnServer(fileIDs);
  100. await FilesDB.instance.deleteMultipleUploadedFiles(fileIDs);
  101. } catch (e) {
  102. _logger.severe(e);
  103. await dialog.hide();
  104. showGenericErrorDialog(context);
  105. rethrow;
  106. }
  107. for (final collectionID in updatedCollectionIDs) {
  108. Bus.instance.fire(
  109. CollectionUpdatedEvent(
  110. collectionID,
  111. deletedFiles
  112. .where((file) => file.collectionID == collectionID)
  113. .toList(),
  114. type: EventType.deletedFromEverywhere,
  115. ),
  116. );
  117. }
  118. }
  119. if (deletedFiles.isNotEmpty) {
  120. Bus.instance.fire(
  121. LocalPhotosUpdatedEvent(
  122. deletedFiles,
  123. type: EventType.deletedFromEverywhere,
  124. ),
  125. );
  126. if (hasLocalOnlyFiles && Platform.isAndroid) {
  127. showShortToast(context, "Files deleted");
  128. } else {
  129. showShortToast(context, "Moved to trash");
  130. }
  131. }
  132. await dialog.hide();
  133. if (uploadedFilesToBeTrashed.isNotEmpty) {
  134. RemoteSyncService.instance.sync(silently: true);
  135. }
  136. }
  137. Future<void> deleteFilesFromRemoteOnly(
  138. BuildContext context,
  139. List<File> files,
  140. ) async {
  141. files.removeWhere((element) => element.uploadedFileID == null);
  142. if (files.isEmpty) {
  143. showToast(context, "Selected files are not on ente");
  144. return;
  145. }
  146. final dialog = createProgressDialog(context, "Deleting...");
  147. await dialog.show();
  148. _logger.info(
  149. "Trying to delete files " + files.map((f) => f.uploadedFileID).toString(),
  150. );
  151. final updatedCollectionIDs = <int>{};
  152. final List<int> uploadedFileIDs = [];
  153. final List<TrashRequest> trashRequests = [];
  154. for (final file in files) {
  155. updatedCollectionIDs.add(file.collectionID);
  156. uploadedFileIDs.add(file.uploadedFileID);
  157. trashRequests.add(TrashRequest(file.uploadedFileID, file.collectionID));
  158. }
  159. try {
  160. await TrashSyncService.instance.trashFilesOnServer(trashRequests);
  161. await FilesDB.instance.deleteMultipleUploadedFiles(uploadedFileIDs);
  162. } catch (e, s) {
  163. _logger.severe("Failed to delete files from remote", e, s);
  164. await dialog.hide();
  165. showGenericErrorDialog(context);
  166. rethrow;
  167. }
  168. for (final collectionID in updatedCollectionIDs) {
  169. Bus.instance.fire(
  170. CollectionUpdatedEvent(
  171. collectionID,
  172. files.where((file) => file.collectionID == collectionID).toList(),
  173. type: EventType.deletedFromRemote,
  174. ),
  175. );
  176. }
  177. Bus.instance
  178. .fire(LocalPhotosUpdatedEvent(files, type: EventType.deletedFromRemote));
  179. SyncService.instance.sync();
  180. await dialog.hide();
  181. RemoteSyncService.instance.sync(silently: true);
  182. }
  183. Future<void> deleteFilesOnDeviceOnly(
  184. BuildContext context,
  185. List<File> files,
  186. ) async {
  187. final dialog = createProgressDialog(context, "Deleting...");
  188. await dialog.show();
  189. _logger.info("Trying to delete files " + files.toString());
  190. final List<String> localAssetIDs = [];
  191. final List<String> localSharedMediaIDs = [];
  192. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  193. bool hasLocalOnlyFiles = false;
  194. for (final file in files) {
  195. if (file.localID != null) {
  196. if (!(await _localFileExist(file))) {
  197. _logger.warning("Already deleted " + file.toString());
  198. alreadyDeletedIDs.add(file.localID);
  199. } else if (file.isSharedMediaToAppSandbox()) {
  200. localSharedMediaIDs.add(file.localID);
  201. } else {
  202. localAssetIDs.add(file.localID);
  203. }
  204. }
  205. if (file.uploadedFileID == null) {
  206. hasLocalOnlyFiles = true;
  207. }
  208. }
  209. if (hasLocalOnlyFiles && Platform.isAndroid) {
  210. final shouldProceed = await shouldProceedWithDeletion(context);
  211. if (!shouldProceed) {
  212. await dialog.hide();
  213. return;
  214. }
  215. }
  216. Set<String> deletedIDs = <String>{};
  217. try {
  218. deletedIDs =
  219. (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  220. } catch (e, s) {
  221. _logger.severe("Could not delete file", e, s);
  222. }
  223. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  224. final List<File> deletedFiles = [];
  225. for (final file in files) {
  226. // Remove only those files that have been removed from disk
  227. if (deletedIDs.contains(file.localID) ||
  228. alreadyDeletedIDs.contains(file.localID)) {
  229. deletedFiles.add(file);
  230. file.localID = null;
  231. FilesDB.instance.update(file);
  232. }
  233. }
  234. if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
  235. Bus.instance.fire(
  236. LocalPhotosUpdatedEvent(
  237. deletedFiles,
  238. type: EventType.deletedFromDevice,
  239. ),
  240. );
  241. }
  242. await dialog.hide();
  243. }
  244. Future<bool> deleteFromTrash(BuildContext context, List<File> files) async {
  245. final result = await showChoiceDialog(
  246. context,
  247. "Delete permanently?",
  248. "This action cannot be undone",
  249. firstAction: "Delete",
  250. actionType: ActionType.critical,
  251. );
  252. if (result != DialogUserChoice.firstChoice) {
  253. return false;
  254. }
  255. final dialog = createProgressDialog(context, "Permanently deleting...");
  256. await dialog.show();
  257. try {
  258. await TrashSyncService.instance.deleteFromTrash(files);
  259. showShortToast(context, "Successfully deleted");
  260. await dialog.hide();
  261. Bus.instance
  262. .fire(FilesUpdatedEvent(files, type: EventType.deletedFromEverywhere));
  263. return true;
  264. } catch (e, s) {
  265. _logger.info("failed to delete from trash", e, s);
  266. await dialog.hide();
  267. await showGenericErrorDialog(context);
  268. return false;
  269. }
  270. }
  271. Future<bool> emptyTrash(BuildContext context) async {
  272. final result = await showChoiceDialog(
  273. context,
  274. "Empty trash?",
  275. "These files will be permanently removed from your ente account",
  276. firstAction: "Empty",
  277. actionType: ActionType.critical,
  278. );
  279. if (result != DialogUserChoice.firstChoice) {
  280. return false;
  281. }
  282. final dialog = createProgressDialog(context, "Please wait...");
  283. await dialog.show();
  284. try {
  285. await TrashSyncService.instance.emptyTrash();
  286. showShortToast(context, "Trash emptied");
  287. await dialog.hide();
  288. return true;
  289. } catch (e, s) {
  290. _logger.info("failed empty trash", e, s);
  291. await dialog.hide();
  292. await showGenericErrorDialog(context);
  293. return false;
  294. }
  295. }
  296. Future<bool> deleteLocalFiles(
  297. BuildContext context,
  298. List<String> localIDs,
  299. ) async {
  300. final List<String> deletedIDs = [];
  301. final List<String> localAssetIDs = [];
  302. final List<String> localSharedMediaIDs = [];
  303. for (String id in localIDs) {
  304. if (id.startsWith(kSharedMediaIdentifier)) {
  305. localSharedMediaIDs.add(id);
  306. } else {
  307. localAssetIDs.add(id);
  308. }
  309. }
  310. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  311. if (Platform.isAndroid) {
  312. final androidInfo = await DeviceInfoPlugin().androidInfo;
  313. if (androidInfo.version.sdkInt < kAndroid11SDKINT) {
  314. deletedIDs
  315. .addAll(await _deleteLocalFilesInBatches(context, localAssetIDs));
  316. } else {
  317. deletedIDs
  318. .addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  319. }
  320. } else {
  321. deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  322. }
  323. if (deletedIDs.isNotEmpty) {
  324. final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
  325. await FilesDB.instance.deleteLocalFiles(deletedIDs);
  326. _logger.info(deletedFiles.length.toString() + " files deleted locally");
  327. Bus.instance.fire(LocalPhotosUpdatedEvent(deletedFiles));
  328. return true;
  329. } else {
  330. showToast(context, "Could not free up space");
  331. return false;
  332. }
  333. }
  334. Future<List<String>> _deleteLocalFilesInOneShot(
  335. BuildContext context,
  336. List<String> localIDs,
  337. ) async {
  338. final List<String> deletedIDs = [];
  339. final dialog = createProgressDialog(
  340. context,
  341. "Deleting " + localIDs.length.toString() + " backed up files...",
  342. );
  343. await dialog.show();
  344. try {
  345. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
  346. } catch (e, s) {
  347. _logger.severe("Could not delete files ", e, s);
  348. }
  349. await dialog.hide();
  350. return deletedIDs;
  351. }
  352. Future<List<String>> _deleteLocalFilesInBatches(
  353. BuildContext context,
  354. List<String> localIDs,
  355. ) async {
  356. final dialogKey = GlobalKey<LinearProgressDialogState>();
  357. final dialog = LinearProgressDialog(
  358. "Deleting " + localIDs.length.toString() + " backed up files...",
  359. key: dialogKey,
  360. );
  361. showDialog(
  362. context: context,
  363. builder: (context) {
  364. return dialog;
  365. },
  366. barrierColor: Colors.black.withOpacity(0.85),
  367. );
  368. const minimumParts = 10;
  369. const minimumBatchSize = 1;
  370. const maximumBatchSize = 100;
  371. final batchSize = min(
  372. max(minimumBatchSize, (localIDs.length / minimumParts).round()),
  373. maximumBatchSize,
  374. );
  375. final List<String> deletedIDs = [];
  376. for (int index = 0; index < localIDs.length; index += batchSize) {
  377. if (dialogKey.currentState != null) {
  378. dialogKey.currentState.setProgress(index / localIDs.length);
  379. }
  380. final ids = localIDs
  381. .getRange(index, min(localIDs.length, index + batchSize))
  382. .toList();
  383. _logger.info("Trying to delete " + ids.toString());
  384. try {
  385. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
  386. _logger.info("Deleted " + ids.toString());
  387. } catch (e, s) {
  388. _logger.severe("Could not delete batch " + ids.toString(), e, s);
  389. for (final id in ids) {
  390. try {
  391. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
  392. _logger.info("Deleted " + id);
  393. } catch (e, s) {
  394. _logger.severe("Could not delete file " + id, e, s);
  395. }
  396. }
  397. }
  398. }
  399. Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
  400. return deletedIDs;
  401. }
  402. Future<bool> _localFileExist(File file) {
  403. if (file.isSharedMediaToAppSandbox()) {
  404. var localFile = io.File(getSharedMediaFilePath(file));
  405. return localFile.exists();
  406. } else {
  407. return file.getAsset().then((asset) {
  408. if (asset == null) {
  409. return false;
  410. }
  411. return asset.exists;
  412. });
  413. }
  414. }
  415. Future<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
  416. final List<String> actuallyDeletedIDs = [];
  417. try {
  418. return Future.forEach(localIDs, (id) async {
  419. String localPath = Configuration.instance.getSharedMediaCacheDirectory() +
  420. "/" +
  421. id.replaceAll(kSharedMediaIdentifier, '');
  422. try {
  423. // verify the file exists as the OS may have already deleted it from cache
  424. if (io.File(localPath).existsSync()) {
  425. await io.File(localPath).delete();
  426. }
  427. actuallyDeletedIDs.add(id);
  428. } catch (e, s) {
  429. _logger.warning("Could not delete file " + id, e, s);
  430. // server log shouldn't contain localId
  431. _logger.severe("Could not delete file ", e, s);
  432. }
  433. }).then((ignore) {
  434. return actuallyDeletedIDs;
  435. });
  436. } catch (e, s) {
  437. _logger.severe("Unexpected error while deleting share media files", e, s);
  438. return Future.value(actuallyDeletedIDs);
  439. }
  440. }
  441. Future<bool> shouldProceedWithDeletion(BuildContext context) async {
  442. final choice = await showChoiceDialog(
  443. context,
  444. "Are you sure?",
  445. "Some of the files you are trying to delete are only available on your device and cannot be recovered if deleted",
  446. firstAction: "Cancel",
  447. secondAction: "Delete",
  448. secondActionColor: Colors.red,
  449. );
  450. return choice == DialogUserChoice.secondChoice;
  451. }