mirror of
https://github.com/ente-io/ente.git
synced 2025-08-08 07:28:26 +00:00
commit
c6ea0f1fd7
@ -1,5 +1,5 @@
|
||||
import "package:photos/models/api/metadata.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/services/file_magic_service.dart';
|
||||
|
||||
class CreateRequest {
|
||||
String encryptedKey;
|
||||
|
54
mobile/lib/models/api/metadata.dart
Normal file
54
mobile/lib/models/api/metadata.dart
Normal file
@ -0,0 +1,54 @@
|
||||
class UpdateMagicMetadataRequest {
|
||||
final int id;
|
||||
final MetadataRequest? magicMetadata;
|
||||
|
||||
UpdateMagicMetadataRequest({required this.id, required this.magicMetadata});
|
||||
|
||||
factory UpdateMagicMetadataRequest.fromJson(dynamic json) {
|
||||
return UpdateMagicMetadataRequest(
|
||||
id: json['id'],
|
||||
magicMetadata: json['magicMetadata'] != null
|
||||
? MetadataRequest.fromJson(json['magicMetadata'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
if (magicMetadata != null) {
|
||||
map['magicMetadata'] = magicMetadata!.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataRequest {
|
||||
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() {
|
||||
final map = <String, dynamic>{};
|
||||
map['version'] = version;
|
||||
map['count'] = count;
|
||||
map['data'] = data;
|
||||
map['header'] = header;
|
||||
return map;
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ import 'package:photos/models/api/collection/collection_file_item.dart';
|
||||
import 'package:photos/models/api/collection/create_request.dart';
|
||||
import "package:photos/models/api/collection/public_url.dart";
|
||||
import "package:photos/models/api/collection/user.dart";
|
||||
import "package:photos/models/api/metadata.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/collection/collection_items.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
@ -37,7 +38,6 @@ import "package:photos/models/metadata/collection_magic.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import 'package:photos/services/file_magic_service.dart';
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import 'package:photos/services/remote_sync_service.dart';
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
@ -666,6 +666,7 @@ class CollectionsService {
|
||||
),
|
||||
);
|
||||
sync().ignore();
|
||||
// not required once remote & local world are separate
|
||||
LocalSyncService.instance.syncAll().ignore();
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/events/force_reload_home_gallery_event.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/extensions/list.dart';
|
||||
import "package:photos/models/api/metadata.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/metadata/common_keys.dart";
|
||||
import "package:photos/models/metadata/file_magic.dart";
|
||||
@ -195,58 +196,3 @@ class FileMagicService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateMagicMetadataRequest {
|
||||
final int id;
|
||||
final MetadataRequest? magicMetadata;
|
||||
|
||||
UpdateMagicMetadataRequest({required this.id, required this.magicMetadata});
|
||||
|
||||
factory UpdateMagicMetadataRequest.fromJson(dynamic json) {
|
||||
return UpdateMagicMetadataRequest(
|
||||
id: json['id'],
|
||||
magicMetadata: json['magicMetadata'] != null
|
||||
? MetadataRequest.fromJson(json['magicMetadata'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
if (magicMetadata != null) {
|
||||
map['magicMetadata'] = magicMetadata!.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataRequest {
|
||||
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() {
|
||||
final map = <String, dynamic>{};
|
||||
map['version'] = version;
|
||||
map['count'] = count;
|
||||
map['data'] = data;
|
||||
map['header'] = header;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,12 @@ import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/api/collection/create_request.dart';
|
||||
import "package:photos/models/api/metadata.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/metadata/collection_magic.dart";
|
||||
import "package:photos/models/metadata/common_keys.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/file_magic_service.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
|
||||
extension HiddenService on CollectionsService {
|
||||
|
@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
|
||||
class StorageErrorWidget extends StatelessWidget {
|
||||
const StorageErrorWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline_outlined,
|
||||
color: strokeBaseDark,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
S.of(context).yourStorageDetailsCouldNotBeFetched,
|
||||
style: getEnteTextTheme(context).small.copyWith(
|
||||
color: textMutedDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/ui/components/buttons/chip_button_widget.dart";
|
||||
import "package:photos/ui/components/info_item_widget.dart";
|
||||
|
||||
class ObjectsItemWidget extends StatelessWidget {
|
||||
final EnteFile file;
|
||||
const ObjectsItemWidget(this.file, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfoItemWidget(
|
||||
key: const ValueKey("Objects"),
|
||||
leadingIcon: Icons.image_search_outlined,
|
||||
subtitleSection: _objectTags(context, file),
|
||||
hasChipButtons: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ChipButtonWidget>> _objectTags(
|
||||
BuildContext context,
|
||||
EnteFile file,
|
||||
) async {
|
||||
try {
|
||||
final chipButtons = <ChipButtonWidget>[];
|
||||
var objectTags = <String, double>{};
|
||||
|
||||
// final thumbnail = await getThumbnail(file);
|
||||
// if (thumbnail != null) {
|
||||
// objectTags = await ObjectDetectionService.instance.predict(thumbnail);
|
||||
// }
|
||||
if (objectTags.isEmpty) {
|
||||
return [
|
||||
ChipButtonWidget(
|
||||
S.of(context).noResults,
|
||||
noChips: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
// sort by values
|
||||
objectTags = Map.fromEntries(
|
||||
objectTags.entries.toList()
|
||||
..sort((e1, e2) => e2.value.compareTo(e1.value)),
|
||||
);
|
||||
|
||||
for (MapEntry<String, double> entry in objectTags.entries) {
|
||||
chipButtons.add(
|
||||
ChipButtonWidget(
|
||||
entry.key +
|
||||
(kDebugMode
|
||||
? "-" + (entry.value * 100).round().toString()
|
||||
: ""),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return chipButtons;
|
||||
} catch (e, s) {
|
||||
Logger("ObjctsItemWidget").info(e, s);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
|
||||
///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=10854%3A57947&t=H5AvR79OYDnB9ekw-4
|
||||
class NewPersonItemWidget extends StatelessWidget {
|
||||
const NewPersonItemWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
const sideOfThumbnail = 60.0;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(4),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: sideOfThumbnail,
|
||||
width: sideOfThumbnail,
|
||||
child: Icon(
|
||||
Icons.add_outlined,
|
||||
color: colorScheme.strokeMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Text(
|
||||
S.of(context).addNewPerson,
|
||||
style:
|
||||
textTheme.body.copyWith(color: colorScheme.textMuted),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IgnorePointer(
|
||||
child: DottedBorder(
|
||||
dashPattern: const [4],
|
||||
color: colorScheme.strokeFainter,
|
||||
strokeWidth: 1,
|
||||
padding: const EdgeInsets.all(0),
|
||||
borderType: BorderType.RRect,
|
||||
radius: const Radius.circular(4),
|
||||
child: SizedBox(
|
||||
//Have to decrease the height and width by 1 pt as the stroke
|
||||
//dotted border gives is of strokeAlign.center, so 0.5 inside and
|
||||
// outside. Here for the row, stroke should be inside so we
|
||||
//decrease the size of this sizedBox by 1 (so it shrinks 0.5 from
|
||||
//every side) so that the strokeAlign.center of this sizedBox
|
||||
//looks like a strokeAlign.inside in the row.
|
||||
height: sideOfThumbnail - 1,
|
||||
//This width will work for this only if the row widget takes up the
|
||||
//full size it's parent (stack).
|
||||
width: constraints.maxWidth - 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
import "dart:math";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:computer/computer.dart";
|
||||
import "package:flutter_image_compress/flutter_image_compress.dart";
|
||||
import "package:image/image.dart" as img;
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/models/ml/face/box.dart";
|
||||
|
||||
/// Bounding box of a face.
|
||||
///
|
||||
/// [xMin] and [yMin] are the coordinates of the top left corner of the box, and
|
||||
/// [width] and [height] are the width and height of the box.
|
||||
///
|
||||
/// One unit is equal to one pixel in the original image.
|
||||
class FaceBoxImage {
|
||||
final int xMin;
|
||||
final int yMin;
|
||||
final int width;
|
||||
final int height;
|
||||
|
||||
FaceBoxImage({
|
||||
required this.xMin,
|
||||
required this.yMin,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
}
|
||||
|
||||
final _logger = Logger("FaceUtil");
|
||||
final _computer = Computer.shared();
|
||||
const _faceImageBufferFactor = 0.2;
|
||||
|
||||
///Convert img.Image to ui.Image and use RawImage to display.
|
||||
Future<List<img.Image>> generateImgFaceThumbnails(
|
||||
String imagePath,
|
||||
List<FaceBox> faceBoxes,
|
||||
) async {
|
||||
final faceThumbnails = <img.Image>[];
|
||||
|
||||
final image = await decodeToImgImage(imagePath);
|
||||
|
||||
for (FaceBox faceBox in faceBoxes) {
|
||||
final croppedImage = cropFaceBoxFromImage(image, faceBox);
|
||||
faceThumbnails.add(croppedImage);
|
||||
}
|
||||
|
||||
return faceThumbnails;
|
||||
}
|
||||
|
||||
Future<List<Uint8List>> generateJpgFaceThumbnails(
|
||||
String imagePath,
|
||||
List<FaceBox> faceBoxes,
|
||||
) async {
|
||||
final image = await decodeToImgImage(imagePath);
|
||||
final croppedImages = <img.Image>[];
|
||||
for (FaceBox faceBox in faceBoxes) {
|
||||
final croppedImage = cropFaceBoxFromImage(image, faceBox);
|
||||
croppedImages.add(croppedImage);
|
||||
}
|
||||
|
||||
return await _computer
|
||||
.compute(_encodeImagesToJpg, param: {"images": croppedImages});
|
||||
}
|
||||
|
||||
Future<img.Image> decodeToImgImage(String imagePath) async {
|
||||
img.Image? image =
|
||||
await _computer.compute(_decodeImageFile, param: {"filePath": imagePath});
|
||||
|
||||
if (image == null) {
|
||||
_logger.info(
|
||||
"Failed to decode image. Compressing to jpg and decoding",
|
||||
);
|
||||
final compressedJPGImage =
|
||||
await FlutterImageCompress.compressWithFile(imagePath);
|
||||
image = await _computer.compute(
|
||||
_decodeJpg,
|
||||
param: {"image": compressedJPGImage},
|
||||
);
|
||||
|
||||
if (image == null) {
|
||||
throw Exception("Failed to decode image");
|
||||
} else {
|
||||
return image;
|
||||
}
|
||||
} else {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an Image from 'package:image/image.dart'
|
||||
img.Image cropFaceBoxFromImage(img.Image image, FaceBox faceBox) {
|
||||
final squareFaceBox = _getSquareFaceBoxImage(image, faceBox);
|
||||
final squareFaceBoxWithBuffer =
|
||||
_addBufferAroundFaceBox(squareFaceBox, _faceImageBufferFactor);
|
||||
return img.copyCrop(
|
||||
image,
|
||||
x: squareFaceBoxWithBuffer.xMin,
|
||||
y: squareFaceBoxWithBuffer.yMin,
|
||||
width: squareFaceBoxWithBuffer.width,
|
||||
height: squareFaceBoxWithBuffer.height,
|
||||
antialias: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a square face box image from the original image with
|
||||
/// side length equal to the maximum of the width and height of the face box in
|
||||
/// the OG image.
|
||||
FaceBoxImage _getSquareFaceBoxImage(img.Image image, FaceBox faceBox) {
|
||||
final width = (image.width * faceBox.width).round();
|
||||
final height = (image.height * faceBox.height).round();
|
||||
final side = max(width, height);
|
||||
final xImage = (image.width * faceBox.x).round();
|
||||
final yImage = (image.height * faceBox.y).round();
|
||||
|
||||
if (height >= width) {
|
||||
final xImageAdj = (xImage - (height - width) / 2).round();
|
||||
return FaceBoxImage(
|
||||
xMin: xImageAdj,
|
||||
yMin: yImage,
|
||||
width: side,
|
||||
height: side,
|
||||
);
|
||||
} else {
|
||||
final yImageAdj = (yImage - (width - height) / 2).round();
|
||||
return FaceBoxImage(
|
||||
xMin: xImage,
|
||||
yMin: yImageAdj,
|
||||
width: side,
|
||||
height: side,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///To add some buffer around the face box so that the face isn't cropped
|
||||
///too close to the face.
|
||||
FaceBoxImage _addBufferAroundFaceBox(
|
||||
FaceBoxImage faceBoxImage,
|
||||
double bufferFactor,
|
||||
) {
|
||||
final heightBuffer = faceBoxImage.height * bufferFactor;
|
||||
final widthBuffer = faceBoxImage.width * bufferFactor;
|
||||
final xMinWithBuffer = faceBoxImage.xMin - widthBuffer;
|
||||
final yMinWithBuffer = faceBoxImage.yMin - heightBuffer;
|
||||
final widthWithBuffer = faceBoxImage.width + 2 * widthBuffer;
|
||||
final heightWithBuffer = faceBoxImage.height + 2 * heightBuffer;
|
||||
//Do not add buffer if the top left edge of the image is out of bounds
|
||||
//after adding the buffer.
|
||||
if (xMinWithBuffer < 0 || yMinWithBuffer < 0) {
|
||||
return faceBoxImage;
|
||||
}
|
||||
//Another similar case that can be handled is when the bottom right edge
|
||||
//of the image is out of bounds after adding the buffer. But the
|
||||
//the visual difference is not as significant as when the top left edge
|
||||
//is out of bounds, so we are not handling that case.
|
||||
return FaceBoxImage(
|
||||
xMin: xMinWithBuffer.round(),
|
||||
yMin: yMinWithBuffer.round(),
|
||||
width: widthWithBuffer.round(),
|
||||
height: heightWithBuffer.round(),
|
||||
);
|
||||
}
|
||||
|
||||
List<Uint8List> _encodeImagesToJpg(Map args) {
|
||||
final images = args["images"] as List<img.Image>;
|
||||
return images.map((img.Image image) => img.encodeJpg(image)).toList();
|
||||
}
|
||||
|
||||
Future<img.Image?> _decodeImageFile(Map args) async {
|
||||
return await img.decodeImageFile(args["filePath"]);
|
||||
}
|
||||
|
||||
img.Image? _decodeJpg(Map args) {
|
||||
return img.decodeJpg(args["image"])!;
|
||||
}
|
@ -24,6 +24,7 @@ import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/events/subscription_purchased_event.dart';
|
||||
import 'package:photos/main.dart';
|
||||
import "package:photos/models/api/metadata.dart";
|
||||
import "package:photos/models/backup/backup_item.dart";
|
||||
import "package:photos/models/backup/backup_item_status.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
@ -34,7 +35,6 @@ import 'package:photos/module/upload/model/upload_url.dart';
|
||||
import "package:photos/module/upload/service/multipart.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/file_magic_service.dart";
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import "package:photos/services/preview_video_store.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
@ -751,7 +751,8 @@ class FileUploader {
|
||||
if (SyncService.instance.shouldStopSync()) {
|
||||
throw SyncStopRequestedError();
|
||||
}
|
||||
final stillLocked = await _uploadLocks.isLocked(lockKey, _processType.toString());
|
||||
final stillLocked =
|
||||
await _uploadLocks.isLocked(lockKey, _processType.toString());
|
||||
if (!stillLocked) {
|
||||
_logger.warning('file ${file.tag} report paused is missing');
|
||||
throw LockFreedError();
|
||||
|
@ -17,13 +17,13 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import "package:photos/models/api/metadata.dart";
|
||||
import "package:photos/models/ffmpeg/ffprobe_props.dart";
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file/file_type.dart';
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/metadata/file_magic.dart";
|
||||
import "package:photos/services/file_magic_service.dart";
|
||||
import "package:photos/utils/exif_util.dart";
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import "package:uuid/uuid.dart";
|
||||
|
@ -1,14 +0,0 @@
|
||||
String convertLatLng(double decimal, bool isLat) {
|
||||
final degree = "${decimal.toString().split(".")[0]}°";
|
||||
final minutesBeforeConversion =
|
||||
double.parse("0.${decimal.toString().split(".")[1]}");
|
||||
final minutes = "${(minutesBeforeConversion * 60).toString().split('.')[0]}'";
|
||||
final secondsBeforeConversion = double.parse(
|
||||
"0.${(minutesBeforeConversion * 60).toString().split('.')[1]}",
|
||||
);
|
||||
final seconds =
|
||||
'${double.parse((secondsBeforeConversion * 60).toString()).toStringAsFixed(0)}" ';
|
||||
final dmsOutput =
|
||||
"$degree$minutes$seconds${isLat ? decimal > 0 ? 'N' : 'S' : decimal > 0 ? 'E' : 'W'}";
|
||||
return dmsOutput;
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
// ignore_for_file: implementation_imports
|
||||
|
||||
import "dart:io";
|
||||
|
||||
import "package:dio/dio.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import 'package:photos/module/upload/model/xml.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
|
||||
final _enteDio = NetworkClient.instance.enteDio;
|
||||
final _dio = NetworkClient.instance.getDio();
|
||||
|
||||
class PartETag extends XmlParsableObject {
|
||||
final int partNumber;
|
||||
final String eTag;
|
||||
|
||||
PartETag(this.partNumber, this.eTag);
|
||||
|
||||
@override
|
||||
String get elementName => "Part";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
"PartNumber": partNumber,
|
||||
"ETag": eTag,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MultipartUploadURLs {
|
||||
final String objectKey;
|
||||
final List<String> partsURLs;
|
||||
final String completeURL;
|
||||
|
||||
MultipartUploadURLs({
|
||||
required this.objectKey,
|
||||
required this.partsURLs,
|
||||
required this.completeURL,
|
||||
});
|
||||
|
||||
factory MultipartUploadURLs.fromMap(Map<String, dynamic> map) {
|
||||
return MultipartUploadURLs(
|
||||
objectKey: map["urls"]["objectKey"],
|
||||
partsURLs: (map["urls"]["partURLs"] as List).cast<String>(),
|
||||
completeURL: map["urls"]["completeURL"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> calculatePartCount(int fileSize) async {
|
||||
final partCount = (fileSize / multipartPartSize).ceil();
|
||||
return partCount;
|
||||
}
|
||||
|
||||
Future<MultipartUploadURLs> getMultipartUploadURLs(int count) async {
|
||||
try {
|
||||
assert(
|
||||
flagService.internalUser,
|
||||
"Multipart upload should not be enabled for external users.",
|
||||
);
|
||||
final response = await _enteDio.get(
|
||||
"/files/multipart-upload-urls",
|
||||
queryParameters: {
|
||||
"count": count,
|
||||
},
|
||||
);
|
||||
|
||||
return MultipartUploadURLs.fromMap(response.data);
|
||||
} on Exception catch (e) {
|
||||
Logger("MultipartUploadURL").severe(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> putMultipartFile(
|
||||
MultipartUploadURLs urls,
|
||||
File encryptedFile,
|
||||
) async {
|
||||
// upload individual parts and get their etags
|
||||
final etags = await uploadParts(urls.partsURLs, encryptedFile);
|
||||
|
||||
// complete the multipart upload
|
||||
await completeMultipartUpload(etags, urls.completeURL);
|
||||
|
||||
return urls.objectKey;
|
||||
}
|
||||
|
||||
Future<Map<int, String>> uploadParts(
|
||||
List<String> partsURLs,
|
||||
File encryptedFile,
|
||||
) async {
|
||||
final partsLength = partsURLs.length;
|
||||
final etags = <int, String>{};
|
||||
|
||||
for (int i = 0; i < partsLength; i++) {
|
||||
final partURL = partsURLs[i];
|
||||
final isLastPart = i == partsLength - 1;
|
||||
final fileSize = isLastPart
|
||||
? encryptedFile.lengthSync() % multipartPartSize
|
||||
: multipartPartSize;
|
||||
|
||||
final response = await _dio.put(
|
||||
partURL,
|
||||
data: encryptedFile.openRead(
|
||||
i * multipartPartSize,
|
||||
isLastPart ? null : multipartPartSize,
|
||||
),
|
||||
options: Options(
|
||||
headers: {
|
||||
Headers.contentLengthHeader: fileSize,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final eTag = response.headers.value("etag");
|
||||
|
||||
if (eTag?.isEmpty ?? true) {
|
||||
throw Exception('ETAG_MISSING');
|
||||
}
|
||||
|
||||
etags[i] = eTag!;
|
||||
}
|
||||
|
||||
return etags;
|
||||
}
|
||||
|
||||
Future<void> completeMultipartUpload(
|
||||
Map<int, String> partEtags,
|
||||
String completeURL,
|
||||
) async {
|
||||
final body = convertJs2Xml({
|
||||
'CompleteMultipartUpload': partEtags.entries
|
||||
.map(
|
||||
(e) => PartETag(
|
||||
e.key + 1,
|
||||
e.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
}).replaceAll('"', '').replaceAll('"', '');
|
||||
|
||||
try {
|
||||
await _dio.post(
|
||||
completeURL,
|
||||
data: body,
|
||||
options: Options(
|
||||
contentType: "text/xml",
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Logger("MultipartUpload").severe(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user