delete_file_util.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. final dialog = createProgressDialog(context, "deleting...");
  121. await dialog.show();
  122. _logger.info("Trying to delete files " +
  123. files.map((f) => f.uploadedFileID).toString());
  124. final updatedCollectionIDs = <int>{};
  125. final List<int> ids = [];
  126. for (final file in files) {
  127. updatedCollectionIDs.add(file.collectionID);
  128. ids.add(file.uploadedFileID);
  129. }
  130. try {
  131. await SyncService.instance.deleteFilesOnServer(ids);
  132. await FilesDB.instance.deleteMultipleUploadedFiles(ids);
  133. } catch (e, s) {
  134. _logger.severe("Failed to delete files from remote", e, s);
  135. await dialog.hide();
  136. showGenericErrorDialog(context);
  137. rethrow;
  138. }
  139. for (final collectionID in updatedCollectionIDs) {
  140. Bus.instance.fire(CollectionUpdatedEvent(
  141. collectionID,
  142. files.where((file) => file.collectionID == collectionID).toList(),
  143. type: EventType.deleted,
  144. ));
  145. }
  146. await dialog.hide();
  147. RemoteSyncService.instance.sync(silently: true);
  148. }
  149. Future<void> deleteFilesOnDeviceOnly(
  150. BuildContext context, List<File> files) async {
  151. final dialog = createProgressDialog(context, "deleting...");
  152. await dialog.show();
  153. _logger.info("Trying to delete files " + files.toString());
  154. final List<String> localAssetIDs = [];
  155. final List<String> localSharedMediaIDs = [];
  156. final List<String> alreadyDeletedIDs = []; // to ignore already deleted files
  157. for (final file in files) {
  158. if (file.localID != null) {
  159. if (!(await _localFileExist(file))) {
  160. _logger.warning("Already deleted " + file.toString());
  161. alreadyDeletedIDs.add(file.localID);
  162. } else if (file.isSharedMediaToAppSandbox()) {
  163. localSharedMediaIDs.add(file.localID);
  164. } else {
  165. localAssetIDs.add(file.localID);
  166. }
  167. }
  168. }
  169. Set<String> deletedIDs = <String>{};
  170. try {
  171. deletedIDs =
  172. (await PhotoManager.editor.deleteWithIds(localAssetIDs)).toSet();
  173. } catch (e, s) {
  174. _logger.severe("Could not delete file", e, s);
  175. }
  176. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  177. final List<File> deletedFiles = [];
  178. for (final file in files) {
  179. // Remove only those files that have been removed from disk
  180. if (deletedIDs.contains(file.localID) ||
  181. alreadyDeletedIDs.contains(file.localID)) {
  182. deletedFiles.add(file);
  183. file.localID = null;
  184. FilesDB.instance.update(file);
  185. }
  186. }
  187. if (deletedFiles.isNotEmpty || alreadyDeletedIDs.isNotEmpty) {
  188. Bus.instance
  189. .fire(LocalPhotosUpdatedEvent(deletedFiles, type: EventType.deleted));
  190. }
  191. await dialog.hide();
  192. }
  193. Future<bool> deleteFromTrash(
  194. BuildContext context, List<TrashFile> files) async {
  195. final result = await showChoiceDialog(context, "delete permanently?",
  196. "the files will be permanently removed from your ente account",
  197. firstAction: "delete", actionType: ActionType.critical);
  198. if (result != DialogUserChoice.firstChoice) {
  199. return false;
  200. }
  201. final dialog = createProgressDialog(context, "permanently deleting...");
  202. await dialog.show();
  203. try {
  204. await TrashSyncService.instance.deleteFromTrash(files);
  205. showToast("successfully deleted");
  206. await dialog.hide();
  207. Bus.instance.fire(FilesUpdatedEvent(files, type: EventType.deleted));
  208. return true;
  209. } catch (e, s) {
  210. _logger.info("failed to delete from trash", e, s);
  211. await dialog.hide();
  212. await showGenericErrorDialog(context);
  213. return false;
  214. }
  215. }
  216. Future<bool> deleteLocalFiles(
  217. BuildContext context, List<String> localIDs) async {
  218. final List<String> deletedIDs = [];
  219. final List<String> localAssetIDs = [];
  220. final List<String> localSharedMediaIDs = [];
  221. for (String id in localIDs) {
  222. if (id.startsWith(kSharedMediaIdentifier)) {
  223. localSharedMediaIDs.add(id);
  224. } else {
  225. localAssetIDs.add(id);
  226. }
  227. }
  228. deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs));
  229. if (Platform.isAndroid) {
  230. final androidInfo = await DeviceInfoPlugin().androidInfo;
  231. if (androidInfo.version.sdkInt < kAndroid11SDKINT) {
  232. deletedIDs
  233. .addAll(await _deleteLocalFilesInBatches(context, localAssetIDs));
  234. } else {
  235. deletedIDs
  236. .addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  237. }
  238. } else {
  239. deletedIDs.addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs));
  240. }
  241. if (deletedIDs.isNotEmpty) {
  242. final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs);
  243. await FilesDB.instance.deleteLocalFiles(deletedIDs);
  244. _logger.info(deletedFiles.length.toString() + " files deleted locally");
  245. Bus.instance.fire(LocalPhotosUpdatedEvent(deletedFiles));
  246. return true;
  247. } else {
  248. showToast("could not free up space");
  249. return false;
  250. }
  251. }
  252. Future<List<String>> _deleteLocalFilesInOneShot(
  253. BuildContext context, List<String> localIDs) async {
  254. final List<String> deletedIDs = [];
  255. final dialog = createProgressDialog(context,
  256. "deleting " + localIDs.length.toString() + " backed up files...");
  257. await dialog.show();
  258. try {
  259. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(localIDs));
  260. } catch (e, s) {
  261. _logger.severe("Could not delete files ", e, s);
  262. }
  263. await dialog.hide();
  264. return deletedIDs;
  265. }
  266. Future<List<String>> _deleteLocalFilesInBatches(
  267. BuildContext context, List<String> localIDs) async {
  268. final dialogKey = GlobalKey<LinearProgressDialogState>();
  269. final dialog = LinearProgressDialog(
  270. "deleting " + localIDs.length.toString() + " backed up files...",
  271. key: dialogKey,
  272. );
  273. showDialog(
  274. context: context,
  275. builder: (context) {
  276. return dialog;
  277. },
  278. barrierColor: Colors.black.withOpacity(0.85),
  279. );
  280. const minimumParts = 10;
  281. const minimumBatchSize = 1;
  282. const maximumBatchSize = 100;
  283. final batchSize = min(
  284. max(minimumBatchSize, (localIDs.length / minimumParts).round()),
  285. maximumBatchSize);
  286. final List<String> deletedIDs = [];
  287. for (int index = 0; index < localIDs.length; index += batchSize) {
  288. if (dialogKey.currentState != null) {
  289. dialogKey.currentState.setProgress(index / localIDs.length);
  290. }
  291. final ids = localIDs
  292. .getRange(index, min(localIDs.length, index + batchSize))
  293. .toList();
  294. _logger.info("Trying to delete " + ids.toString());
  295. try {
  296. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds(ids));
  297. _logger.info("Deleted " + ids.toString());
  298. } catch (e, s) {
  299. _logger.severe("Could not delete batch " + ids.toString(), e, s);
  300. for (final id in ids) {
  301. try {
  302. deletedIDs.addAll(await PhotoManager.editor.deleteWithIds([id]));
  303. _logger.info("Deleted " + id);
  304. } catch (e, s) {
  305. _logger.severe("Could not delete file " + id, e, s);
  306. }
  307. }
  308. }
  309. }
  310. Navigator.of(dialogKey.currentContext, rootNavigator: true).pop('dialog');
  311. return deletedIDs;
  312. }
  313. Future<bool> _localFileExist(File file) {
  314. if (file.isSharedMediaToAppSandbox()) {
  315. var localFile = io.File(getSharedMediaFilePath(file));
  316. return localFile.exists();
  317. } else {
  318. return file.getAsset().then((asset) {
  319. if (asset == null) {
  320. return false;
  321. }
  322. return asset.exists;
  323. });
  324. }
  325. }
  326. Future<List<String>> _tryDeleteSharedMediaFiles(List<String> localIDs) {
  327. final List<String> actuallyDeletedIDs = [];
  328. try {
  329. return Future.forEach(localIDs, (id) async {
  330. String localPath = Configuration.instance.getSharedMediaCacheDirectory() +
  331. "/" +
  332. id.replaceAll(kSharedMediaIdentifier, '');
  333. try {
  334. // verify the file exists as the OS may have already deleted it from cache
  335. if (io.File(localPath).existsSync()) {
  336. await io.File(localPath).delete();
  337. }
  338. actuallyDeletedIDs.add(id);
  339. } catch (e, s) {
  340. _logger.warning("Could not delete file " + id, e, s);
  341. // server log shouldn't contain localId
  342. _logger.severe("Could not delete file ", e, s);
  343. }
  344. }).then((ignore) {
  345. return actuallyDeletedIDs;
  346. });
  347. } catch (e, s) {
  348. _logger.severe("Unexpected error while deleting share media files", e, s);
  349. return Future.value(actuallyDeletedIDs);
  350. }
  351. }