ente/lib/utils/file_uploader_util.dart

209 lines
7.2 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:archive/archive_io.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:motionphoto/motionphoto.dart';
2021-08-03 11:58:17 +00:00
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
2021-08-03 11:58:17 +00:00
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/models/file.dart' as ente;
import 'package:photos/models/file_type.dart';
2021-07-21 20:47:43 +00:00
import 'package:photos/models/location.dart';
import 'package:photos/utils/crypto_util.dart';
2021-10-30 05:51:23 +00:00
import 'package:photos/utils/file_util.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
final _logger = Logger("FileUtil");
const kMaximumThumbnailCompressionAttempts = 2;
class MediaUploadData {
final io.File sourceFile;
final Uint8List thumbnail;
final bool isDeleted;
// presents the hash for the original video or image file.
// for livePhotos, fileHash represents the image hash value
final String fileHash;
final String liveVideoHash;
final String zipHash;
MediaUploadData(
this.sourceFile,
this.thumbnail,
this.isDeleted, {
this.fileHash,
this.liveVideoHash,
this.zipHash,
});
}
Future<MediaUploadData> getUploadDataFromEnteFile(ente.File file) async {
2021-07-24 17:33:59 +00:00
if (file.isSharedMediaToAppSandbox()) {
return await _getMediaUploadDataFromAppCache(file);
} else {
return await _getMediaUploadDataFromAssetFile(file);
}
}
Future<MediaUploadData> _getMediaUploadDataFromAssetFile(ente.File file) async {
io.File sourceFile;
Uint8List thumbnailData;
bool isDeleted;
String fileHash, livePhotoVideoHash, zipHash;
// The timeouts are to safeguard against https://github.com/CaiJingLong/flutter_photo_manager/issues/467
2022-07-04 06:02:17 +00:00
final asset = await file
.getAsset()
.timeout(const Duration(seconds: 3))
.catchError((e) async {
if (e is TimeoutException) {
_logger.info("Asset fetch timed out for " + file.toString());
return await file.getAsset();
} else {
throw e;
}
});
if (asset == null) {
throw InvalidFileError("asset is null");
}
sourceFile = await asset.originFile
2022-07-04 06:02:17 +00:00
.timeout(const Duration(seconds: 3))
.catchError((e) async {
if (e is TimeoutException) {
_logger.info("Origin file fetch timed out for " + file.toString());
return await asset.originFile;
} else {
throw e;
}
});
if (sourceFile == null || !sourceFile.existsSync()) {
throw InvalidFileError("source fill is null or do not exist");
}
2021-08-03 11:58:17 +00:00
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
await _decorateEnteFileData(file, asset);
fileHash = Sodium.bin2base64(await CryptoUtil.getHash(sourceFile));
2021-08-03 11:58:17 +00:00
if (file.fileType == FileType.livePhoto && io.Platform.isIOS) {
final io.File videoUrl = await Motionphoto.getLivePhotoFile(file.localID);
if (videoUrl == null || !videoUrl.existsSync()) {
2022-06-06 05:24:05 +00:00
String errMsg =
"missing livePhoto url for ${file.toString()} with subType ${file.fileSubType}";
_logger.severe(errMsg);
throw InvalidFileUploadState(errMsg);
}
livePhotoVideoHash = Sodium.bin2base64(await CryptoUtil.getHash(videoUrl));
final tempPath = Configuration.instance.getTempDirectory();
2021-08-06 08:52:07 +00:00
// .elp -> ente live photo
final livePhotoPath = tempPath + file.generatedID.toString() + ".elp";
_logger.fine("Uploading zipped live photo from " + livePhotoPath);
var encoder = ZipFileEncoder();
encoder.create(livePhotoPath);
2021-08-03 11:58:17 +00:00
encoder.addFile(videoUrl, "video" + extension(videoUrl.path));
encoder.addFile(sourceFile, "image" + extension(sourceFile.path));
encoder.close();
2021-08-03 11:58:17 +00:00
// delete the temporary video and image copy (only in IOS)
if (io.Platform.isIOS) {
2021-08-09 06:21:09 +00:00
await sourceFile.delete();
2021-08-03 11:58:17 +00:00
}
// new sourceFile which needs to be uploaded
sourceFile = io.File(livePhotoPath);
zipHash = Sodium.bin2base64(await CryptoUtil.getHash(sourceFile));
}
2022-05-12 03:27:32 +00:00
thumbnailData = await asset.thumbnailDataWithSize(
2022-07-04 06:02:17 +00:00
const ThumbnailSize(kThumbnailLargeSize, kThumbnailLargeSize),
2021-07-21 20:47:43 +00:00
quality: kThumbnailQuality,
);
if (thumbnailData == null) {
throw InvalidFileError("unable to get asset thumbData");
}
int compressionAttempts = 0;
2021-07-21 20:47:43 +00:00
while (thumbnailData.length > kThumbnailDataLimit &&
compressionAttempts < kMaximumThumbnailCompressionAttempts) {
_logger.info("Thumbnail size " + thumbnailData.length.toString());
thumbnailData = await compressThumbnail(thumbnailData);
_logger
.info("Compressed thumbnail size " + thumbnailData.length.toString());
compressionAttempts++;
}
isDeleted = asset == null || !(await asset.exists);
return MediaUploadData(
sourceFile,
thumbnailData,
isDeleted,
fileHash: fileHash,
liveVideoHash: livePhotoVideoHash,
zipHash: zipHash,
);
}
Future<void> _decorateEnteFileData(ente.File file, AssetEntity asset) async {
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
if (file.location == null ||
(file.location.latitude == 0 && file.location.longitude == 0)) {
final latLong = await asset.latlngAsync();
file.location = Location(latLong.latitude, latLong.longitude);
}
if (file.title == null || file.title.isEmpty) {
2022-07-28 07:57:16 +00:00
_logger.warning("Title was missing ${file.tag()}");
file.title = await asset.titleAsync;
}
}
Future<MediaUploadData> _getMediaUploadDataFromAppCache(ente.File file) async {
io.File sourceFile;
Uint8List thumbnailData;
bool isDeleted = false;
var localPath = getSharedMediaFilePath(file);
sourceFile = io.File(localPath);
if (!sourceFile.existsSync()) {
_logger.warning("File doesn't exist in app sandbox");
throw InvalidFileError("File doesn't exist in app sandbox");
}
try {
thumbnailData = await getThumbnailFromInAppCacheFile(file);
return MediaUploadData(sourceFile, thumbnailData, isDeleted);
} catch (e, s) {
_logger.severe("failed to generate thumbnail", e, s);
throw InvalidFileError(
2022-06-11 08:23:52 +00:00
"thumbnail generation failed for fileType: ${file.fileType.toString()}",
);
}
}
Future<Uint8List> getThumbnailFromInAppCacheFile(ente.File file) async {
2021-07-24 13:38:47 +00:00
var localFile = io.File(getSharedMediaFilePath(file));
if (!localFile.existsSync()) {
return null;
}
if (file.fileType == FileType.video) {
final thumbnailFilePath = await VideoThumbnail.thumbnailFile(
video: localFile.path,
imageFormat: ImageFormat.JPEG,
thumbnailPath: (await getTemporaryDirectory()).path,
maxWidth: kThumbnailLargeSize,
2021-09-20 15:18:20 +00:00
quality: 80,
);
localFile = io.File(thumbnailFilePath);
}
2021-08-09 06:30:30 +00:00
var thumbnailData = await localFile.readAsBytes();
int compressionAttempts = 0;
while (thumbnailData.length > kThumbnailDataLimit &&
compressionAttempts < kMaximumThumbnailCompressionAttempts) {
_logger.info("Thumbnail size " + thumbnailData.length.toString());
thumbnailData = await compressThumbnail(thumbnailData);
_logger
.info("Compressed thumbnail size " + thumbnailData.length.toString());
compressionAttempts++;
}
return thumbnailData;
}