ente/lib/utils/file_uploader.dart

826 lines
26 KiB
Dart
Raw Normal View History

2020-11-09 12:28:43 +00:00
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io' as io;
2021-02-13 12:28:35 +00:00
import 'dart:math';
2021-07-27 16:45:16 +00:00
import 'dart:typed_data';
2020-11-16 16:35:16 +00:00
import 'package:connectivity/connectivity.dart';
import 'package:dio/dio.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:photos/core/configuration.dart';
2021-02-26 09:21:47 +00:00
import 'package:photos/core/errors.dart';
import 'package:photos/core/event_bus.dart';
2020-11-19 18:22:30 +00:00
import 'package:photos/core/network.dart';
2020-11-09 16:10:43 +00:00
import 'package:photos/db/files_db.dart';
import 'package:photos/db/upload_locks_db.dart';
import 'package:photos/events/files_updated_event.dart';
2021-04-21 13:09:18 +00:00
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/events/subscription_purchased_event.dart';
2021-05-07 17:27:11 +00:00
import 'package:photos/main.dart';
import 'package:photos/models/encryption_result.dart';
import 'package:photos/models/file.dart';
2022-06-18 14:52:33 +00:00
import 'package:photos/models/file_type.dart';
import 'package:photos/models/upload_url.dart';
import 'package:photos/services/collections_service.dart';
2021-06-18 06:46:56 +00:00
import 'package:photos/services/local_sync_service.dart';
2021-01-13 08:50:14 +00:00
import 'package:photos/services/sync_service.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_download_util.dart';
import 'package:photos/utils/file_uploader_util.dart';
import 'package:shared_preferences/shared_preferences.dart';
class FileUploader {
static const kMaximumConcurrentUploads = 4;
static const kMaximumConcurrentVideoUploads = 2;
static const kMaximumThumbnailCompressionAttempts = 2;
static const kMaximumUploadAttempts = 4;
static const kBlockedUploadsPollFrequency = Duration(seconds: 2);
static const kFileUploadTimeout = Duration(minutes: 50);
final _logger = Logger("FileUploader");
2020-11-19 18:22:30 +00:00
final _dio = Network.instance.getDio();
final _queue = LinkedHashMap<String, FileUploadItem>();
final _uploadLocks = UploadLocksDB.instance;
final kSafeBufferForLockExpiry = Duration(days: 1).inMicroseconds;
final kBGTaskDeathTimeout = Duration(seconds: 5).inMicroseconds;
final _uploadURLs = Queue<UploadURL>();
// Maintains the count of files in the current upload session.
// Upload session is the period between the first entry into the _queue and last entry out of the _queue
int _totalCountInUploadSession = 0;
2022-06-18 15:04:01 +00:00
// _uploadCounter indicates number of uploads which are currently in progress
int _uploadCounter = 0;
int _videoUploadCounter = 0;
2021-03-03 20:18:50 +00:00
ProcessType _processType;
bool _isBackground;
2021-03-04 14:59:32 +00:00
SharedPreferences _prefs;
FileUploader._privateConstructor() {
Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
_uploadURLFetchInProgress = null;
});
}
2020-10-21 16:20:41 +00:00
static FileUploader instance = FileUploader._privateConstructor();
Future<void> init(bool isBackground) async {
2021-03-04 14:59:32 +00:00
_prefs = await SharedPreferences.getInstance();
_isBackground = isBackground;
2021-03-03 20:18:50 +00:00
_processType =
isBackground ? ProcessType.background : ProcessType.foreground;
final currentTime = DateTime.now().microsecondsSinceEpoch;
await _uploadLocks.releaseLocksAcquiredByOwnerBefore(
2022-06-11 08:23:52 +00:00
_processType.toString(),
currentTime,
);
await _uploadLocks
.releaseAllLocksAcquiredBefore(currentTime - kSafeBufferForLockExpiry);
2021-05-07 17:27:11 +00:00
if (!isBackground) {
await _prefs.reload();
final isBGTaskDead = (_prefs.getInt(kLastBGTaskHeartBeatTime) ?? 0) <
(currentTime - kBGTaskDeathTimeout);
if (isBGTaskDead) {
await _uploadLocks.releaseLocksAcquiredByOwnerBefore(
2022-06-11 08:23:52 +00:00
ProcessType.background.toString(),
currentTime,
);
_logger.info("BG task was found dead, cleared all locks");
}
_pollBackgroundUploadStatus();
}
Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
if (event.type == EventType.deletedFromDevice ||
event.type == EventType.deletedFromEverywhere) {
2022-06-11 08:23:52 +00:00
removeFromQueueWhere(
(file) {
for (final updatedFile in event.updatedFiles) {
if (file.generatedID == updatedFile.generatedID) {
return true;
}
}
2022-06-11 08:23:52 +00:00
return false;
},
InvalidFileError("File already deleted"),
);
}
});
}
2020-11-09 16:10:43 +00:00
Future<File> upload(File file, int collectionID) {
// If the file hasn't been queued yet, queue it
_totalCountInUploadSession++;
if (!_queue.containsKey(file.localID)) {
2020-11-09 16:10:43 +00:00
final completer = Completer<File>();
_queue[file.localID] = FileUploadItem(file, collectionID, completer);
2020-11-09 12:28:43 +00:00
_pollQueue();
2020-11-09 16:10:43 +00:00
return completer.future;
2020-11-09 12:28:43 +00:00
}
2020-11-09 16:10:43 +00:00
// If the file exists in the queue for a matching collectionID,
// return the existing future
final item = _queue[file.localID];
2020-11-09 16:10:43 +00:00
if (item.collectionID == collectionID) {
_totalCountInUploadSession--;
2020-11-09 16:10:43 +00:00
return item.completer.future;
}
// Else wait for the existing upload to complete,
// and add it to the relevant collection
return item.completer.future.then((uploadedFile) {
return CollectionsService.instance
.addToCollection(collectionID, [uploadedFile]).then((aVoid) {
return uploadedFile;
});
});
}
2020-11-09 16:10:43 +00:00
Future<File> forceUpload(File file, int collectionID) async {
2022-06-11 08:23:52 +00:00
_logger.info(
"Force uploading " +
file.toString() +
" into collection " +
collectionID.toString(),
);
_totalCountInUploadSession++;
2020-11-09 16:10:43 +00:00
// If the file hasn't been queued yet, ez.
if (!_queue.containsKey(file.localID)) {
2020-11-19 14:59:01 +00:00
final completer = Completer<File>();
_queue[file.localID] = FileUploadItem(
2020-11-19 14:59:01 +00:00
file,
collectionID,
completer,
status: UploadStatus.in_progress,
);
_encryptAndUploadFileToCollection(file, collectionID, forcedUpload: true);
return completer.future;
2020-11-09 16:10:43 +00:00
}
var item = _queue[file.localID];
2020-11-09 16:10:43 +00:00
// If the file is being uploaded right now, wait and proceed
if (item.status == UploadStatus.in_progress ||
2021-03-04 17:32:57 +00:00
item.status == UploadStatus.in_background) {
_totalCountInUploadSession--;
2021-02-26 08:28:01 +00:00
final uploadedFile = await item.completer.future;
if (uploadedFile.collectionID == collectionID) {
// Do nothing
} else {
await CollectionsService.instance
.addToCollection(collectionID, [uploadedFile]);
}
return uploadedFile;
2020-11-09 16:10:43 +00:00
} else {
// If the file is yet to be processed,
2021-02-26 08:16:31 +00:00
// 1. Set the status to in_progress
// 2. Force upload the file
// 3. Add to the relevant collection
item = _queue[file.localID];
2021-02-26 08:16:31 +00:00
item.status = UploadStatus.in_progress;
2021-02-26 08:28:01 +00:00
final uploadedFile = await _encryptAndUploadFileToCollection(
2022-06-11 08:23:52 +00:00
file,
collectionID,
forcedUpload: true,
);
2021-02-26 08:28:01 +00:00
if (item.collectionID == collectionID) {
return uploadedFile;
} else {
await CollectionsService.instance
.addToCollection(item.collectionID, [uploadedFile]);
return uploadedFile;
}
2020-11-09 16:10:43 +00:00
}
}
int getCurrentSessionUploadCount() {
return _totalCountInUploadSession;
2021-03-22 07:14:16 +00:00
}
2021-02-25 16:14:27 +00:00
void clearQueue(final Error reason) {
final List<String> uploadsToBeRemoved = [];
_queue.entries
.where((entry) => entry.value.status == UploadStatus.not_started)
.forEach((pendingUpload) {
uploadsToBeRemoved.add(pendingUpload.key);
});
for (final id in uploadsToBeRemoved) {
2021-02-25 16:14:27 +00:00
_queue.remove(id).completer.completeError(reason);
}
_totalCountInUploadSession = 0;
}
void removeFromQueueWhere(final bool Function(File) fn, final Error reason) {
List<String> uploadsToBeRemoved = [];
_queue.entries
.where((entry) => entry.value.status == UploadStatus.not_started)
.forEach((pendingUpload) {
if (fn(pendingUpload.value.file)) {
uploadsToBeRemoved.add(pendingUpload.key);
}
});
for (final id in uploadsToBeRemoved) {
_queue.remove(id).completer.completeError(reason);
}
_totalCountInUploadSession -= uploadsToBeRemoved.length;
}
2020-11-09 12:28:43 +00:00
void _pollQueue() {
2021-01-13 08:50:14 +00:00
if (SyncService.instance.shouldStopSync()) {
2021-02-25 16:14:27 +00:00
clearQueue(SyncStopRequestedError());
2021-01-13 08:50:14 +00:00
}
2021-07-27 16:45:16 +00:00
if (_queue.isEmpty) {
// Upload session completed
_totalCountInUploadSession = 0;
return;
}
2022-06-18 15:04:01 +00:00
if (_uploadCounter < kMaximumConcurrentUploads) {
2022-06-18 14:52:33 +00:00
var pendingEntry = _queue.entries
2022-06-11 08:23:52 +00:00
.firstWhere(
(entry) => entry.value.status == UploadStatus.not_started,
orElse: () => null,
)
2020-11-16 16:35:16 +00:00
?.value;
2022-06-18 14:52:33 +00:00
2022-06-18 14:59:35 +00:00
if (pendingEntry != null &&
2022-06-18 14:54:25 +00:00
pendingEntry.file.fileType == FileType.video &&
2022-06-18 15:04:01 +00:00
_videoUploadCounter >= kMaximumConcurrentVideoUploads) {
2022-06-18 14:52:33 +00:00
// check if there's any non-video entry which can be queued for upload
pendingEntry = _queue.entries
.firstWhere(
(entry) =>
entry.value.status == UploadStatus.not_started &&
entry.value.file.fileType != FileType.video,
orElse: () => null,
)
?.value;
}
if (pendingEntry != null) {
pendingEntry.status = UploadStatus.in_progress;
2020-11-16 16:35:16 +00:00
_encryptAndUploadFileToCollection(
2022-06-18 14:52:33 +00:00
pendingEntry.file,
pendingEntry.collectionID,
2022-06-11 08:23:52 +00:00
);
2020-11-16 16:35:16 +00:00
}
2020-11-09 12:28:43 +00:00
}
}
2022-06-11 08:23:52 +00:00
Future<File> _encryptAndUploadFileToCollection(
File file,
int collectionID, {
bool forcedUpload = false,
}) async {
2022-06-18 15:04:01 +00:00
_uploadCounter++;
2022-06-18 14:52:33 +00:00
if (file.fileType == FileType.video) {
2022-06-18 15:04:01 +00:00
_videoUploadCounter++;
2022-06-18 14:52:33 +00:00
}
final localID = file.localID;
try {
2022-06-11 08:23:52 +00:00
final uploadedFile =
await _tryToUpload(file, collectionID, forcedUpload).timeout(
kFileUploadTimeout,
onTimeout: () {
final message = "Upload timed out for file " + file.toString();
_logger.severe(message);
throw TimeoutException(message);
},
);
_queue.remove(localID).completer.complete(uploadedFile);
2021-02-26 08:16:31 +00:00
return uploadedFile;
} catch (e) {
if (e is LockAlreadyAcquiredError) {
_queue[localID].status = UploadStatus.in_background;
return _queue[localID].completer.future;
} else {
_queue.remove(localID).completer.completeError(e);
return null;
}
} finally {
2022-06-18 15:04:01 +00:00
_uploadCounter--;
2022-06-18 14:52:33 +00:00
if (file.fileType == FileType.video) {
2022-06-18 15:04:01 +00:00
_videoUploadCounter--;
2022-06-18 14:52:33 +00:00
}
2020-11-19 14:59:01 +00:00
_pollQueue();
}
}
Future<File> _tryToUpload(
2022-06-11 08:23:52 +00:00
File file,
int collectionID,
bool forcedUpload,
) async {
2020-11-16 16:35:16 +00:00
final connectivityResult = await (Connectivity().checkConnectivity());
2020-11-18 16:02:32 +00:00
var canUploadUnderCurrentNetworkConditions =
(connectivityResult == ConnectivityResult.wifi ||
Configuration.instance.shouldBackupOverMobileData());
if (!canUploadUnderCurrentNetworkConditions && !forcedUpload) {
2020-11-16 16:35:16 +00:00
throw WiFiUnavailableError();
}
final fileOnDisk = await FilesDB.instance.getFile(file.generatedID);
final wasAlreadyUploaded = fileOnDisk.uploadedFileID != null &&
2021-10-26 12:31:36 +00:00
fileOnDisk.updationTime != -1 &&
fileOnDisk.collectionID == collectionID;
if (wasAlreadyUploaded) {
return fileOnDisk;
}
2020-11-16 16:35:16 +00:00
try {
await _uploadLocks.acquireLock(
file.localID,
2021-03-03 20:18:50 +00:00
_processType.toString(),
DateTime.now().microsecondsSinceEpoch,
);
} catch (e) {
_logger.warning("Lock was already taken for " + file.toString());
throw LockAlreadyAcquiredError();
}
final tempDirectory = Configuration.instance.getTempDirectory();
final encryptedFilePath = tempDirectory +
file.generatedID.toString() +
(_isBackground ? "_bg" : "") +
".encrypted";
final encryptedThumbnailPath = tempDirectory +
file.generatedID.toString() +
"_thumbnail" +
(_isBackground ? "_bg" : "") +
".encrypted";
MediaUploadData mediaUploadData;
try {
2022-06-11 08:23:52 +00:00
_logger.info(
"Trying to upload " +
file.toString() +
", isForced: " +
forcedUpload.toString(),
);
try {
mediaUploadData = await getUploadDataFromEnteFile(file);
} catch (e) {
if (e is InvalidFileError) {
await _onInvalidFileError(file, e);
2021-05-13 15:30:34 +00:00
} else {
rethrow;
2021-05-13 15:30:34 +00:00
}
}
2021-07-27 16:45:16 +00:00
Uint8List key;
bool isUpdatedFile =
2021-10-26 12:31:36 +00:00
file.uploadedFileID != null && file.updationTime == -1;
if (isUpdatedFile) {
_logger.info("File was updated " + file.toString());
key = decryptFileKey(file);
} else {
key = null;
}
if (io.File(encryptedFilePath).existsSync()) {
2021-08-09 06:21:09 +00:00
await io.File(encryptedFilePath).delete();
}
final encryptedFile = io.File(encryptedFilePath);
final fileAttributes = await CryptoUtil.encryptFile(
mediaUploadData.sourceFile.path,
encryptedFilePath,
key: key,
);
var thumbnailData = mediaUploadData.thumbnail;
2020-11-22 18:07:40 +00:00
final encryptedThumbnailData =
2021-05-04 15:32:23 +00:00
await CryptoUtil.encryptChaCha(thumbnailData, fileAttributes.key);
if (io.File(encryptedThumbnailPath).existsSync()) {
2021-08-09 06:21:09 +00:00
await io.File(encryptedThumbnailPath).delete();
}
final encryptedThumbnailFile = io.File(encryptedThumbnailPath);
2021-08-09 06:26:11 +00:00
await encryptedThumbnailFile
.writeAsBytes(encryptedThumbnailData.encryptedData);
final thumbnailUploadURL = await _getUploadURL();
String thumbnailObjectKey =
await _putFile(thumbnailUploadURL, encryptedThumbnailFile);
2021-02-25 16:32:49 +00:00
final fileUploadURL = await _getUploadURL();
2021-11-27 15:04:21 +00:00
String fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
2021-02-25 16:32:49 +00:00
final metadata =
await file.getMetadataForUpload(mediaUploadData.sourceFile);
2021-05-04 15:32:23 +00:00
final encryptedMetadataData = await CryptoUtil.encryptChaCha(
2022-06-11 08:23:52 +00:00
utf8.encode(jsonEncode(metadata)),
fileAttributes.key,
);
final fileDecryptionHeader = Sodium.bin2base64(fileAttributes.header);
final thumbnailDecryptionHeader =
Sodium.bin2base64(encryptedThumbnailData.header);
final encryptedMetadata =
Sodium.bin2base64(encryptedMetadataData.encryptedData);
final metadataDecryptionHeader =
Sodium.bin2base64(encryptedMetadataData.header);
2021-03-17 21:07:17 +00:00
if (SyncService.instance.shouldStopSync()) {
throw SyncStopRequestedError();
}
File remoteFile;
if (isUpdatedFile) {
2021-03-03 16:04:45 +00:00
remoteFile = await _updateFile(
file,
fileObjectKey,
fileDecryptionHeader,
2021-08-09 06:33:47 +00:00
await encryptedFile.length(),
thumbnailObjectKey,
thumbnailDecryptionHeader,
2021-08-09 06:33:47 +00:00
await encryptedThumbnailFile.length(),
encryptedMetadata,
metadataDecryptionHeader,
);
// Update across all collections
2021-03-03 16:04:45 +00:00
await FilesDB.instance.updateUploadedFileAcrossCollections(remoteFile);
} else {
final encryptedFileKeyData = CryptoUtil.encryptSync(
fileAttributes.key,
CollectionsService.instance.getCollectionKey(collectionID),
);
final encryptedKey =
Sodium.bin2base64(encryptedFileKeyData.encryptedData);
final keyDecryptionNonce =
Sodium.bin2base64(encryptedFileKeyData.nonce);
2021-03-03 16:04:45 +00:00
remoteFile = await _uploadFile(
file,
collectionID,
encryptedKey,
keyDecryptionNonce,
fileAttributes,
fileObjectKey,
fileDecryptionHeader,
2021-08-09 06:33:47 +00:00
await encryptedFile.length(),
thumbnailObjectKey,
thumbnailDecryptionHeader,
2021-08-09 06:33:47 +00:00
await encryptedThumbnailFile.length(),
encryptedMetadata,
metadataDecryptionHeader,
);
if (mediaUploadData.isDeleted) {
_logger.info("File found to be deleted");
remoteFile.localID = null;
}
2021-03-03 16:04:45 +00:00
await FilesDB.instance.update(remoteFile);
}
if (!_isBackground) {
Bus.instance.fire(LocalPhotosUpdatedEvent([remoteFile]));
}
2021-03-03 16:04:45 +00:00
_logger.info("File upload complete for " + remoteFile.toString());
return remoteFile;
} catch (e, s) {
if (!(e is NoActiveSubscriptionError ||
e is StorageLimitExceededError ||
e is FileTooLargeForPlanError)) {
2021-02-26 08:31:28 +00:00
_logger.severe("File upload failed for " + file.toString(), e, s);
2021-02-02 16:35:38 +00:00
}
2021-07-27 16:45:16 +00:00
rethrow;
} finally {
2021-07-27 16:45:16 +00:00
if (io.Platform.isIOS &&
mediaUploadData != null &&
mediaUploadData.sourceFile != null) {
2021-08-09 06:21:09 +00:00
await mediaUploadData.sourceFile.delete();
}
if (io.File(encryptedFilePath).existsSync()) {
2021-08-09 06:21:09 +00:00
await io.File(encryptedFilePath).delete();
}
if (io.File(encryptedThumbnailPath).existsSync()) {
2021-08-09 06:21:09 +00:00
await io.File(encryptedThumbnailPath).delete();
}
2021-03-03 20:18:50 +00:00
await _uploadLocks.releaseLock(file.localID, _processType.toString());
}
}
Future _onInvalidFileError(File file, InvalidFileError e) async {
2021-08-22 05:22:30 +00:00
String ext = file.title == null ? "no title" : extension(file.title);
_logger.severe(
2022-06-11 08:23:52 +00:00
"Invalid file: (ext: $ext) encountered: " + file.toString(),
e,
);
await FilesDB.instance.deleteLocalFile(file);
2021-06-18 06:46:56 +00:00
await LocalSyncService.instance.trackInvalidFile(file);
throw e;
2021-02-26 09:04:02 +00:00
}
Future<File> _uploadFile(
File file,
int collectionID,
String encryptedKey,
String keyDecryptionNonce,
EncryptionResult fileAttributes,
String fileObjectKey,
String fileDecryptionHeader,
int fileSize,
String thumbnailObjectKey,
String thumbnailDecryptionHeader,
int thumbnailSize,
String encryptedMetadata,
2021-07-27 17:04:32 +00:00
String metadataDecryptionHeader, {
int attempt = 1,
}) async {
final request = {
"collectionID": collectionID,
"encryptedKey": encryptedKey,
"keyDecryptionNonce": keyDecryptionNonce,
"file": {
"objectKey": fileObjectKey,
"decryptionHeader": fileDecryptionHeader,
"size": fileSize,
},
"thumbnail": {
"objectKey": thumbnailObjectKey,
"decryptionHeader": thumbnailDecryptionHeader,
"size": thumbnailSize,
},
"metadata": {
"encryptedData": encryptedMetadata,
"decryptionHeader": metadataDecryptionHeader,
}
};
2021-02-02 19:01:32 +00:00
try {
final response = await _dio.post(
Configuration.instance.getHttpEndpoint() + "/files",
options: Options(
2022-06-11 08:23:52 +00:00
headers: {"X-Auth-Token": Configuration.instance.getToken()},
),
2021-02-02 19:01:32 +00:00
data: request,
);
final data = response.data;
file.uploadedFileID = data["id"];
file.collectionID = collectionID;
file.updationTime = data["updationTime"];
file.ownerID = data["ownerID"];
file.encryptedKey = encryptedKey;
file.keyDecryptionNonce = keyDecryptionNonce;
file.fileDecryptionHeader = fileDecryptionHeader;
file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
file.metadataDecryptionHeader = metadataDecryptionHeader;
return file;
} on DioError catch (e) {
if (e.response?.statusCode == 413) {
throw FileTooLargeForPlanError();
} else if (e.response?.statusCode == 426) {
_onStorageLimitExceeded();
2021-07-27 17:04:32 +00:00
} else if (attempt < kMaximumUploadAttempts) {
_logger.info("Upload file failed, will retry in 3 seconds");
await Future.delayed(Duration(seconds: 3));
return _uploadFile(
file,
collectionID,
encryptedKey,
keyDecryptionNonce,
2021-07-27 17:04:32 +00:00
fileAttributes,
fileObjectKey,
fileDecryptionHeader,
fileSize,
2021-07-27 17:04:32 +00:00
thumbnailObjectKey,
thumbnailDecryptionHeader,
thumbnailSize,
2021-07-27 17:04:32 +00:00
encryptedMetadata,
metadataDecryptionHeader,
attempt: attempt + 1,
2021-07-27 17:04:32 +00:00
);
2021-02-02 19:01:32 +00:00
}
2021-07-27 16:45:16 +00:00
rethrow;
2021-02-02 19:01:32 +00:00
}
}
Future<File> _updateFile(
File file,
String fileObjectKey,
String fileDecryptionHeader,
int fileSize,
String thumbnailObjectKey,
String thumbnailDecryptionHeader,
int thumbnailSize,
String encryptedMetadata,
2021-07-27 17:04:32 +00:00
String metadataDecryptionHeader, {
int attempt = 1,
}) async {
final request = {
"id": file.uploadedFileID,
"file": {
"objectKey": fileObjectKey,
"decryptionHeader": fileDecryptionHeader,
"size": fileSize,
},
"thumbnail": {
"objectKey": thumbnailObjectKey,
"decryptionHeader": thumbnailDecryptionHeader,
"size": thumbnailSize,
},
"metadata": {
"encryptedData": encryptedMetadata,
"decryptionHeader": metadataDecryptionHeader,
}
};
2021-02-02 19:01:32 +00:00
try {
2022-05-10 07:45:11 +00:00
final response = await _dio.put(
Configuration.instance.getHttpEndpoint() + "/files/update",
2021-02-02 19:01:32 +00:00
options: Options(
2022-06-11 08:23:52 +00:00
headers: {"X-Auth-Token": Configuration.instance.getToken()},
),
2021-02-02 19:01:32 +00:00
data: request,
);
final data = response.data;
file.uploadedFileID = data["id"];
file.updationTime = data["updationTime"];
file.fileDecryptionHeader = fileDecryptionHeader;
file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
file.metadataDecryptionHeader = metadataDecryptionHeader;
return file;
} on DioError catch (e) {
2021-03-03 04:58:36 +00:00
if (e.response?.statusCode == 426) {
_onStorageLimitExceeded();
2021-07-27 17:04:32 +00:00
} else if (attempt < kMaximumUploadAttempts) {
_logger.info("Update file failed, will retry in 3 seconds");
await Future.delayed(Duration(seconds: 3));
return _updateFile(
file,
fileObjectKey,
fileDecryptionHeader,
fileSize,
2021-07-27 17:04:32 +00:00
thumbnailObjectKey,
thumbnailDecryptionHeader,
thumbnailSize,
2021-07-27 17:04:32 +00:00
encryptedMetadata,
metadataDecryptionHeader,
attempt: attempt + 1,
2021-07-27 17:04:32 +00:00
);
2021-02-02 19:01:32 +00:00
}
2021-07-27 16:45:16 +00:00
rethrow;
2021-02-02 19:01:32 +00:00
}
}
2020-11-09 17:43:40 +00:00
Future<UploadURL> _getUploadURL() async {
if (_uploadURLs.isEmpty) {
2022-06-22 07:19:34 +00:00
await _fetchUploadURLs(_queue.length);
2020-11-09 17:43:40 +00:00
}
return _uploadURLs.removeFirst();
}
// can upload is a helper method to verify that user can actually upload
// new files or not based on their subscription plan and storage limit.
// To avoid creating new endpoint, we are using fetchUploadUrls as alternative
// method.
2022-06-22 07:19:34 +00:00
Future<void> canUpload(int fileCount) async {
return await _fetchUploadURLs(fileCount);
}
2020-11-09 17:43:40 +00:00
Future<void> _uploadURLFetchInProgress;
2022-06-22 07:19:34 +00:00
Future<void> _fetchUploadURLs(int fileCount) async {
2021-07-27 16:45:16 +00:00
_uploadURLFetchInProgress ??= Future<void>(() async {
try {
final response = await _dio.get(
Configuration.instance.getHttpEndpoint() + "/files/upload-urls",
queryParameters: {
2022-06-22 07:19:34 +00:00
"count": min(42, fileCount * 2), // m4gic number
2021-07-27 16:45:16 +00:00
},
options: Options(
2022-06-11 08:23:52 +00:00
headers: {"X-Auth-Token": Configuration.instance.getToken()},
),
2021-07-27 16:45:16 +00:00
);
final urls = (response.data["urls"] as List)
.map((e) => UploadURL.fromMap(e))
.toList();
_uploadURLs.addAll(urls);
2021-12-02 05:08:51 +00:00
} on DioError catch (e, s) {
2021-07-27 16:45:16 +00:00
if (e.response != null) {
if (e.response.statusCode == 402) {
final error = NoActiveSubscriptionError();
clearQueue(error);
throw error;
} else if (e.response.statusCode == 426) {
final error = StorageLimitExceededError();
clearQueue(error);
throw error;
2021-12-02 05:08:51 +00:00
} else {
_logger.severe("Could not fetch upload URLs", e, s);
2021-02-25 16:14:27 +00:00
}
2021-02-02 16:35:38 +00:00
}
2021-07-27 16:45:16 +00:00
rethrow;
2021-12-02 05:08:51 +00:00
} finally {
_uploadURLFetchInProgress = null;
2021-07-27 16:45:16 +00:00
}
});
2020-11-09 17:43:40 +00:00
return _uploadURLFetchInProgress;
2020-11-09 12:28:43 +00:00
}
void _onStorageLimitExceeded() {
2021-02-25 16:14:27 +00:00
clearQueue(StorageLimitExceededError());
throw StorageLimitExceededError();
}
Future<String> _putFile(
UploadURL uploadURL,
io.File file, {
int contentLength,
int attempt = 1,
}) async {
2021-08-09 06:33:47 +00:00
final fileSize = contentLength ?? await file.length();
2022-06-11 08:23:52 +00:00
_logger.info(
"Putting object for " +
file.toString() +
" of size: " +
fileSize.toString(),
);
2020-11-09 12:28:43 +00:00
final startTime = DateTime.now().millisecondsSinceEpoch;
2021-02-13 12:26:37 +00:00
try {
await _dio.put(
uploadURL.url,
2020-11-19 14:59:01 +00:00
data: file.openRead(),
2021-02-13 12:26:37 +00:00
options: Options(
headers: {
Headers.contentLengthHeader: fileSize,
},
),
);
2022-06-11 08:23:52 +00:00
_logger.info(
"Upload speed : " +
(fileSize / (DateTime.now().millisecondsSinceEpoch - startTime))
.toString() +
" kilo bytes per second",
);
2021-02-13 12:26:37 +00:00
return uploadURL.objectKey;
} on DioError catch (e) {
if (e.message.startsWith(
2022-06-11 08:23:52 +00:00
"HttpException: Content size exceeds specified contentLength.",
) &&
attempt == 1) {
2022-06-11 08:23:52 +00:00
return _putFile(
uploadURL,
file,
contentLength: (await file.readAsBytes()).length,
attempt: 2,
);
2021-02-25 15:53:32 +00:00
} else if (attempt < kMaximumUploadAttempts) {
final newUploadURL = await _getUploadURL();
2022-06-11 08:23:52 +00:00
return _putFile(
newUploadURL,
file,
contentLength: (await file.readAsBytes()).length,
attempt: attempt + 1,
);
2021-02-13 12:26:37 +00:00
} else {
_logger.info(
2022-06-11 08:23:52 +00:00
"Upload failed for file with size " + fileSize.toString(),
e,
);
2021-07-27 16:45:16 +00:00
rethrow;
2021-02-13 12:26:37 +00:00
}
}
}
Future<void> _pollBackgroundUploadStatus() async {
final blockedUploads = _queue.entries
2021-03-04 17:32:57 +00:00
.where((e) => e.value.status == UploadStatus.in_background)
.toList();
for (final upload in blockedUploads) {
final file = upload.value.file;
final isStillLocked = await _uploadLocks.isLocked(
2022-06-11 08:23:52 +00:00
file.localID,
ProcessType.background.toString(),
);
if (!isStillLocked) {
final completer = _queue.remove(upload.key).completer;
final dbFile =
await FilesDB.instance.getFile(upload.value.file.generatedID);
if (dbFile.uploadedFileID != null) {
_logger.info("Background upload success detected");
completer.complete(dbFile);
} else {
_logger.info("Background upload failure detected");
completer.completeError(SilentlyCancelUploadsError());
}
}
}
Future.delayed(kBlockedUploadsPollFrequency, () async {
await _pollBackgroundUploadStatus();
});
}
}
2020-11-09 12:28:43 +00:00
class FileUploadItem {
final File file;
2020-11-09 16:10:43 +00:00
final int collectionID;
2020-11-09 12:28:43 +00:00
final Completer<File> completer;
UploadStatus status;
FileUploadItem(
this.file,
2020-11-09 16:10:43 +00:00
this.collectionID,
2020-11-09 12:28:43 +00:00
this.completer, {
this.status = UploadStatus.not_started,
});
}
enum UploadStatus {
not_started,
in_progress,
2021-03-04 17:32:57 +00:00
in_background,
2020-11-09 12:28:43 +00:00
completed,
}
2021-03-03 20:18:50 +00:00
enum ProcessType {
background,
foreground,
}