file_magic_service.dart 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:dio/dio.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import 'package:photos/core/constants.dart';
  7. import 'package:photos/core/event_bus.dart';
  8. import 'package:photos/core/network/network.dart';
  9. import 'package:photos/db/files_db.dart';
  10. import 'package:photos/events/files_updated_event.dart';
  11. import 'package:photos/events/force_reload_home_gallery_event.dart';
  12. import 'package:photos/events/local_photos_updated_event.dart';
  13. import 'package:photos/extensions/list.dart';
  14. import 'package:photos/models/file.dart';
  15. import "package:photos/models/metadata/common_keys.dart";
  16. import "package:photos/models/metadata/file_magic.dart";
  17. import 'package:photos/services/remote_sync_service.dart';
  18. import 'package:photos/utils/crypto_util.dart';
  19. import 'package:photos/utils/file_download_util.dart';
  20. class FileMagicService {
  21. final _logger = Logger("FileMagicService");
  22. late Dio _enteDio;
  23. late FilesDB _filesDB;
  24. FileMagicService._privateConstructor() {
  25. _filesDB = FilesDB.instance;
  26. _enteDio = NetworkClient.instance.enteDio;
  27. }
  28. static final FileMagicService instance =
  29. FileMagicService._privateConstructor();
  30. Future<void> changeVisibility(List<File> files, int visibility) async {
  31. final Map<String, dynamic> update = {magicKeyVisibility: visibility};
  32. await _updateMagicData(files, update);
  33. if (visibility == visibleVisibility) {
  34. // Force reload home gallery to pull in the now unarchived files
  35. Bus.instance.fire(ForceReloadHomeGalleryEvent("unarchivedFiles"));
  36. Bus.instance.fire(
  37. LocalPhotosUpdatedEvent(
  38. files,
  39. type: EventType.unarchived,
  40. source: "vizChange",
  41. ),
  42. );
  43. } else {
  44. Bus.instance.fire(
  45. LocalPhotosUpdatedEvent(
  46. files,
  47. type: EventType.archived,
  48. source: "vizChange",
  49. ),
  50. );
  51. }
  52. }
  53. Future<void> updatePublicMagicMetadata(
  54. List<File> files,
  55. Map<String, dynamic>? newMetadataUpdate, {
  56. Map<int, Map<String, dynamic>>? metadataUpdateMap,
  57. }) async {
  58. final params = <String, dynamic>{};
  59. params['metadataList'] = [];
  60. final int ownerID = Configuration.instance.getUserID()!;
  61. try {
  62. for (final file in files) {
  63. if (file.uploadedFileID == null) {
  64. throw AssertionError(
  65. "operation is only supported on backed up files",
  66. );
  67. } else if (file.ownerID != ownerID) {
  68. throw AssertionError("cannot modify memories not owned by you");
  69. }
  70. // read the existing magic metadata and apply new updates to existing data
  71. // current update is simple replace. This will be enhanced in the future,
  72. // as required.
  73. final newUpdates = metadataUpdateMap != null
  74. ? metadataUpdateMap[file.uploadedFileID]
  75. : newMetadataUpdate;
  76. assert(
  77. newUpdates != null && newUpdates.isNotEmpty,
  78. "can not apply empty updates",
  79. );
  80. final Map<String, dynamic> jsonToUpdate =
  81. jsonDecode(file.pubMmdEncodedJson ?? '{}');
  82. newUpdates!.forEach((key, value) {
  83. jsonToUpdate[key] = value;
  84. });
  85. // update the local information so that it's reflected on UI
  86. file.pubMmdEncodedJson = jsonEncode(jsonToUpdate);
  87. file.pubMagicMetadata = PubMagicMetadata.fromJson(jsonToUpdate);
  88. final fileKey = getFileKey(file);
  89. final encryptedMMd = await CryptoUtil.encryptChaCha(
  90. utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
  91. fileKey,
  92. );
  93. params['metadataList'].add(
  94. UpdateMagicMetadataRequest(
  95. id: file.uploadedFileID!,
  96. magicMetadata: MetadataRequest(
  97. version: file.pubMmdVersion,
  98. count: jsonToUpdate.length,
  99. data: CryptoUtil.bin2base64(encryptedMMd.encryptedData!),
  100. header: CryptoUtil.bin2base64(encryptedMMd.header!),
  101. ),
  102. ),
  103. );
  104. file.pubMmdVersion = file.pubMmdVersion + 1;
  105. }
  106. await _enteDio.put("/files/public-magic-metadata", data: params);
  107. // update the state of the selected file. Same file in other collection
  108. // should be eventually synced after remote sync has completed
  109. await _filesDB.insertMultiple(files);
  110. RemoteSyncService.instance.sync(silently: true).ignore();
  111. } on DioError catch (e) {
  112. if (e.response != null && e.response!.statusCode == 409) {
  113. RemoteSyncService.instance.sync(silently: true).ignore();
  114. }
  115. rethrow;
  116. } catch (e, s) {
  117. _logger.severe("failed to sync magic metadata", e, s);
  118. rethrow;
  119. }
  120. }
  121. Future<void> _updateMagicData(
  122. List<File> files,
  123. Map<String, dynamic> newMetadataUpdate,
  124. ) async {
  125. final params = <String, dynamic>{};
  126. final int ownerID = Configuration.instance.getUserID()!;
  127. final batchedFiles = files.chunks(batchSize);
  128. try {
  129. for (final batch in batchedFiles) {
  130. params['metadataList'] = [];
  131. for (final file in batch) {
  132. if (file.uploadedFileID == null) {
  133. throw AssertionError(
  134. "operation is only supported on backed up files",
  135. );
  136. } else if (file.ownerID != ownerID) {
  137. throw AssertionError("cannot modify memories not owned by you");
  138. }
  139. // read the existing magic metadata and apply new updates to existing data
  140. // current update is simple replace. This will be enhanced in the future,
  141. // as required.
  142. final Map<String, dynamic> jsonToUpdate =
  143. jsonDecode(file.mMdEncodedJson ?? '{}');
  144. newMetadataUpdate.forEach((key, value) {
  145. jsonToUpdate[key] = value;
  146. });
  147. // update the local information so that it's reflected on UI
  148. file.mMdEncodedJson = jsonEncode(jsonToUpdate);
  149. file.magicMetadata = MagicMetadata.fromJson(jsonToUpdate);
  150. final fileKey = getFileKey(file);
  151. final encryptedMMd = await CryptoUtil.encryptChaCha(
  152. utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
  153. fileKey,
  154. );
  155. params['metadataList'].add(
  156. UpdateMagicMetadataRequest(
  157. id: file.uploadedFileID!,
  158. magicMetadata: MetadataRequest(
  159. version: file.mMdVersion,
  160. count: jsonToUpdate.length,
  161. data: CryptoUtil.bin2base64(encryptedMMd.encryptedData!),
  162. header: CryptoUtil.bin2base64(encryptedMMd.header!),
  163. ),
  164. ),
  165. );
  166. file.mMdVersion = file.mMdVersion + 1;
  167. }
  168. await _enteDio.put("/files/magic-metadata", data: params);
  169. await _filesDB.insertMultiple(files);
  170. }
  171. // update the state of the selected file. Same file in other collection
  172. // should be eventually synced after remote sync has completed
  173. RemoteSyncService.instance.sync(silently: true).ignore();
  174. } on DioError catch (e) {
  175. if (e.response != null && e.response!.statusCode == 409) {
  176. RemoteSyncService.instance.sync(silently: true).ignore();
  177. }
  178. rethrow;
  179. } catch (e, s) {
  180. _logger.severe("failed to sync magic metadata", e, s);
  181. rethrow;
  182. }
  183. }
  184. }
  185. class UpdateMagicMetadataRequest {
  186. final int id;
  187. final MetadataRequest? magicMetadata;
  188. UpdateMagicMetadataRequest({required this.id, required this.magicMetadata});
  189. factory UpdateMagicMetadataRequest.fromJson(dynamic json) {
  190. return UpdateMagicMetadataRequest(
  191. id: json['id'],
  192. magicMetadata: json['magicMetadata'] != null
  193. ? MetadataRequest.fromJson(json['magicMetadata'])
  194. : null,
  195. );
  196. }
  197. Map<String, dynamic> toJson() {
  198. final map = <String, dynamic>{};
  199. map['id'] = id;
  200. if (magicMetadata != null) {
  201. map['magicMetadata'] = magicMetadata!.toJson();
  202. }
  203. return map;
  204. }
  205. }
  206. class MetadataRequest {
  207. int? version;
  208. int? count;
  209. String? data;
  210. String? header;
  211. MetadataRequest({
  212. required this.version,
  213. required this.count,
  214. required this.data,
  215. required this.header,
  216. });
  217. MetadataRequest.fromJson(dynamic json) {
  218. version = json['version'];
  219. count = json['count'];
  220. data = json['data'];
  221. header = json['header'];
  222. }
  223. Map<String, dynamic> toJson() {
  224. final map = <String, dynamic>{};
  225. map['version'] = version;
  226. map['count'] = count;
  227. map['data'] = data;
  228. map['header'] = header;
  229. return map;
  230. }
  231. }