ente/lib/services/file_magic_service.dart

254 lines
8.4 KiB
Dart
Raw Normal View History

import 'dart:convert';
2022-12-30 08:27:18 +00:00
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
2021-10-26 14:46:58 +00:00
import 'package:photos/core/configuration.dart';
2022-11-18 06:05:46 +00:00
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
2023-02-03 07:39:04 +00:00
import 'package:photos/core/network/network.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/files_updated_event.dart';
2021-10-26 14:46:58 +00:00
import 'package:photos/events/force_reload_home_gallery_event.dart';
2021-09-23 11:14:15 +00:00
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/extensions/list.dart';
import 'package:photos/models/file.dart';
import "package:photos/models/metadata/common_keys.dart";
import "package:photos/models/metadata/file_magic.dart";
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_download_util.dart';
2021-09-18 03:43:13 +00:00
class FileMagicService {
final _logger = Logger("FileMagicService");
2022-12-30 08:27:18 +00:00
late Dio _enteDio;
late FilesDB _filesDB;
2021-09-18 03:43:13 +00:00
FileMagicService._privateConstructor() {
_filesDB = FilesDB.instance;
2023-02-03 07:39:04 +00:00
_enteDio = NetworkClient.instance.enteDio;
2021-09-18 03:43:13 +00:00
}
2021-09-18 03:43:13 +00:00
static final FileMagicService instance =
FileMagicService._privateConstructor();
Future<void> changeVisibility(List<File> files, int visibility) async {
final Map<String, dynamic> update = {magicKeyVisibility: visibility};
2021-09-23 11:14:15 +00:00
await _updateMagicData(files, update);
if (visibility == visibleVisibility) {
2021-09-23 11:14:15 +00:00
// Force reload home gallery to pull in the now unarchived files
2022-11-11 10:23:35 +00:00
Bus.instance.fire(ForceReloadHomeGalleryEvent("unarchivedFiles"));
2022-11-11 13:09:22 +00:00
Bus.instance.fire(
LocalPhotosUpdatedEvent(
files,
type: EventType.unarchived,
source: "vizChange",
),
);
} else {
2022-11-11 13:09:22 +00:00
Bus.instance.fire(
LocalPhotosUpdatedEvent(
files,
type: EventType.archived,
source: "vizChange",
),
);
2021-09-23 11:14:15 +00:00
}
2021-09-18 03:54:32 +00:00
}
2021-10-26 14:46:58 +00:00
Future<void> updatePublicMagicMetadata(
2022-06-11 08:23:52 +00:00
List<File> files,
2022-12-30 08:27:18 +00:00
Map<String, dynamic>? newMetadataUpdate, {
Map<int, Map<String, dynamic>>? metadataUpdateMap,
}) async {
2021-10-26 10:37:14 +00:00
final params = <String, dynamic>{};
params['metadataList'] = [];
2022-12-30 08:27:18 +00:00
final int ownerID = Configuration.instance.getUserID()!;
2021-10-26 10:37:14 +00:00
try {
for (final file in files) {
if (file.uploadedFileID == null) {
throw AssertionError(
2022-06-11 08:23:52 +00:00
"operation is only supported on backed up files",
);
2021-10-26 10:37:14 +00:00
} else if (file.ownerID != ownerID) {
throw AssertionError("cannot modify memories not owned by you");
}
// read the existing magic metadata and apply new updates to existing data
// current update is simple replace. This will be enhanced in the future,
// as required.
final newUpdates = metadataUpdateMap != null
? metadataUpdateMap[file.uploadedFileID]
: newMetadataUpdate;
assert(
2022-11-14 11:26:32 +00:00
newUpdates != null && newUpdates.isNotEmpty,
"can not apply empty updates",
);
final Map<String, dynamic> jsonToUpdate =
2022-12-30 08:27:18 +00:00
jsonDecode(file.pubMmdEncodedJson ?? '{}');
newUpdates!.forEach((key, value) {
2021-10-26 10:37:14 +00:00
jsonToUpdate[key] = value;
});
// update the local information so that it's reflected on UI
file.pubMmdEncodedJson = jsonEncode(jsonToUpdate);
file.pubMagicMetadata = PubMagicMetadata.fromJson(jsonToUpdate);
2023-02-27 10:44:27 +00:00
final fileKey = getFileKey(file);
2021-10-26 10:37:14 +00:00
final encryptedMMd = await CryptoUtil.encryptChaCha(
2022-12-30 08:27:18 +00:00
utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
2022-06-11 08:23:52 +00:00
fileKey,
);
params['metadataList'].add(
UpdateMagicMetadataRequest(
2022-12-30 08:27:18 +00:00
id: file.uploadedFileID!,
2021-10-26 10:37:14 +00:00
magicMetadata: MetadataRequest(
version: file.pubMmdVersion,
count: jsonToUpdate.length,
2023-02-03 04:41:45 +00:00
data: CryptoUtil.bin2base64(encryptedMMd.encryptedData!),
header: CryptoUtil.bin2base64(encryptedMMd.header!),
2022-06-11 08:23:52 +00:00
),
),
);
2021-10-26 10:37:14 +00:00
file.pubMmdVersion = file.pubMmdVersion + 1;
}
2022-10-14 15:03:55 +00:00
await _enteDio.put("/files/public-magic-metadata", data: params);
2021-10-26 10:37:14 +00:00
// update the state of the selected file. Same file in other collection
// should be eventually synced after remote sync has completed
await _filesDB.insertMultiple(files);
2023-01-11 07:05:40 +00:00
RemoteSyncService.instance.sync(silently: true).ignore();
2021-10-26 10:37:14 +00:00
} on DioError catch (e) {
2022-12-30 08:27:18 +00:00
if (e.response != null && e.response!.statusCode == 409) {
2023-01-11 07:05:40 +00:00
RemoteSyncService.instance.sync(silently: true).ignore();
2021-10-26 10:37:14 +00:00
}
rethrow;
} catch (e, s) {
_logger.severe("failed to sync magic metadata", e, s);
rethrow;
}
}
2021-10-26 14:46:58 +00:00
2021-09-18 03:54:32 +00:00
Future<void> _updateMagicData(
2022-06-11 08:23:52 +00:00
List<File> files,
Map<String, dynamic> newMetadataUpdate,
) async {
2021-09-18 03:43:13 +00:00
final params = <String, dynamic>{};
2022-12-30 08:27:18 +00:00
final int ownerID = Configuration.instance.getUserID()!;
2022-11-18 06:05:46 +00:00
final batchedFiles = files.chunks(batchSize);
try {
for (final batch in batchedFiles) {
2022-11-18 07:22:34 +00:00
params['metadataList'] = [];
for (final file in batch) {
if (file.uploadedFileID == null) {
throw AssertionError(
"operation is only supported on backed up files",
);
} else if (file.ownerID != ownerID) {
throw AssertionError("cannot modify memories not owned by you");
}
// read the existing magic metadata and apply new updates to existing data
// current update is simple replace. This will be enhanced in the future,
// as required.
final Map<String, dynamic> jsonToUpdate =
2022-12-30 08:27:18 +00:00
jsonDecode(file.mMdEncodedJson ?? '{}');
newMetadataUpdate.forEach((key, value) {
jsonToUpdate[key] = value;
});
// update the local information so that it's reflected on UI
file.mMdEncodedJson = jsonEncode(jsonToUpdate);
file.magicMetadata = MagicMetadata.fromJson(jsonToUpdate);
2023-02-27 10:44:27 +00:00
final fileKey = getFileKey(file);
final encryptedMMd = await CryptoUtil.encryptChaCha(
2022-12-30 08:27:18 +00:00
utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
fileKey,
2022-06-11 08:23:52 +00:00
);
params['metadataList'].add(
UpdateMagicMetadataRequest(
2022-12-30 08:27:18 +00:00
id: file.uploadedFileID!,
magicMetadata: MetadataRequest(
version: file.mMdVersion,
count: jsonToUpdate.length,
2023-02-03 04:41:45 +00:00
data: CryptoUtil.bin2base64(encryptedMMd.encryptedData!),
header: CryptoUtil.bin2base64(encryptedMMd.header!),
),
),
);
file.mMdVersion = file.mMdVersion + 1;
}
2021-09-18 03:43:13 +00:00
await _enteDio.put("/files/magic-metadata", data: params);
await _filesDB.insertMultiple(files);
}
2021-09-18 03:54:32 +00:00
2021-09-18 03:43:13 +00:00
// update the state of the selected file. Same file in other collection
// should be eventually synced after remote sync has completed
2023-01-11 07:05:40 +00:00
RemoteSyncService.instance.sync(silently: true).ignore();
} on DioError catch (e) {
2022-12-30 08:27:18 +00:00
if (e.response != null && e.response!.statusCode == 409) {
2023-01-11 07:05:40 +00:00
RemoteSyncService.instance.sync(silently: true).ignore();
}
rethrow;
} catch (e, s) {
_logger.severe("failed to sync magic metadata", e, s);
rethrow;
}
}
}
class UpdateMagicMetadataRequest {
final int id;
2022-12-30 08:27:18 +00:00
final MetadataRequest? magicMetadata;
2022-12-30 08:27:18 +00:00
UpdateMagicMetadataRequest({required this.id, required this.magicMetadata});
factory UpdateMagicMetadataRequest.fromJson(dynamic json) {
return UpdateMagicMetadataRequest(
2022-06-11 08:23:52 +00:00
id: json['id'],
magicMetadata: json['magicMetadata'] != null
? MetadataRequest.fromJson(json['magicMetadata'])
: null,
);
}
Map<String, dynamic> toJson() {
2021-09-20 06:41:38 +00:00
final map = <String, dynamic>{};
map['id'] = id;
2021-09-18 03:43:13 +00:00
if (magicMetadata != null) {
2022-12-30 08:27:18 +00:00
map['magicMetadata'] = magicMetadata!.toJson();
}
return map;
}
}
class MetadataRequest {
2022-12-30 08:27:18 +00:00
int? version;
int? count;
String? data;
String? header;
MetadataRequest({
required this.version,
required this.count,
required this.data,
required this.header,
});
MetadataRequest.fromJson(dynamic json) {
version = json['version'];
count = json['count'];
data = json['data'];
header = json['header'];
}
Map<String, dynamic> toJson() {
2022-08-29 14:43:31 +00:00
final map = <String, dynamic>{};
map['version'] = version;
map['count'] = count;
map['data'] = data;
map['header'] = header;
return map;
}
}