2022-12-08 04:28:33 +00:00
|
|
|
import 'dart:io';
|
|
|
|
|
2022-12-26 09:21:58 +00:00
|
|
|
import 'package:flutter/foundation.dart';
|
2022-09-23 01:18:00 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2020-04-24 12:40:24 +00:00
|
|
|
import 'package:path/path.dart';
|
2021-07-18 08:56:54 +00:00
|
|
|
import 'package:photo_manager/photo_manager.dart';
|
2020-05-25 15:07:22 +00:00
|
|
|
import 'package:photos/core/configuration.dart';
|
2021-07-23 12:53:39 +00:00
|
|
|
import 'package:photos/core/constants.dart';
|
2023-08-25 04:39:30 +00:00
|
|
|
import 'package:photos/models/file/file_type.dart';
|
2023-04-02 12:47:18 +00:00
|
|
|
import 'package:photos/models/location/location.dart';
|
2023-05-25 05:51:56 +00:00
|
|
|
import "package:photos/models/metadata/file_magic.dart";
|
2021-08-24 06:51:02 +00:00
|
|
|
import 'package:photos/services/feature_flag_service.dart';
|
2022-11-05 05:10:22 +00:00
|
|
|
import 'package:photos/utils/date_time_util.dart';
|
2021-10-27 12:47:34 +00:00
|
|
|
import 'package:photos/utils/exif_util.dart';
|
2022-07-28 09:02:10 +00:00
|
|
|
import 'package:photos/utils/file_uploader_util.dart';
|
2020-03-28 13:56:06 +00:00
|
|
|
|
2023-08-24 16:56:24 +00:00
|
|
|
class EnteFile {
|
2022-09-22 15:13:19 +00:00
|
|
|
int? generatedID;
|
|
|
|
int? uploadedFileID;
|
|
|
|
int? ownerID;
|
|
|
|
int? collectionID;
|
|
|
|
String? localID;
|
|
|
|
String? title;
|
|
|
|
String? deviceFolder;
|
|
|
|
int? creationTime;
|
|
|
|
int? modificationTime;
|
|
|
|
int? updationTime;
|
2023-06-30 12:47:25 +00:00
|
|
|
int? addedTime;
|
2022-09-22 15:13:19 +00:00
|
|
|
Location? location;
|
|
|
|
late FileType fileType;
|
|
|
|
int? fileSubType;
|
|
|
|
int? duration;
|
|
|
|
String? exif;
|
|
|
|
String? hash;
|
|
|
|
int? metadataVersion;
|
|
|
|
String? encryptedKey;
|
|
|
|
String? keyDecryptionNonce;
|
|
|
|
String? fileDecryptionHeader;
|
|
|
|
String? thumbnailDecryptionHeader;
|
|
|
|
String? metadataDecryptionHeader;
|
2022-09-24 10:36:05 +00:00
|
|
|
int? fileSize;
|
2022-09-22 15:13:19 +00:00
|
|
|
|
|
|
|
String? mMdEncodedJson;
|
2021-09-16 13:14:51 +00:00
|
|
|
int mMdVersion = 0;
|
2022-09-22 15:13:19 +00:00
|
|
|
MagicMetadata? _mmd;
|
2021-10-26 14:46:58 +00:00
|
|
|
|
2021-09-20 06:41:38 +00:00
|
|
|
MagicMetadata get magicMetadata =>
|
|
|
|
_mmd ?? MagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
|
2021-10-26 14:46:58 +00:00
|
|
|
|
2021-09-23 07:16:08 +00:00
|
|
|
set magicMetadata(val) => _mmd = val;
|
2021-09-16 13:14:51 +00:00
|
|
|
|
2021-10-26 05:25:42 +00:00
|
|
|
// public magic metadata is shared if during file/album sharing
|
2022-09-22 15:13:19 +00:00
|
|
|
String? pubMmdEncodedJson;
|
2021-10-26 05:25:42 +00:00
|
|
|
int pubMmdVersion = 0;
|
2022-09-22 15:13:19 +00:00
|
|
|
PubMagicMetadata? _pubMmd;
|
2021-10-26 14:46:58 +00:00
|
|
|
|
2022-09-22 15:13:19 +00:00
|
|
|
PubMagicMetadata? get pubMagicMetadata =>
|
2021-10-26 10:37:14 +00:00
|
|
|
_pubMmd ?? PubMagicMetadata.fromEncodedJson(pubMmdEncodedJson ?? '{}');
|
2021-10-26 14:46:58 +00:00
|
|
|
|
2021-10-26 05:25:42 +00:00
|
|
|
set pubMagicMetadata(val) => _pubMmd = val;
|
|
|
|
|
2022-08-29 07:58:49 +00:00
|
|
|
// in Version 1, live photo hash is stored as zip's hash.
|
|
|
|
// in V2: LivePhoto hash is stored as imgHash:vidHash
|
|
|
|
static const kCurrentMetadataVersion = 2;
|
2021-08-18 12:15:22 +00:00
|
|
|
|
2022-09-23 01:18:00 +00:00
|
|
|
static final _logger = Logger('File');
|
|
|
|
|
2023-08-24 16:56:24 +00:00
|
|
|
EnteFile();
|
2020-08-11 23:04:16 +00:00
|
|
|
|
2023-08-24 16:56:24 +00:00
|
|
|
static Future<EnteFile> fromAsset(String pathName, AssetEntity asset) async {
|
|
|
|
final EnteFile file = EnteFile();
|
2020-08-09 22:34:59 +00:00
|
|
|
file.localID = asset.id;
|
2020-06-19 23:03:26 +00:00
|
|
|
file.title = asset.title;
|
2021-06-02 21:46:38 +00:00
|
|
|
file.deviceFolder = pathName;
|
2023-04-02 12:47:18 +00:00
|
|
|
file.location =
|
|
|
|
Location(latitude: asset.latitude, longitude: asset.longitude);
|
2023-08-22 13:44:15 +00:00
|
|
|
file.fileType = fileTypeFromAsset(asset);
|
2022-11-10 10:40:35 +00:00
|
|
|
file.creationTime = parseFileCreationTime(file.title, asset);
|
2022-11-10 10:30:11 +00:00
|
|
|
file.modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
|
|
|
file.fileSubType = asset.subtype;
|
|
|
|
file.metadataVersion = kCurrentMetadataVersion;
|
|
|
|
return file;
|
|
|
|
}
|
|
|
|
|
2022-11-10 10:40:35 +00:00
|
|
|
static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
|
2022-11-10 10:30:11 +00:00
|
|
|
int creationTime = asset.createDateTime.microsecondsSinceEpoch;
|
2022-11-10 10:37:41 +00:00
|
|
|
if (creationTime >= jan011981Time) {
|
2022-11-10 10:30:11 +00:00
|
|
|
// assuming that fileSystem is returning correct creationTime.
|
|
|
|
// During upload, this might get overridden with exif Creation time
|
|
|
|
return creationTime;
|
|
|
|
} else {
|
2022-11-10 10:37:41 +00:00
|
|
|
if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
|
2022-11-10 10:36:22 +00:00
|
|
|
creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
2022-11-10 10:30:11 +00:00
|
|
|
} else {
|
2022-11-10 10:36:22 +00:00
|
|
|
creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
|
2022-11-10 10:30:11 +00:00
|
|
|
}
|
2020-05-05 20:42:21 +00:00
|
|
|
try {
|
2022-11-10 09:13:21 +00:00
|
|
|
final parsedDateTime = parseDateTimeFromFileNameV2(
|
2022-11-10 10:30:11 +00:00
|
|
|
basenameWithoutExtension(fileTitle ?? ""),
|
2022-11-10 09:21:57 +00:00
|
|
|
);
|
2022-11-10 10:30:11 +00:00
|
|
|
if (parsedDateTime != null) {
|
|
|
|
creationTime = parsedDateTime.microsecondsSinceEpoch;
|
|
|
|
}
|
2020-05-05 20:42:21 +00:00
|
|
|
} catch (e) {
|
2022-11-10 10:30:11 +00:00
|
|
|
// ignore
|
2020-05-05 20:42:21 +00:00
|
|
|
}
|
|
|
|
}
|
2022-11-10 10:30:11 +00:00
|
|
|
|
|
|
|
return creationTime;
|
2020-03-30 15:08:50 +00:00
|
|
|
}
|
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
Future<AssetEntity?> get getAsset {
|
2021-07-24 17:22:46 +00:00
|
|
|
if (localID == null) {
|
|
|
|
return Future.value(null);
|
|
|
|
}
|
2022-09-22 15:13:19 +00:00
|
|
|
return AssetEntity.fromId(localID!);
|
2020-04-25 10:28:22 +00:00
|
|
|
}
|
|
|
|
|
2020-08-11 23:04:16 +00:00
|
|
|
void applyMetadata(Map<String, dynamic> metadata) {
|
|
|
|
localID = metadata["localID"];
|
|
|
|
title = metadata["title"];
|
|
|
|
deviceFolder = metadata["deviceFolder"];
|
2021-07-18 08:56:54 +00:00
|
|
|
creationTime = metadata["creationTime"] ?? 0;
|
2021-03-23 08:03:51 +00:00
|
|
|
modificationTime = metadata["modificationTime"] ?? creationTime;
|
2021-04-19 10:50:57 +00:00
|
|
|
final latitude = double.tryParse(metadata["latitude"].toString());
|
|
|
|
final longitude = double.tryParse(metadata["longitude"].toString());
|
2021-01-13 08:29:17 +00:00
|
|
|
if (latitude == null || longitude == null) {
|
|
|
|
location = null;
|
|
|
|
} else {
|
2023-04-02 12:47:18 +00:00
|
|
|
location = Location(latitude: latitude, longitude: longitude);
|
2021-01-13 08:29:17 +00:00
|
|
|
}
|
2022-12-30 14:50:40 +00:00
|
|
|
fileType = getFileType(metadata["fileType"] ?? -1);
|
2021-08-18 13:23:42 +00:00
|
|
|
fileSubType = metadata["subType"] ?? -1;
|
|
|
|
duration = metadata["duration"] ?? 0;
|
|
|
|
exif = metadata["exif"];
|
|
|
|
hash = metadata["hash"];
|
2022-08-29 08:05:16 +00:00
|
|
|
// handle past live photos upload from web client
|
|
|
|
if (hash == null &&
|
|
|
|
fileType == FileType.livePhoto &&
|
2023-05-31 07:05:14 +00:00
|
|
|
metadata.containsKey('imageHash') &&
|
|
|
|
metadata.containsKey('videoHash')) {
|
2022-08-29 08:05:16 +00:00
|
|
|
// convert to imgHash:vidHash
|
|
|
|
hash =
|
2023-05-31 07:05:14 +00:00
|
|
|
'${metadata['imageHash']}$kLivePhotoHashSeparator${metadata['videoHash']}';
|
2022-08-29 08:05:16 +00:00
|
|
|
}
|
2021-08-18 13:23:42 +00:00
|
|
|
metadataVersion = metadata["version"] ?? 0;
|
2020-08-11 23:04:16 +00:00
|
|
|
}
|
|
|
|
|
2022-07-28 09:02:10 +00:00
|
|
|
Future<Map<String, dynamic>> getMetadataForUpload(
|
|
|
|
MediaUploadData mediaUploadData,
|
|
|
|
) async {
|
2022-09-23 01:48:25 +00:00
|
|
|
final asset = await getAsset;
|
2021-10-22 09:30:06 +00:00
|
|
|
// asset can be null for files shared to app
|
|
|
|
if (asset != null) {
|
2022-05-12 03:27:32 +00:00
|
|
|
fileSubType = asset.subtype;
|
2021-10-22 09:30:06 +00:00
|
|
|
if (fileType == FileType.video) {
|
|
|
|
duration = asset.duration;
|
|
|
|
}
|
|
|
|
}
|
2022-12-08 04:28:33 +00:00
|
|
|
bool hasExifTime = false;
|
2023-04-18 05:19:10 +00:00
|
|
|
if ((fileType == FileType.image || fileType == FileType.video) &&
|
|
|
|
mediaUploadData.sourceFile != null) {
|
2023-04-17 12:48:05 +00:00
|
|
|
final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!);
|
|
|
|
if (exifData != null) {
|
|
|
|
if (fileType == FileType.image) {
|
|
|
|
final exifTime = await getCreationTimeFromEXIF(null, exifData);
|
|
|
|
if (exifTime != null) {
|
|
|
|
hasExifTime = true;
|
|
|
|
creationTime = exifTime.microsecondsSinceEpoch;
|
|
|
|
}
|
|
|
|
}
|
2023-04-18 07:08:52 +00:00
|
|
|
if (Platform.isAndroid) {
|
2023-04-17 12:48:05 +00:00
|
|
|
//Fix for missing location data in lower android versions.
|
2023-04-18 07:31:15 +00:00
|
|
|
final Location? exifLocation = locationFromExif(exifData);
|
2023-05-03 04:55:07 +00:00
|
|
|
if (Location.isValidLocation(exifLocation)) {
|
2023-04-17 12:48:05 +00:00
|
|
|
location = exifLocation;
|
|
|
|
}
|
|
|
|
}
|
2021-10-27 12:48:58 +00:00
|
|
|
}
|
2021-10-27 12:47:34 +00:00
|
|
|
}
|
2022-12-08 05:25:32 +00:00
|
|
|
// Try to get the timestamp from fileName. In case of iOS, file names are
|
2022-12-08 04:28:33 +00:00
|
|
|
// generic IMG_XXXX, so only parse it on Android devices
|
|
|
|
if (!hasExifTime && Platform.isAndroid && title != null) {
|
|
|
|
final timeFromFileName = parseDateTimeFromFileNameV2(title!);
|
|
|
|
if (timeFromFileName != null) {
|
2022-12-08 05:39:58 +00:00
|
|
|
// only use timeFromFileName if the existing creationTime and
|
|
|
|
// timeFromFilename belongs to different date.
|
2022-12-08 05:25:32 +00:00
|
|
|
// This is done because many times the fileTimeStamp will only give us
|
|
|
|
// the date, not time value but the photo_manager's creation time will
|
|
|
|
// contain the time.
|
|
|
|
final bool useFileTimeStamp = creationTime == null ||
|
2022-12-08 05:39:58 +00:00
|
|
|
!areFromSameDay(
|
2022-12-08 06:07:57 +00:00
|
|
|
creationTime!,
|
|
|
|
timeFromFileName.microsecondsSinceEpoch,
|
|
|
|
);
|
2022-12-08 05:25:32 +00:00
|
|
|
if (useFileTimeStamp) {
|
|
|
|
creationTime = timeFromFileName.microsecondsSinceEpoch;
|
|
|
|
}
|
2022-12-08 04:28:33 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-29 07:58:49 +00:00
|
|
|
hash = mediaUploadData.hashData?.fileHash;
|
2022-09-23 01:48:25 +00:00
|
|
|
return metadata;
|
2021-10-22 09:30:06 +00:00
|
|
|
}
|
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
Map<String, dynamic> get metadata {
|
2021-07-22 18:41:58 +00:00
|
|
|
final metadata = <String, dynamic>{};
|
2022-09-23 01:48:25 +00:00
|
|
|
metadata["localID"] = isSharedMediaToAppSandbox ? null : localID;
|
2020-08-10 23:47:22 +00:00
|
|
|
metadata["title"] = title;
|
|
|
|
metadata["deviceFolder"] = deviceFolder;
|
|
|
|
metadata["creationTime"] = creationTime;
|
|
|
|
metadata["modificationTime"] = modificationTime;
|
2021-10-25 11:31:11 +00:00
|
|
|
metadata["fileType"] = fileType.index;
|
2021-01-19 08:30:48 +00:00
|
|
|
if (location != null &&
|
2022-09-22 15:13:19 +00:00
|
|
|
location!.latitude != null &&
|
|
|
|
location!.longitude != null) {
|
|
|
|
metadata["latitude"] = location!.latitude;
|
|
|
|
metadata["longitude"] = location!.longitude;
|
2021-01-19 08:30:48 +00:00
|
|
|
}
|
2021-10-22 09:30:06 +00:00
|
|
|
if (fileSubType != null) {
|
2021-08-31 08:20:32 +00:00
|
|
|
metadata["subType"] = fileSubType;
|
|
|
|
}
|
2021-10-22 09:30:06 +00:00
|
|
|
if (duration != null) {
|
|
|
|
metadata["duration"] = duration;
|
|
|
|
}
|
|
|
|
if (hash != null) {
|
|
|
|
metadata["hash"] = hash;
|
|
|
|
}
|
|
|
|
if (metadataVersion != null) {
|
|
|
|
metadata["version"] = metadataVersion;
|
|
|
|
}
|
2020-08-10 23:47:22 +00:00
|
|
|
return metadata;
|
|
|
|
}
|
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
String get downloadUrl {
|
2022-08-10 13:27:32 +00:00
|
|
|
final endpoint = Configuration.instance.getHttpEndpoint();
|
2022-08-10 13:28:33 +00:00
|
|
|
if (endpoint != kDefaultProductionEndpoint ||
|
2022-08-10 13:27:32 +00:00
|
|
|
FeatureFlagService.instance.disableCFWorker()) {
|
|
|
|
return endpoint + "/files/download/" + uploadedFileID.toString();
|
2021-05-06 22:32:02 +00:00
|
|
|
} else {
|
2022-08-10 13:51:57 +00:00
|
|
|
return "https://files.ente.io/?fileID=" + uploadedFileID.toString();
|
2021-05-06 22:32:02 +00:00
|
|
|
}
|
2020-05-25 15:07:22 +00:00
|
|
|
}
|
|
|
|
|
2022-11-05 05:52:28 +00:00
|
|
|
String? get caption {
|
|
|
|
return pubMagicMetadata?.caption;
|
|
|
|
}
|
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
String get thumbnailUrl {
|
2022-08-10 13:27:32 +00:00
|
|
|
final endpoint = Configuration.instance.getHttpEndpoint();
|
2022-08-10 13:28:33 +00:00
|
|
|
if (endpoint != kDefaultProductionEndpoint ||
|
2022-08-10 13:27:32 +00:00
|
|
|
FeatureFlagService.instance.disableCFWorker()) {
|
|
|
|
return endpoint + "/files/preview/" + uploadedFileID.toString();
|
2021-05-06 22:32:02 +00:00
|
|
|
} else {
|
2022-08-10 13:51:57 +00:00
|
|
|
return "https://thumbnails.ente.io/?fileID=" + uploadedFileID.toString();
|
2021-05-06 22:32:02 +00:00
|
|
|
}
|
2020-05-27 14:20:52 +00:00
|
|
|
}
|
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
String get displayName {
|
2022-09-22 15:13:19 +00:00
|
|
|
if (pubMagicMetadata != null && pubMagicMetadata!.editedName != null) {
|
|
|
|
return pubMagicMetadata!.editedName!;
|
2021-11-09 11:40:40 +00:00
|
|
|
}
|
2022-12-26 09:21:58 +00:00
|
|
|
if (title == null && kDebugMode) _logger.severe('File title is null');
|
2022-09-23 01:18:00 +00:00
|
|
|
return title ?? '';
|
2021-11-09 11:40:40 +00:00
|
|
|
}
|
|
|
|
|
2023-04-12 08:13:21 +00:00
|
|
|
// return 0 if the height is not available
|
2023-05-03 07:18:40 +00:00
|
|
|
int get height {
|
2023-04-12 08:13:21 +00:00
|
|
|
return pubMagicMetadata?.h ?? 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int get width {
|
|
|
|
return pubMagicMetadata?.w ?? 0;
|
|
|
|
}
|
|
|
|
|
2021-07-22 06:11:32 +00:00
|
|
|
// returns true if the file isn't available in the user's gallery
|
2022-09-23 01:48:25 +00:00
|
|
|
bool get isRemoteFile {
|
2021-07-19 05:37:34 +00:00
|
|
|
return localID == null && uploadedFileID != null;
|
|
|
|
}
|
|
|
|
|
2022-11-11 06:23:01 +00:00
|
|
|
bool get isUploaded {
|
|
|
|
return uploadedFileID != null;
|
|
|
|
}
|
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
bool get isSharedMediaToAppSandbox {
|
2022-07-05 20:05:51 +00:00
|
|
|
return localID != null &&
|
2022-09-22 15:13:19 +00:00
|
|
|
(localID!.startsWith(oldSharedMediaIdentifier) ||
|
|
|
|
localID!.startsWith(sharedMediaIdentifier));
|
2021-07-22 06:11:32 +00:00
|
|
|
}
|
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
bool get hasLocation {
|
2021-07-22 07:25:30 +00:00
|
|
|
return location != null &&
|
2023-04-18 07:31:15 +00:00
|
|
|
((location!.longitude ?? 0) != 0 || (location!.latitude ?? 0) != 0);
|
2021-07-22 07:25:30 +00:00
|
|
|
}
|
|
|
|
|
2020-04-17 20:37:56 +00:00
|
|
|
@override
|
2020-04-23 20:00:20 +00:00
|
|
|
String toString() {
|
2022-06-05 11:47:59 +00:00
|
|
|
return '''File(generatedID: $generatedID, localID: $localID, title: $title,
|
2021-08-04 20:09:00 +00:00
|
|
|
uploadedFileId: $uploadedFileID, modificationTime: $modificationTime,
|
2021-08-04 19:33:57 +00:00
|
|
|
ownerID: $ownerID, collectionID: $collectionID, updationTime: $updationTime)''';
|
2020-04-24 12:40:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool operator ==(Object o) {
|
|
|
|
if (identical(this, o)) return true;
|
|
|
|
|
2023-08-24 16:56:24 +00:00
|
|
|
return o is EnteFile &&
|
2020-08-09 22:34:59 +00:00
|
|
|
o.generatedID == generatedID &&
|
|
|
|
o.uploadedFileID == uploadedFileID &&
|
|
|
|
o.localID == localID;
|
2020-04-24 12:40:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
int get hashCode {
|
2020-08-09 22:34:59 +00:00
|
|
|
return generatedID.hashCode ^ uploadedFileID.hashCode ^ localID.hashCode;
|
2020-04-17 20:37:56 +00:00
|
|
|
}
|
2020-06-23 18:57:22 +00:00
|
|
|
|
2022-09-23 01:48:25 +00:00
|
|
|
String get tag {
|
2020-06-23 18:57:22 +00:00
|
|
|
return "local_" +
|
2020-08-09 22:34:59 +00:00
|
|
|
localID.toString() +
|
2020-06-23 18:57:22 +00:00
|
|
|
":remote_" +
|
2020-08-09 22:34:59 +00:00
|
|
|
uploadedFileID.toString() +
|
2020-06-23 20:13:45 +00:00
|
|
|
":generated_" +
|
2020-08-09 22:34:59 +00:00
|
|
|
generatedID.toString();
|
2020-06-23 18:57:22 +00:00
|
|
|
}
|
2022-07-19 06:39:45 +00:00
|
|
|
|
|
|
|
String cacheKey() {
|
|
|
|
// todo: Neeraj: 19thJuly'22: evaluate and add fileHash as the key?
|
2022-09-15 11:04:33 +00:00
|
|
|
return localID ?? uploadedFileID?.toString() ?? generatedID.toString();
|
2022-07-19 06:39:45 +00:00
|
|
|
}
|
2020-03-30 14:28:46 +00:00
|
|
|
}
|