file.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import 'dart:io';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:logging/logging.dart';
  4. import 'package:path/path.dart';
  5. import 'package:photo_manager/photo_manager.dart';
  6. import 'package:photos/core/configuration.dart';
  7. import 'package:photos/core/constants.dart';
  8. import 'package:photos/models/file/file_type.dart';
  9. import 'package:photos/models/location/location.dart';
  10. import "package:photos/models/metadata/file_magic.dart";
  11. import "package:photos/service_locator.dart";
  12. import 'package:photos/utils/date_time_util.dart';
  13. import 'package:photos/utils/exif_util.dart';
  14. import 'package:photos/utils/file_uploader_util.dart';
  15. //Todo: files with no location data have lat and long set to 0.0. This should ideally be null.
  16. class EnteFile {
  17. int? generatedID;
  18. int? uploadedFileID;
  19. int? ownerID;
  20. int? collectionID;
  21. String? localID;
  22. String? title;
  23. String? deviceFolder;
  24. int? creationTime;
  25. int? modificationTime;
  26. int? updationTime;
  27. int? addedTime;
  28. Location? location;
  29. late FileType fileType;
  30. int? fileSubType;
  31. int? duration;
  32. String? exif;
  33. String? hash;
  34. int? metadataVersion;
  35. String? encryptedKey;
  36. String? keyDecryptionNonce;
  37. String? fileDecryptionHeader;
  38. String? thumbnailDecryptionHeader;
  39. String? metadataDecryptionHeader;
  40. int? fileSize;
  41. String? mMdEncodedJson;
  42. int mMdVersion = 0;
  43. MagicMetadata? _mmd;
  44. MagicMetadata get magicMetadata =>
  45. _mmd ?? MagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
  46. set magicMetadata(val) => _mmd = val;
  47. // public magic metadata is shared if during file/album sharing
  48. String? pubMmdEncodedJson;
  49. int pubMmdVersion = 0;
  50. PubMagicMetadata? _pubMmd;
  51. PubMagicMetadata? get pubMagicMetadata =>
  52. _pubMmd ?? PubMagicMetadata.fromEncodedJson(pubMmdEncodedJson ?? '{}');
  53. set pubMagicMetadata(val) => _pubMmd = val;
  54. // in Version 1, live photo hash is stored as zip's hash.
  55. // in V2: LivePhoto hash is stored as imgHash:vidHash
  56. static const kCurrentMetadataVersion = 2;
  57. static final _logger = Logger('File');
  58. EnteFile();
  59. static Future<EnteFile> fromAsset(String pathName, AssetEntity asset) async {
  60. final EnteFile file = EnteFile();
  61. file.localID = asset.id;
  62. file.title = asset.title;
  63. file.deviceFolder = pathName;
  64. file.location =
  65. Location(latitude: asset.latitude, longitude: asset.longitude);
  66. file.fileType = fileTypeFromAsset(asset);
  67. file.creationTime = parseFileCreationTime(file.title, asset);
  68. file.modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
  69. file.fileSubType = asset.subtype;
  70. file.metadataVersion = kCurrentMetadataVersion;
  71. return file;
  72. }
  73. static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
  74. int creationTime = asset.createDateTime.microsecondsSinceEpoch;
  75. final int modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
  76. if (creationTime >= jan011981Time) {
  77. // assuming that fileSystem is returning correct creationTime.
  78. // During upload, this might get overridden with exif Creation time
  79. // When the assetModifiedTime is less than creationTime, than just use
  80. // that as creationTime. This is to handle cases where file might be
  81. // copied to the fileSystem from somewhere else See #https://superuser.com/a/1091147
  82. if (modificationTime >= jan011981Time &&
  83. modificationTime < creationTime) {
  84. _logger.info(
  85. 'LocalID: ${asset.id} modification time is less than creation time. Using modification time as creation time',
  86. );
  87. creationTime = modificationTime;
  88. }
  89. return creationTime;
  90. } else {
  91. if (modificationTime >= jan011981Time) {
  92. creationTime = modificationTime;
  93. } else {
  94. creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
  95. }
  96. try {
  97. final parsedDateTime = parseDateTimeFromFileNameV2(
  98. basenameWithoutExtension(fileTitle ?? ""),
  99. );
  100. if (parsedDateTime != null) {
  101. creationTime = parsedDateTime.microsecondsSinceEpoch;
  102. }
  103. } catch (e) {
  104. // ignore
  105. }
  106. }
  107. return creationTime;
  108. }
  109. Future<AssetEntity?> get getAsset {
  110. if (localID == null) {
  111. return Future.value(null);
  112. }
  113. return AssetEntity.fromId(localID!);
  114. }
  115. void applyMetadata(Map<String, dynamic> metadata) {
  116. localID = metadata["localID"];
  117. title = metadata["title"];
  118. deviceFolder = metadata["deviceFolder"];
  119. creationTime = metadata["creationTime"] ?? 0;
  120. modificationTime = metadata["modificationTime"] ?? creationTime;
  121. final latitude = double.tryParse(metadata["latitude"].toString());
  122. final longitude = double.tryParse(metadata["longitude"].toString());
  123. if (latitude == null || longitude == null) {
  124. location = null;
  125. } else {
  126. location = Location(latitude: latitude, longitude: longitude);
  127. }
  128. fileType = getFileType(metadata["fileType"] ?? -1);
  129. fileSubType = metadata["subType"] ?? -1;
  130. duration = metadata["duration"] ?? 0;
  131. exif = metadata["exif"];
  132. hash = metadata["hash"];
  133. // handle past live photos upload from web client
  134. if (hash == null &&
  135. fileType == FileType.livePhoto &&
  136. metadata.containsKey('imageHash') &&
  137. metadata.containsKey('videoHash')) {
  138. // convert to imgHash:vidHash
  139. hash =
  140. '${metadata['imageHash']}$kLivePhotoHashSeparator${metadata['videoHash']}';
  141. }
  142. metadataVersion = metadata["version"] ?? 0;
  143. }
  144. Future<Map<String, dynamic>> getMetadataForUpload(
  145. MediaUploadData mediaUploadData,
  146. ) async {
  147. final asset = await getAsset;
  148. // asset can be null for files shared to app
  149. if (asset != null) {
  150. fileSubType = asset.subtype;
  151. if (fileType == FileType.video) {
  152. duration = asset.duration;
  153. }
  154. }
  155. bool hasExifTime = false;
  156. if ((fileType == FileType.image || fileType == FileType.video) &&
  157. mediaUploadData.sourceFile != null) {
  158. final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!);
  159. if (exifData != null) {
  160. if (fileType == FileType.image) {
  161. final exifTime = await getCreationTimeFromEXIF(null, exifData);
  162. if (exifTime != null) {
  163. hasExifTime = true;
  164. creationTime = exifTime.microsecondsSinceEpoch;
  165. }
  166. }
  167. if (Platform.isAndroid) {
  168. //Fix for missing location data in lower android versions.
  169. final Location? exifLocation = locationFromExif(exifData);
  170. if (Location.isValidLocation(exifLocation)) {
  171. location = exifLocation;
  172. }
  173. }
  174. }
  175. }
  176. // Try to get the timestamp from fileName. In case of iOS, file names are
  177. // generic IMG_XXXX, so only parse it on Android devices
  178. if (!hasExifTime && Platform.isAndroid && title != null) {
  179. final timeFromFileName = parseDateTimeFromFileNameV2(title!);
  180. if (timeFromFileName != null) {
  181. // only use timeFromFileName if the existing creationTime and
  182. // timeFromFilename belongs to different date.
  183. // This is done because many times the fileTimeStamp will only give us
  184. // the date, not time value but the photo_manager's creation time will
  185. // contain the time.
  186. final bool useFileTimeStamp = creationTime == null ||
  187. !areFromSameDay(
  188. creationTime!,
  189. timeFromFileName.microsecondsSinceEpoch,
  190. );
  191. if (useFileTimeStamp) {
  192. creationTime = timeFromFileName.microsecondsSinceEpoch;
  193. }
  194. }
  195. }
  196. hash = mediaUploadData.hashData?.fileHash;
  197. return metadata;
  198. }
  199. Map<String, dynamic> get metadata {
  200. final metadata = <String, dynamic>{};
  201. metadata["localID"] = isSharedMediaToAppSandbox ? null : localID;
  202. metadata["title"] = title;
  203. metadata["deviceFolder"] = deviceFolder;
  204. metadata["creationTime"] = creationTime;
  205. metadata["modificationTime"] = modificationTime;
  206. metadata["fileType"] = fileType.index;
  207. if (location != null &&
  208. location!.latitude != null &&
  209. location!.longitude != null) {
  210. metadata["latitude"] = location!.latitude;
  211. metadata["longitude"] = location!.longitude;
  212. }
  213. if (fileSubType != null) {
  214. metadata["subType"] = fileSubType;
  215. }
  216. if (duration != null) {
  217. metadata["duration"] = duration;
  218. }
  219. if (hash != null) {
  220. metadata["hash"] = hash;
  221. }
  222. if (metadataVersion != null) {
  223. metadata["version"] = metadataVersion;
  224. }
  225. return metadata;
  226. }
  227. String get downloadUrl {
  228. final endpoint = Configuration.instance.getHttpEndpoint();
  229. if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) {
  230. return endpoint + "/files/download/" + uploadedFileID.toString();
  231. } else {
  232. return "https://files.ente.io/?fileID=" + uploadedFileID.toString();
  233. }
  234. }
  235. String? get caption {
  236. return pubMagicMetadata?.caption;
  237. }
  238. String get thumbnailUrl {
  239. final endpoint = Configuration.instance.getHttpEndpoint();
  240. if (endpoint != kDefaultProductionEndpoint || flagService.disableCFWorker) {
  241. return endpoint + "/files/preview/" + uploadedFileID.toString();
  242. } else {
  243. return "https://thumbnails.ente.io/?fileID=" + uploadedFileID.toString();
  244. }
  245. }
  246. String get displayName {
  247. if (pubMagicMetadata != null && pubMagicMetadata!.editedName != null) {
  248. return pubMagicMetadata!.editedName!;
  249. }
  250. if (title == null && kDebugMode) _logger.severe('File title is null');
  251. return title ?? '';
  252. }
  253. // return 0 if the height is not available
  254. int get height {
  255. return pubMagicMetadata?.h ?? 0;
  256. }
  257. int get width {
  258. return pubMagicMetadata?.w ?? 0;
  259. }
  260. bool get hasDimensions {
  261. return height != 0 && width != 0;
  262. }
  263. // returns true if the file isn't available in the user's gallery
  264. bool get isRemoteFile {
  265. return localID == null && uploadedFileID != null;
  266. }
  267. bool get isUploaded {
  268. return uploadedFileID != null;
  269. }
  270. bool get isSharedMediaToAppSandbox {
  271. return localID != null &&
  272. (localID!.startsWith(oldSharedMediaIdentifier) ||
  273. localID!.startsWith(sharedMediaIdentifier));
  274. }
  275. bool get hasLocation {
  276. return location != null &&
  277. ((location!.longitude ?? 0) != 0 || (location!.latitude ?? 0) != 0);
  278. }
  279. @override
  280. String toString() {
  281. return '''File(generatedID: $generatedID, localID: $localID, title: $title,
  282. type: $fileType, uploadedFileId: $uploadedFileID, modificationTime: $modificationTime,
  283. ownerID: $ownerID, collectionID: $collectionID, updationTime: $updationTime)''';
  284. }
  285. @override
  286. bool operator ==(Object o) {
  287. if (identical(this, o)) return true;
  288. return o is EnteFile &&
  289. o.generatedID == generatedID &&
  290. o.uploadedFileID == uploadedFileID &&
  291. o.localID == localID;
  292. }
  293. @override
  294. int get hashCode {
  295. return generatedID.hashCode ^ uploadedFileID.hashCode ^ localID.hashCode;
  296. }
  297. String get tag {
  298. return "local_" +
  299. localID.toString() +
  300. ":remote_" +
  301. uploadedFileID.toString() +
  302. ":generated_" +
  303. generatedID.toString();
  304. }
  305. String cacheKey() {
  306. // todo: Neeraj: 19thJuly'22: evaluate and add fileHash as the key?
  307. return localID ?? uploadedFileID?.toString() ?? generatedID.toString();
  308. }
  309. EnteFile copyWith({
  310. int? generatedID,
  311. int? uploadedFileID,
  312. int? ownerID,
  313. int? collectionID,
  314. String? localID,
  315. String? title,
  316. String? deviceFolder,
  317. int? creationTime,
  318. int? modificationTime,
  319. int? updationTime,
  320. int? addedTime,
  321. Location? location,
  322. FileType? fileType,
  323. int? fileSubType,
  324. int? duration,
  325. String? exif,
  326. String? hash,
  327. int? metadataVersion,
  328. String? encryptedKey,
  329. String? keyDecryptionNonce,
  330. String? fileDecryptionHeader,
  331. String? thumbnailDecryptionHeader,
  332. String? metadataDecryptionHeader,
  333. int? fileSize,
  334. String? mMdEncodedJson,
  335. int? mMdVersion,
  336. MagicMetadata? magicMetadata,
  337. String? pubMmdEncodedJson,
  338. int? pubMmdVersion,
  339. PubMagicMetadata? pubMagicMetadata,
  340. }) {
  341. return EnteFile()
  342. ..generatedID = generatedID ?? this.generatedID
  343. ..uploadedFileID = uploadedFileID ?? this.uploadedFileID
  344. ..ownerID = ownerID ?? this.ownerID
  345. ..collectionID = collectionID ?? this.collectionID
  346. ..localID = localID ?? this.localID
  347. ..title = title ?? this.title
  348. ..deviceFolder = deviceFolder ?? this.deviceFolder
  349. ..creationTime = creationTime ?? this.creationTime
  350. ..modificationTime = modificationTime ?? this.modificationTime
  351. ..updationTime = updationTime ?? this.updationTime
  352. ..addedTime = addedTime ?? this.addedTime
  353. ..location = location ?? this.location
  354. ..fileType = fileType ?? this.fileType
  355. ..fileSubType = fileSubType ?? this.fileSubType
  356. ..duration = duration ?? this.duration
  357. ..exif = exif ?? this.exif
  358. ..hash = hash ?? this.hash
  359. ..metadataVersion = metadataVersion ?? this.metadataVersion
  360. ..encryptedKey = encryptedKey ?? this.encryptedKey
  361. ..keyDecryptionNonce = keyDecryptionNonce ?? this.keyDecryptionNonce
  362. ..fileDecryptionHeader = fileDecryptionHeader ?? this.fileDecryptionHeader
  363. ..thumbnailDecryptionHeader =
  364. thumbnailDecryptionHeader ?? this.thumbnailDecryptionHeader
  365. ..metadataDecryptionHeader =
  366. metadataDecryptionHeader ?? this.metadataDecryptionHeader
  367. ..fileSize = fileSize ?? this.fileSize
  368. ..mMdEncodedJson = mMdEncodedJson ?? this.mMdEncodedJson
  369. ..mMdVersion = mMdVersion ?? this.mMdVersion
  370. ..magicMetadata = magicMetadata ?? this.magicMetadata
  371. ..pubMmdEncodedJson = pubMmdEncodedJson ?? this.pubMmdEncodedJson
  372. ..pubMmdVersion = pubMmdVersion ?? this.pubMmdVersion
  373. ..pubMagicMetadata = pubMagicMetadata ?? this.pubMagicMetadata;
  374. }
  375. }