file_magic_service.dart 7.8 KB

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