file.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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/ente_file.dart';
  9. import 'package:photos/models/file_type.dart';
  10. import 'package:photos/models/location/location.dart';
  11. import "package:photos/models/metadata/file_magic.dart";
  12. import 'package:photos/services/feature_flag_service.dart';
  13. import 'package:photos/utils/date_time_util.dart';
  14. import 'package:photos/utils/exif_util.dart';
  15. import 'package:photos/utils/file_uploader_util.dart';
  16. class File extends 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. File();
  59. static Future<File> fromAsset(String pathName, AssetEntity asset) async {
  60. final File file = File();
  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. if (creationTime >= jan011981Time) {
  76. // assuming that fileSystem is returning correct creationTime.
  77. // During upload, this might get overridden with exif Creation time
  78. return creationTime;
  79. } else {
  80. if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
  81. creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
  82. } else {
  83. creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
  84. }
  85. try {
  86. final parsedDateTime = parseDateTimeFromFileNameV2(
  87. basenameWithoutExtension(fileTitle ?? ""),
  88. );
  89. if (parsedDateTime != null) {
  90. creationTime = parsedDateTime.microsecondsSinceEpoch;
  91. }
  92. } catch (e) {
  93. // ignore
  94. }
  95. }
  96. return creationTime;
  97. }
  98. Future<AssetEntity?> get getAsset {
  99. if (localID == null) {
  100. return Future.value(null);
  101. }
  102. return AssetEntity.fromId(localID!);
  103. }
  104. void applyMetadata(Map<String, dynamic> metadata) {
  105. localID = metadata["localID"];
  106. title = metadata["title"];
  107. deviceFolder = metadata["deviceFolder"];
  108. creationTime = metadata["creationTime"] ?? 0;
  109. modificationTime = metadata["modificationTime"] ?? creationTime;
  110. final latitude = double.tryParse(metadata["latitude"].toString());
  111. final longitude = double.tryParse(metadata["longitude"].toString());
  112. if (latitude == null || longitude == null) {
  113. location = null;
  114. } else {
  115. location = Location(latitude: latitude, longitude: longitude);
  116. }
  117. fileType = getFileType(metadata["fileType"] ?? -1);
  118. fileSubType = metadata["subType"] ?? -1;
  119. duration = metadata["duration"] ?? 0;
  120. exif = metadata["exif"];
  121. hash = metadata["hash"];
  122. // handle past live photos upload from web client
  123. if (hash == null &&
  124. fileType == FileType.livePhoto &&
  125. metadata.containsKey('imageHash') &&
  126. metadata.containsKey('videoHash')) {
  127. // convert to imgHash:vidHash
  128. hash =
  129. '${metadata['imageHash']}$kLivePhotoHashSeparator${metadata['videoHash']}';
  130. }
  131. metadataVersion = metadata["version"] ?? 0;
  132. }
  133. Future<Map<String, dynamic>> getMetadataForUpload(
  134. MediaUploadData mediaUploadData,
  135. ) async {
  136. final asset = await getAsset;
  137. // asset can be null for files shared to app
  138. if (asset != null) {
  139. fileSubType = asset.subtype;
  140. if (fileType == FileType.video) {
  141. duration = asset.duration;
  142. }
  143. }
  144. bool hasExifTime = false;
  145. if ((fileType == FileType.image || fileType == FileType.video) &&
  146. mediaUploadData.sourceFile != null) {
  147. final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!);
  148. if (exifData != null) {
  149. if (fileType == FileType.image) {
  150. final exifTime = await getCreationTimeFromEXIF(null, exifData);
  151. if (exifTime != null) {
  152. hasExifTime = true;
  153. creationTime = exifTime.microsecondsSinceEpoch;
  154. }
  155. }
  156. if (Platform.isAndroid) {
  157. //Fix for missing location data in lower android versions.
  158. final Location? exifLocation = locationFromExif(exifData);
  159. if (Location.isValidLocation(exifLocation)) {
  160. location = exifLocation;
  161. }
  162. }
  163. }
  164. }
  165. // Try to get the timestamp from fileName. In case of iOS, file names are
  166. // generic IMG_XXXX, so only parse it on Android devices
  167. if (!hasExifTime && Platform.isAndroid && title != null) {
  168. final timeFromFileName = parseDateTimeFromFileNameV2(title!);
  169. if (timeFromFileName != null) {
  170. // only use timeFromFileName if the existing creationTime and
  171. // timeFromFilename belongs to different date.
  172. // This is done because many times the fileTimeStamp will only give us
  173. // the date, not time value but the photo_manager's creation time will
  174. // contain the time.
  175. final bool useFileTimeStamp = creationTime == null ||
  176. !areFromSameDay(
  177. creationTime!,
  178. timeFromFileName.microsecondsSinceEpoch,
  179. );
  180. if (useFileTimeStamp) {
  181. creationTime = timeFromFileName.microsecondsSinceEpoch;
  182. }
  183. }
  184. }
  185. hash = mediaUploadData.hashData?.fileHash;
  186. return metadata;
  187. }
  188. Map<String, dynamic> get metadata {
  189. final metadata = <String, dynamic>{};
  190. metadata["localID"] = isSharedMediaToAppSandbox ? null : localID;
  191. metadata["title"] = title;
  192. metadata["deviceFolder"] = deviceFolder;
  193. metadata["creationTime"] = creationTime;
  194. metadata["modificationTime"] = modificationTime;
  195. metadata["fileType"] = fileType.index;
  196. if (location != null &&
  197. location!.latitude != null &&
  198. location!.longitude != null) {
  199. metadata["latitude"] = location!.latitude;
  200. metadata["longitude"] = location!.longitude;
  201. }
  202. if (fileSubType != null) {
  203. metadata["subType"] = fileSubType;
  204. }
  205. if (duration != null) {
  206. metadata["duration"] = duration;
  207. }
  208. if (hash != null) {
  209. metadata["hash"] = hash;
  210. }
  211. if (metadataVersion != null) {
  212. metadata["version"] = metadataVersion;
  213. }
  214. return metadata;
  215. }
  216. String get downloadUrl {
  217. final endpoint = Configuration.instance.getHttpEndpoint();
  218. if (endpoint != kDefaultProductionEndpoint ||
  219. FeatureFlagService.instance.disableCFWorker()) {
  220. return endpoint + "/files/download/" + uploadedFileID.toString();
  221. } else {
  222. return "https://files.ente.io/?fileID=" + uploadedFileID.toString();
  223. }
  224. }
  225. String? get caption {
  226. return pubMagicMetadata?.caption;
  227. }
  228. String get thumbnailUrl {
  229. final endpoint = Configuration.instance.getHttpEndpoint();
  230. if (endpoint != kDefaultProductionEndpoint ||
  231. FeatureFlagService.instance.disableCFWorker()) {
  232. return endpoint + "/files/preview/" + uploadedFileID.toString();
  233. } else {
  234. return "https://thumbnails.ente.io/?fileID=" + uploadedFileID.toString();
  235. }
  236. }
  237. String get displayName {
  238. if (pubMagicMetadata != null && pubMagicMetadata!.editedName != null) {
  239. return pubMagicMetadata!.editedName!;
  240. }
  241. if (title == null && kDebugMode) _logger.severe('File title is null');
  242. return title ?? '';
  243. }
  244. // return 0 if the height is not available
  245. int get height {
  246. return pubMagicMetadata?.h ?? 0;
  247. }
  248. int get width {
  249. return pubMagicMetadata?.w ?? 0;
  250. }
  251. // returns true if the file isn't available in the user's gallery
  252. bool get isRemoteFile {
  253. return localID == null && uploadedFileID != null;
  254. }
  255. bool get isUploaded {
  256. return uploadedFileID != null;
  257. }
  258. bool get isSharedMediaToAppSandbox {
  259. return localID != null &&
  260. (localID!.startsWith(oldSharedMediaIdentifier) ||
  261. localID!.startsWith(sharedMediaIdentifier));
  262. }
  263. bool get hasLocation {
  264. return location != null &&
  265. ((location!.longitude ?? 0) != 0 || (location!.latitude ?? 0) != 0);
  266. }
  267. @override
  268. String toString() {
  269. return '''File(generatedID: $generatedID, localID: $localID, title: $title,
  270. uploadedFileId: $uploadedFileID, modificationTime: $modificationTime,
  271. ownerID: $ownerID, collectionID: $collectionID, updationTime: $updationTime)''';
  272. }
  273. @override
  274. bool operator ==(Object o) {
  275. if (identical(this, o)) return true;
  276. return o is File &&
  277. o.generatedID == generatedID &&
  278. o.uploadedFileID == uploadedFileID &&
  279. o.localID == localID;
  280. }
  281. @override
  282. int get hashCode {
  283. return generatedID.hashCode ^ uploadedFileID.hashCode ^ localID.hashCode;
  284. }
  285. String get tag {
  286. return "local_" +
  287. localID.toString() +
  288. ":remote_" +
  289. uploadedFileID.toString() +
  290. ":generated_" +
  291. generatedID.toString();
  292. }
  293. @override
  294. String cacheKey() {
  295. // todo: Neeraj: 19thJuly'22: evaluate and add fileHash as the key?
  296. return localID ?? uploadedFileID?.toString() ?? generatedID.toString();
  297. }
  298. }