delete_file_util.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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/models/trash_file.dart';
  19. import 'package:photos/models/trash_item_request.dart';
  20. import 'package:photos/services/remote_sync_service.dart';
  21. import 'package:photos/services/sync_service.dart';
  22. import 'package:photos/services/trash_sync_service.dart';
  23. import 'package:photos/ui/common/dialogs.dart';
  24. import 'package:photos/ui/linear_progress_dialog.dart';
  25. import 'package:photos/utils/dialog_util.dart';
  26. import 'package:photos/utils/toast_util.dart';
  27. import 'file_util.dart';
  28. final _logger = Logger("DeleteFileUtil");
  29. Future<void> deleteFilesFromEverywhere(
  30. BuildContext context, List<File> files) 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. for (final file in files) {
  38. if (file.localID != null) {
  39. if (!(await _localFileExist(file))) {
  40. _logger.warning("Already deleted " + file.toString());
  41. alreadyDeletedIDs.add(file.localID);
  42. } else if (file.isSharedMediaToAppSandbox()) {
  43. localSharedMediaIDs.add(file.localID);
  44. } else {
  45. localAssetIDs.add(file.localID);
  46. }
  47. }
  48. }
  49. Set<String> deletedIDs = <String>{};
  50. try {
  51. deletedIDs =
  52. (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  53. } catch (e, s) {
  54. _logger.severe("Could not delete file", e, s);
  55. }
  56. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  57. final updatedCollectionIDs = <int>{};
  58. final List<TrashRequest> uploadedFilesToBeTrashed = [];
  59. final List<File> deletedFiles = [];
  60. for (final file in files) {
  61. if (file.localID != null) {
  62. if (file.uploadedFileID != null && file.collectionID != null) {
  63. uploadedFilesToBeTrashed.add(TrashRequest(file.uploadedFileID, file.collectionID));
  64. updatedCollectionIDs.add(file.collectionID);
  65. } else {
  66. await FilesDB.instance.deleteLocalFile(file);
  67. }
  68. // Remove only those files that have already been removed from disk
  69. if (deletedIDs.contains(file.localID) ||
  70. alreadyDeletedIDs.contains(file.localID)) {
  71. deletedFiles.add(file);
  72. if (file.uploadedFileID != null && file.collectionID != null) {
  73. uploadedFilesToBeTrashed.add(TrashRequest(file.uploadedFileID, file.collectionID));
  74. updatedCollectionIDs.add(file.collectionID);
  75. } else {
  76. await FilesDB.instance.deleteLocalFile(file);
  77. }
  78. }
  79. } else {
  80. updatedCollectionIDs.add(file.collectionID);
  81. deletedFiles.add(file);
  82. uploadedFilesToBeTrashed.add(TrashRequest(file.uploadedFileID, file.collectionID));
  83. }
  84. }
  85. if (uploadedFilesToBeTrashed.isNotEmpty) {
  86. try {
  87. final fileIDs = uploadedFilesToBeTrashed.map((item) => item.fileID).toList();
  88. await TrashSyncService.instance.trashFilesOnServer(uploadedFilesToBeTrashed);
  89. // await SyncService.instance
  90. // .deleteFilesOnServer(fileIDs);
  91. await FilesDB.instance.deleteMultipleUploadedFiles(fileIDs);
  92. } catch (e) {
  93. _logger.severe(e);
  94. await dialog.hide();
  95. showGenericErrorDialog(context);
  96. rethrow;
  97. }
  98. for (final collectionID in updatedCollectionIDs) {
  99. Bus.instance.fire(CollectionUpdatedEvent(
  100. collectionID,
  101. deletedFiles
  102. .where((file) => file.collectionID == collectionID)
  103. .toList(),
  104. type: EventType.deleted,
  105. ));
  106. }
  107. }
  108. if (deletedFiles.isNotEmpty) {
  109. Bus.instance
  110. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  111. }
  112. await dialog.hide();
  113. showToast("deleted from everywhere");
  114. if (uploadedFilesToBeTrashed.isNotEmpty) {
  115. RemoteSyncService.instance.sync(silently: true);
  116. }
  117. }
  118. Future<void> deleteFilesFromRemoteOnly(
  119. BuildContext context, List<File> files) async {
  120. files.removeWhere((element) => element.uploadedFileID == null);
  121. if(files.isEmpty) {
  122. showToast("selected files are not uploaded on ente");
  123. return;
  124. }
  125. final dialog = createProgressDialog(context, "deleting...");
  126. await dialog.show();
  127. _logger.info("Trying to delete files " +
  128. files.map((f) => f.uploadedFileID).toString());
  129. final updatedCollectionIDs = <int>{};
  130. final List<int> uploadedFileIDs = [];
  131. final List<TrashRequest> trashRequests = [];
  132. for (final file in files) {
  133. updatedCollectionIDs.add(file.collectionID);
  134. uploadedFileIDs.add(file.uploadedFileID);
  135. trashRequests.add(TrashRequest(file.uploadedFileID, file.collectionID));
  136. }
  137. try {
  138. await TrashSyncService.instance.trashFilesOnServer(trashRequests);
  139. await FilesDB.instance.deleteMultipleUploadedFiles(uploadedFileIDs);
  140. } catch (e, s) {
  141. _logger.severe("Failed to delete files from remote", e, s);
  142. await dialog.hide();
  143. showGenericErrorDialog(context);
  144. rethrow;
  145. }
  146. for (final collectionID in updatedCollectionIDs) {
  147. Bus.instance.fire(CollectionUpdatedEvent(
  148. collectionID,
  149. files.where((file) => file.collectionID == collectionID).toList(),
  150. type: EventType.deleted,
  151. ));
  152. }
  153. Bus.instance
  154. .fire(LocalPhotosUpdatedEvent(files, type: EventType.deleted));
  155. await dialog.hide();
  156. RemoteSyncService.instance.sync(silently: true);
  157. }
  158. Future<void> deleteFilesOnDeviceOnly(
  159. BuildContext context, List<File> files) async {
  160. final dialog = createProgressDialog(context, "deleting...");
  161. await dialog.show();
  162. _logger.info("Trying to delete files " + files.toString());
  163. final List<String> localAssetIDs = [];
  164. final List<String> localSharedMediaIDs = [];
  165. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  166. for (final file in files) {
  167. if (file.localID != null) {
  168. if (!(await _localFileExist(file))) {
  169. _logger.warning("Already deleted " + file.toString());
  170. alreadyDeletedIDs.add(file.localID);
  171. } else if (file.isSharedMediaToAppSandbox()) {
  172. localSharedMediaIDs.add(file.localID);
  173. } else {
  174. localAssetIDs.add(file.localID);
  175. }
  176. }
  177. }
  178. Set<String> deletedIDs = <String>{};
  179. try {
  180. deletedIDs =
  181. (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  182. } catch (e, s) {
  183. _logger.severe("Could not delete file", e, s);
  184. }
  185. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  186. final List<File> deletedFiles = [];
  187. for (final file in files) {
  188. // Remove only those files that have been removed from disk
  189. if (deletedIDs.contains(file.localID) ||
  190. alreadyDeletedIDs.contains(file.localID)) {
  191. deletedFiles.add(file);
  192. file.localID = null;
  193. FilesDB.instance.update(file);
  194. }
  195. }
  196. if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
  197. Bus.instance
  198. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  199. }
  200. await dialog.hide();
  201. }
  202. Future<bool> deleteFromTrash(
  203. BuildContext context, List<File> files) async {
  204. final result = await showChoiceDialog(context, "delete permanently?",
  205. "the files will be permanently removed from your ente account",
  206. firstAction: "delete", actionType: ActionType.critical);
  207. if (result != DialogUserChoice.firstChoice) {
  208. return false;
  209. }
  210. final dialog = createProgressDialog(context, "permanently deleting...");
  211. await dialog.show();
  212. try {
  213. await TrashSyncService.instance.deleteFromTrash(files);
  214. showToast("successfully deleted");
  215. await dialog.hide();
  216. Bus.instance.fire(FilesUpdatedEvent(files, type: EventType.deleted));
  217. return true;
  218. } catch (e, s) {
  219. _logger.info("failed to delete from trash", e, s);
  220. await dialog.hide();
  221. await showGenericErrorDialog(context);
  222. return false;
  223. }
  224. }
  225. Future<bool> deleteLocalFiles(
  226. BuildContext context, List<String> localIDs) async {
  227. final List<String> deletedIDs = [];
  228. final List<String> localAssetIDs = [];
  229. final List<String> localSharedMediaIDs = [];
  230. for (String id in localIDs) {
  231. if (id.startsWith(kSharedMediaIdentifier)) {
  232. localSharedMediaIDs.add(id);
  233. } else {
  234. localAssetIDs.add(id);
  235. }
  236. }
  237. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  238. if (Platform.isAndroid) {
  239. final androidInfo = await DeviceInfoPlugin().androidInfo;
  240. if (androidInfo.version.sdkInt < kAndroid11SDKINT) {
  241. deletedIDs
  242. .addAll(await _deleteLocalFilesInBatches(context, localAssetIDs));
  243. } else {
  244. deletedIDs
  245. .addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  246. }
  247. } else {
  248. deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  249. }
  250. if (deletedIDs.isNotEmpty) {
  251. final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
  252. await FilesDB.instance.deleteLocalFiles(deletedIDs);
  253. _logger.info(deletedFiles.length.toString() + " files deleted locally");
  254. Bus.instance.fire(LocalPhotosUpdatedEvent(deletedFiles));
  255. return true;
  256. } else {
  257. showToast("could not free up space");
  258. return false;
  259. }
  260. }
  261. Future<List<String>> _deleteLocalFilesInOneShot(
  262. BuildContext context, List<String> localIDs) async {
  263. final List<String> deletedIDs = [];
  264. final dialog = createProgressDialog(context,
  265. "deleting " + localIDs.length.toString() + " backed up files...");
  266. await dialog.show();
  267. try {
  268. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
  269. } catch (e, s) {
  270. _logger.severe("Could not delete files ", e, s);
  271. }
  272. await dialog.hide();
  273. return deletedIDs;
  274. }
  275. Future<List<String>> _deleteLocalFilesInBatches(
  276. BuildContext context, List<String> localIDs) async {
  277. final dialogKey = GlobalKey<LinearProgressDialogState>();
  278. final dialog = LinearProgressDialog(
  279. "deleting " + localIDs.length.toString() + " backed up files...",
  280. key: dialogKey,
  281. );
  282. showDialog(
  283. context: context,
  284. builder: (context) {
  285. return dialog;
  286. },
  287. barrierColor: Colors.black.withOpacity(0.85),
  288. );
  289. const minimumParts = 10;
  290. const minimumBatchSize = 1;
  291. const maximumBatchSize = 100;
  292. final batchSize = min(
  293. max(minimumBatchSize, (localIDs.length / minimumParts).round()),
  294. maximumBatchSize);
  295. final List<String> deletedIDs = [];
  296. for (int index = 0; index < localIDs.length; index += batchSize) {
  297. if (dialogKey.currentState != null) {
  298. dialogKey.currentState.setProgress(index / localIDs.length);
  299. }
  300. final ids = localIDs
  301. .getRange(index, min(localIDs.length, index + batchSize))
  302. .toList();
  303. _logger.info("Trying to delete " + ids.toString());
  304. try {
  305. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
  306. _logger.info("Deleted " + ids.toString());
  307. } catch (e, s) {
  308. _logger.severe("Could not delete batch " + ids.toString(), e, s);
  309. for (final id in ids) {
  310. try {
  311. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
  312. _logger.info("Deleted " + id);
  313. } catch (e, s) {
  314. _logger.severe("Could not delete file " + id, e, s);
  315. }
  316. }
  317. }
  318. }
  319. Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
  320. return deletedIDs;
  321. }
  322. Future<bool> _localFileExist(File file) {
  323. if (file.isSharedMediaToAppSandbox()) {
  324. var localFile = io.File(getSharedMediaFilePath(file));
  325. return localFile.exists();
  326. } else {
  327. return file.getAsset().then((asset) {
  328. if (asset == null) {
  329. return false;
  330. }
  331. return asset.exists;
  332. });
  333. }
  334. }
  335. Future<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
  336. final List<String> actuallyDeletedIDs = [];
  337. try {
  338. return Future.forEach(localIDs, (id) async {
  339. String localPath = Configuration.instance.getSharedMediaCacheDirectory() +
  340. "/" +
  341. id.replaceAll(kSharedMediaIdentifier, '');
  342. try {
  343. // verify the file exists as the OS may have already deleted it from cache
  344. if (io.File(localPath).existsSync()) {
  345. await io.File(localPath).delete();
  346. }
  347. actuallyDeletedIDs.add(id);
  348. } catch (e, s) {
  349. _logger.warning("Could not delete file " + id, e, s);
  350. // server log shouldn't contain localId
  351. _logger.severe("Could not delete file ", e, s);
  352. }
  353. }).then((ignore) {
  354. return actuallyDeletedIDs;
  355. });
  356. } catch (e, s) {
  357. _logger.severe("Unexpected error while deleting share media files", e, s);
  358. return Future.value(actuallyDeletedIDs);
  359. }
  360. }