file_uploader.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. import 'dart:async';
  2. import 'dart:collection';
  3. import 'dart:convert';
  4. import 'dart:io' as io;
  5. import 'dart:math';
  6. import 'dart:typed_data';
  7. import 'package:connectivity/connectivity.dart';
  8. import 'package:dio/dio.dart';
  9. import 'package:flutter_sodium/flutter_sodium.dart';
  10. import 'package:logging/logging.dart';
  11. import 'package:photos/core/configuration.dart';
  12. import 'package:photos/core/errors.dart';
  13. import 'package:photos/core/event_bus.dart';
  14. import 'package:photos/core/network.dart';
  15. import 'package:photos/db/files_db.dart';
  16. import 'package:photos/db/upload_locks_db.dart';
  17. import 'package:photos/events/local_photos_updated_event.dart';
  18. import 'package:photos/events/subscription_purchased_event.dart';
  19. import 'package:photos/main.dart';
  20. import 'package:photos/models/encryption_result.dart';
  21. import 'package:photos/models/file.dart';
  22. import 'package:photos/models/upload_url.dart';
  23. import 'package:photos/services/collections_service.dart';
  24. import 'package:photos/services/local_sync_service.dart';
  25. import 'package:photos/services/sync_service.dart';
  26. import 'package:photos/utils/crypto_util.dart';
  27. import 'package:photos/utils/file_uploader_util.dart';
  28. import 'package:photos/utils/file_util.dart';
  29. import 'package:shared_preferences/shared_preferences.dart';
  30. class FileUploader {
  31. static const kMaximumConcurrentUploads = 4;
  32. static const kMaximumThumbnailCompressionAttempts = 2;
  33. static const kMaximumUploadAttempts = 4;
  34. static const kBlockedUploadsPollFrequency = Duration(seconds: 2);
  35. final _logger = Logger("FileUploader");
  36. final _dio = Network.instance.getDio();
  37. final _queue = LinkedHashMap<String, FileUploadItem>();
  38. final _uploadLocks = UploadLocksDB.instance;
  39. final kSafeBufferForLockExpiry = Duration(days: 1).inMicroseconds;
  40. final kBGTaskDeathTimeout = Duration(seconds: 5).inMicroseconds;
  41. final _uploadURLs = Queue<UploadURL>();
  42. // Maintains the count of files in the current upload session.
  43. // Upload session is the period between the first entry into the _queue and last entry out of the _queue
  44. int _totalCountInUploadSession = 0;
  45. int _currentlyUploading = 0;
  46. ProcessType _processType;
  47. bool _isBackground;
  48. SharedPreferences _prefs;
  49. FileUploader._privateConstructor() {
  50. Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
  51. _uploadURLFetchInProgress = null;
  52. });
  53. }
  54. static FileUploader instance = FileUploader._privateConstructor();
  55. Future<void> init(bool isBackground) async {
  56. _prefs = await SharedPreferences.getInstance();
  57. _isBackground = isBackground;
  58. _processType =
  59. isBackground ? ProcessType.background : ProcessType.foreground;
  60. final currentTime = DateTime.now().microsecondsSinceEpoch;
  61. await _uploadLocks.releaseLocksAcquiredByOwnerBefore(
  62. _processType.toString(), currentTime);
  63. await _uploadLocks
  64. .releaseAllLocksAcquiredBefore(currentTime - kSafeBufferForLockExpiry);
  65. if (!isBackground) {
  66. await _prefs.reload();
  67. final isBGTaskDead = (_prefs.getInt(kLastBGTaskHeartBeatTime) ?? 0) <
  68. (currentTime - kBGTaskDeathTimeout);
  69. if (isBGTaskDead) {
  70. await _uploadLocks.releaseLocksAcquiredByOwnerBefore(
  71. ProcessType.background.toString(), currentTime);
  72. _logger.info("BG task was found dead, cleared all locks");
  73. }
  74. _pollBackgroundUploadStatus();
  75. }
  76. }
  77. Future<File> upload(File file, int collectionID) {
  78. // If the file hasn't been queued yet, queue it
  79. _totalCountInUploadSession++;
  80. if (!_queue.containsKey(file.localID)) {
  81. final completer = Completer<File>();
  82. _queue[file.localID] = FileUploadItem(file, collectionID, completer);
  83. _pollQueue();
  84. return completer.future;
  85. }
  86. // If the file exists in the queue for a matching collectionID,
  87. // return the existing future
  88. final item = _queue[file.localID];
  89. if (item.collectionID == collectionID) {
  90. _totalCountInUploadSession--;
  91. return item.completer.future;
  92. }
  93. // Else wait for the existing upload to complete,
  94. // and add it to the relevant collection
  95. return item.completer.future.then((uploadedFile) {
  96. return CollectionsService.instance
  97. .addToCollection(collectionID, [uploadedFile]).then((aVoid) {
  98. return uploadedFile;
  99. });
  100. });
  101. }
  102. Future<File> forceUpload(File file, int collectionID) async {
  103. _logger.info("Force uploading " +
  104. file.toString() +
  105. " into collection " +
  106. collectionID.toString());
  107. _totalCountInUploadSession++;
  108. // If the file hasn't been queued yet, ez.
  109. if (!_queue.containsKey(file.localID)) {
  110. final completer = Completer<File>();
  111. _queue[file.localID] = FileUploadItem(
  112. file,
  113. collectionID,
  114. completer,
  115. status: UploadStatus.in_progress,
  116. );
  117. _encryptAndUploadFileToCollection(file, collectionID, forcedUpload: true);
  118. return completer.future;
  119. }
  120. var item = _queue[file.localID];
  121. // If the file is being uploaded right now, wait and proceed
  122. if (item.status == UploadStatus.in_progress ||
  123. item.status == UploadStatus.in_background) {
  124. _totalCountInUploadSession--;
  125. final uploadedFile = await item.completer.future;
  126. if (uploadedFile.collectionID == collectionID) {
  127. // Do nothing
  128. } else {
  129. await CollectionsService.instance
  130. .addToCollection(collectionID, [uploadedFile]);
  131. }
  132. return uploadedFile;
  133. } else {
  134. // If the file is yet to be processed,
  135. // 1. Set the status to in_progress
  136. // 2. Force upload the file
  137. // 3. Add to the relevant collection
  138. item = _queue[file.localID];
  139. item.status = UploadStatus.in_progress;
  140. final uploadedFile = await _encryptAndUploadFileToCollection(
  141. file, collectionID,
  142. forcedUpload: true);
  143. if (item.collectionID == collectionID) {
  144. return uploadedFile;
  145. } else {
  146. await CollectionsService.instance
  147. .addToCollection(item.collectionID, [uploadedFile]);
  148. return uploadedFile;
  149. }
  150. }
  151. }
  152. int getCurrentSessionUploadCount() {
  153. return _totalCountInUploadSession;
  154. }
  155. void clearQueue(final Error reason) {
  156. final List<String> uploadsToBeRemoved = [];
  157. _queue.entries
  158. .where((entry) => entry.value.status == UploadStatus.not_started)
  159. .forEach((pendingUpload) {
  160. uploadsToBeRemoved.add(pendingUpload.key);
  161. });
  162. for (final id in uploadsToBeRemoved) {
  163. _queue.remove(id).completer.completeError(reason);
  164. }
  165. _totalCountInUploadSession = 0;
  166. }
  167. void removeFromQueueWhere(final bool Function(File) fn, final Error reason) {
  168. List<String> uploadsToBeRemoved = [];
  169. _queue.entries
  170. .where((entry) => entry.value.status == UploadStatus.not_started)
  171. .forEach((pendingUpload) {
  172. if (fn(pendingUpload.value.file)) {
  173. uploadsToBeRemoved.add(pendingUpload.key);
  174. }
  175. });
  176. for (final id in uploadsToBeRemoved) {
  177. _queue.remove(id).completer.completeError(reason);
  178. }
  179. _totalCountInUploadSession -= uploadsToBeRemoved.length;
  180. }
  181. void _pollQueue() {
  182. if (SyncService.instance.shouldStopSync()) {
  183. clearQueue(SyncStopRequestedError());
  184. }
  185. if (_queue.isEmpty) {
  186. // Upload session completed
  187. _totalCountInUploadSession = 0;
  188. return;
  189. }
  190. if (_currentlyUploading < kMaximumConcurrentUploads) {
  191. final firstPendingEntry = _queue.entries
  192. .firstWhere((entry) => entry.value.status == UploadStatus.not_started,
  193. orElse: () => null)
  194. ?.value;
  195. if (firstPendingEntry != null) {
  196. firstPendingEntry.status = UploadStatus.in_progress;
  197. _encryptAndUploadFileToCollection(
  198. firstPendingEntry.file, firstPendingEntry.collectionID);
  199. }
  200. }
  201. }
  202. Future<File> _encryptAndUploadFileToCollection(File file, int collectionID,
  203. {bool forcedUpload = false}) async {
  204. _currentlyUploading++;
  205. final localID = file.localID;
  206. try {
  207. final uploadedFile = await _tryToUpload(file, collectionID, forcedUpload);
  208. _queue.remove(localID).completer.complete(uploadedFile);
  209. return uploadedFile;
  210. } catch (e) {
  211. if (e is LockAlreadyAcquiredError) {
  212. _queue[localID].status = UploadStatus.in_background;
  213. return _queue[localID].completer.future;
  214. } else {
  215. _queue.remove(localID).completer.completeError(e);
  216. return null;
  217. }
  218. } finally {
  219. _currentlyUploading--;
  220. _pollQueue();
  221. }
  222. }
  223. Future<File> _tryToUpload(
  224. File file, int collectionID, bool forcedUpload) async {
  225. final connectivityResult = await (Connectivity().checkConnectivity());
  226. var canUploadUnderCurrentNetworkConditions =
  227. (connectivityResult == ConnectivityResult.wifi ||
  228. Configuration.instance.shouldBackupOverMobileData());
  229. if (!canUploadUnderCurrentNetworkConditions && !forcedUpload) {
  230. throw WiFiUnavailableError();
  231. }
  232. try {
  233. await _uploadLocks.acquireLock(
  234. file.localID,
  235. _processType.toString(),
  236. DateTime.now().microsecondsSinceEpoch,
  237. );
  238. } catch (e) {
  239. _logger.warning("Lock was already taken for " + file.toString());
  240. throw LockAlreadyAcquiredError();
  241. }
  242. final tempDirectory = Configuration.instance.getTempDirectory();
  243. final encryptedFilePath = tempDirectory +
  244. file.generatedID.toString() +
  245. (_isBackground ? "_bg" : "") +
  246. ".encrypted";
  247. final encryptedThumbnailPath = tempDirectory +
  248. file.generatedID.toString() +
  249. "_thumbnail" +
  250. (_isBackground ? "_bg" : "") +
  251. ".encrypted";
  252. MediaUploadData mediaUploadData;
  253. try {
  254. _logger.info("Trying to upload " +
  255. file.toString() +
  256. ", isForced: " +
  257. forcedUpload.toString());
  258. mediaUploadData =
  259. await getUploadDataFromEnteFile(file).catchError((e) async {
  260. if (e is InvalidFileError) {
  261. _onInvalidFileError(file);
  262. } else {
  263. throw e;
  264. }
  265. });
  266. Uint8List key;
  267. var isAlreadyUploadedFile = file.uploadedFileID != null;
  268. if (isAlreadyUploadedFile) {
  269. key = decryptFileKey(file);
  270. } else {
  271. key = null;
  272. }
  273. if (io.File(encryptedFilePath).existsSync()) {
  274. io.File(encryptedFilePath).deleteSync();
  275. }
  276. final encryptedFile = io.File(encryptedFilePath);
  277. final fileAttributes = await CryptoUtil.encryptFile(
  278. mediaUploadData.sourceFile.path,
  279. encryptedFilePath,
  280. key: key,
  281. );
  282. var thumbnailData = mediaUploadData.thumbnail;
  283. final encryptedThumbnailData =
  284. await CryptoUtil.encryptChaCha(thumbnailData, fileAttributes.key);
  285. if (io.File(encryptedThumbnailPath).existsSync()) {
  286. io.File(encryptedThumbnailPath).deleteSync();
  287. }
  288. final encryptedThumbnailFile = io.File(encryptedThumbnailPath);
  289. encryptedThumbnailFile
  290. .writeAsBytesSync(encryptedThumbnailData.encryptedData);
  291. final thumbnailUploadURL = await _getUploadURL();
  292. String thumbnailObjectKey =
  293. await _putFile(thumbnailUploadURL, encryptedThumbnailFile);
  294. final fileUploadURL = await _getUploadURL();
  295. String fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
  296. final encryptedMetadataData = await CryptoUtil.encryptChaCha(
  297. utf8.encode(jsonEncode(file.getMetadata())), fileAttributes.key);
  298. final fileDecryptionHeader = Sodium.bin2base64(fileAttributes.header);
  299. final thumbnailDecryptionHeader =
  300. Sodium.bin2base64(encryptedThumbnailData.header);
  301. final encryptedMetadata =
  302. Sodium.bin2base64(encryptedMetadataData.encryptedData);
  303. final metadataDecryptionHeader =
  304. Sodium.bin2base64(encryptedMetadataData.header);
  305. if (SyncService.instance.shouldStopSync()) {
  306. throw SyncStopRequestedError();
  307. }
  308. File remoteFile;
  309. if (isAlreadyUploadedFile) {
  310. remoteFile = await _updateFile(
  311. file,
  312. fileObjectKey,
  313. fileDecryptionHeader,
  314. thumbnailObjectKey,
  315. thumbnailDecryptionHeader,
  316. encryptedMetadata,
  317. metadataDecryptionHeader,
  318. );
  319. // Update across all collections
  320. await FilesDB.instance.updateUploadedFileAcrossCollections(remoteFile);
  321. } else {
  322. remoteFile = await _uploadFile(
  323. file,
  324. collectionID,
  325. fileAttributes,
  326. fileObjectKey,
  327. fileDecryptionHeader,
  328. thumbnailObjectKey,
  329. thumbnailDecryptionHeader,
  330. encryptedMetadata,
  331. metadataDecryptionHeader,
  332. );
  333. if (mediaUploadData.isDeleted) {
  334. _logger.info("File found to be deleted");
  335. remoteFile.localID = null;
  336. }
  337. await FilesDB.instance.update(remoteFile);
  338. }
  339. if (!_isBackground) {
  340. Bus.instance.fire(LocalPhotosUpdatedEvent([remoteFile]));
  341. }
  342. _logger.info("File upload complete for " + remoteFile.toString());
  343. return remoteFile;
  344. } catch (e, s) {
  345. if (!(e is NoActiveSubscriptionError || e is StorageLimitExceededError)) {
  346. _logger.severe("File upload failed for " + file.toString(), e, s);
  347. }
  348. rethrow;
  349. } finally {
  350. if (io.Platform.isIOS &&
  351. mediaUploadData != null &&
  352. mediaUploadData.sourceFile != null) {
  353. mediaUploadData.sourceFile.deleteSync();
  354. }
  355. if (io.File(encryptedFilePath).existsSync()) {
  356. io.File(encryptedFilePath).deleteSync();
  357. }
  358. if (io.File(encryptedThumbnailPath).existsSync()) {
  359. io.File(encryptedThumbnailPath).deleteSync();
  360. }
  361. await _uploadLocks.releaseLock(file.localID, _processType.toString());
  362. }
  363. }
  364. Future _onInvalidFileError(File file) async {
  365. _logger.warning("Invalid file encountered: " + file.toString());
  366. await FilesDB.instance.deleteLocalFile(file);
  367. await LocalSyncService.instance.trackInvalidFile(file);
  368. throw InvalidFileError();
  369. }
  370. Future<File> _uploadFile(
  371. File file,
  372. int collectionID,
  373. EncryptionResult fileAttributes,
  374. String fileObjectKey,
  375. String fileDecryptionHeader,
  376. String thumbnailObjectKey,
  377. String thumbnailDecryptionHeader,
  378. String encryptedMetadata,
  379. String metadataDecryptionHeader, {
  380. int attempt = 1,
  381. }) async {
  382. final encryptedFileKeyData = CryptoUtil.encryptSync(
  383. fileAttributes.key,
  384. CollectionsService.instance.getCollectionKey(collectionID),
  385. );
  386. final encryptedKey = Sodium.bin2base64(encryptedFileKeyData.encryptedData);
  387. final keyDecryptionNonce = Sodium.bin2base64(encryptedFileKeyData.nonce);
  388. final request = {
  389. "collectionID": collectionID,
  390. "encryptedKey": encryptedKey,
  391. "keyDecryptionNonce": keyDecryptionNonce,
  392. "file": {
  393. "objectKey": fileObjectKey,
  394. "decryptionHeader": fileDecryptionHeader,
  395. },
  396. "thumbnail": {
  397. "objectKey": thumbnailObjectKey,
  398. "decryptionHeader": thumbnailDecryptionHeader,
  399. },
  400. "metadata": {
  401. "encryptedData": encryptedMetadata,
  402. "decryptionHeader": metadataDecryptionHeader,
  403. }
  404. };
  405. try {
  406. final response = await _dio.post(
  407. Configuration.instance.getHttpEndpoint() + "/files",
  408. options: Options(
  409. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  410. data: request,
  411. );
  412. final data = response.data;
  413. file.uploadedFileID = data["id"];
  414. file.collectionID = collectionID;
  415. file.updationTime = data["updationTime"];
  416. file.ownerID = data["ownerID"];
  417. file.encryptedKey = encryptedKey;
  418. file.keyDecryptionNonce = keyDecryptionNonce;
  419. file.fileDecryptionHeader = fileDecryptionHeader;
  420. file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
  421. file.metadataDecryptionHeader = metadataDecryptionHeader;
  422. return file;
  423. } on DioError catch (e) {
  424. if (e.response?.statusCode == 426) {
  425. _onStorageLimitExceeded();
  426. } else if (attempt < kMaximumUploadAttempts) {
  427. _logger.info("Upload file failed, will retry in 3 seconds");
  428. await Future.delayed(Duration(seconds: 3));
  429. return _uploadFile(
  430. file,
  431. collectionID,
  432. fileAttributes,
  433. fileObjectKey,
  434. fileDecryptionHeader,
  435. thumbnailObjectKey,
  436. thumbnailDecryptionHeader,
  437. encryptedMetadata,
  438. metadataDecryptionHeader,
  439. attempt: attempt++,
  440. );
  441. }
  442. rethrow;
  443. }
  444. }
  445. Future<File> _updateFile(
  446. File file,
  447. String fileObjectKey,
  448. String fileDecryptionHeader,
  449. String thumbnailObjectKey,
  450. String thumbnailDecryptionHeader,
  451. String encryptedMetadata,
  452. String metadataDecryptionHeader, {
  453. int attempt = 1,
  454. }) async {
  455. final request = {
  456. "id": file.uploadedFileID,
  457. "file": {
  458. "objectKey": fileObjectKey,
  459. "decryptionHeader": fileDecryptionHeader,
  460. },
  461. "thumbnail": {
  462. "objectKey": thumbnailObjectKey,
  463. "decryptionHeader": thumbnailDecryptionHeader,
  464. },
  465. "metadata": {
  466. "encryptedData": encryptedMetadata,
  467. "decryptionHeader": metadataDecryptionHeader,
  468. }
  469. };
  470. try {
  471. final response = await _dio.post(
  472. Configuration.instance.getHttpEndpoint() + "/files",
  473. options: Options(
  474. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  475. data: request,
  476. );
  477. final data = response.data;
  478. file.uploadedFileID = data["id"];
  479. file.updationTime = data["updationTime"];
  480. file.fileDecryptionHeader = fileDecryptionHeader;
  481. file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
  482. file.metadataDecryptionHeader = metadataDecryptionHeader;
  483. return file;
  484. } on DioError catch (e) {
  485. if (e.response?.statusCode == 426) {
  486. _onStorageLimitExceeded();
  487. } else if (attempt < kMaximumUploadAttempts) {
  488. _logger.info("Update file failed, will retry in 3 seconds");
  489. await Future.delayed(Duration(seconds: 3));
  490. return _updateFile(
  491. file,
  492. fileObjectKey,
  493. fileDecryptionHeader,
  494. thumbnailObjectKey,
  495. thumbnailDecryptionHeader,
  496. encryptedMetadata,
  497. metadataDecryptionHeader,
  498. attempt: attempt++,
  499. );
  500. }
  501. rethrow;
  502. }
  503. }
  504. Future<UploadURL> _getUploadURL() async {
  505. if (_uploadURLs.isEmpty) {
  506. await _fetchUploadURLs();
  507. }
  508. return _uploadURLs.removeFirst();
  509. }
  510. Future<void> _uploadURLFetchInProgress;
  511. Future<void> _fetchUploadURLs() async {
  512. _uploadURLFetchInProgress ??= Future<void>(() async {
  513. try {
  514. final response = await _dio.get(
  515. Configuration.instance.getHttpEndpoint() + "/files/upload-urls",
  516. queryParameters: {
  517. "count": min(42, 2 * _queue.length), // m4gic number
  518. },
  519. options: Options(
  520. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  521. );
  522. final urls = (response.data["urls"] as List)
  523. .map((e) => UploadURL.fromMap(e))
  524. .toList();
  525. _uploadURLs.addAll(urls);
  526. _uploadURLFetchInProgress = null;
  527. } on DioError catch (e) {
  528. _uploadURLFetchInProgress = null;
  529. if (e.response != null) {
  530. if (e.response.statusCode == 402) {
  531. final error = NoActiveSubscriptionError();
  532. clearQueue(error);
  533. throw error;
  534. } else if (e.response.statusCode == 426) {
  535. final error = StorageLimitExceededError();
  536. clearQueue(error);
  537. throw error;
  538. }
  539. }
  540. rethrow;
  541. }
  542. });
  543. return _uploadURLFetchInProgress;
  544. }
  545. void _onStorageLimitExceeded() {
  546. clearQueue(StorageLimitExceededError());
  547. throw StorageLimitExceededError();
  548. }
  549. Future<String> _putFile(
  550. UploadURL uploadURL,
  551. io.File file, {
  552. int contentLength,
  553. int attempt = 1,
  554. }) async {
  555. final fileSize = contentLength ?? file.lengthSync();
  556. final startTime = DateTime.now().millisecondsSinceEpoch;
  557. try {
  558. await _dio.put(
  559. uploadURL.url,
  560. data: file.openRead(),
  561. options: Options(
  562. headers: {
  563. Headers.contentLengthHeader: fileSize,
  564. },
  565. ),
  566. );
  567. _logger.info("Upload speed : " +
  568. (file.lengthSync() /
  569. (DateTime.now().millisecondsSinceEpoch - startTime))
  570. .toString() +
  571. " kilo bytes per second");
  572. return uploadURL.objectKey;
  573. } on DioError catch (e) {
  574. if (e.message.startsWith(
  575. "HttpException: Content size exceeds specified contentLength.") &&
  576. attempt == 1) {
  577. return _putFile(uploadURL, file,
  578. contentLength: file.readAsBytesSync().length, attempt: 2);
  579. } else if (attempt < kMaximumUploadAttempts) {
  580. final newUploadURL = await _getUploadURL();
  581. return _putFile(newUploadURL, file,
  582. contentLength: file.readAsBytesSync().length, attempt: attempt++);
  583. } else {
  584. _logger.info(
  585. "Upload failed for file with size " + fileSize.toString(), e);
  586. rethrow;
  587. }
  588. }
  589. }
  590. Future<void> _pollBackgroundUploadStatus() async {
  591. final blockedUploads = _queue.entries
  592. .where((e) => e.value.status == UploadStatus.in_background)
  593. .toList();
  594. for (final upload in blockedUploads) {
  595. final file = upload.value.file;
  596. final isStillLocked = await _uploadLocks.isLocked(
  597. file.localID, ProcessType.background.toString());
  598. if (!isStillLocked) {
  599. final completer = _queue.remove(upload.key).completer;
  600. final dbFile =
  601. await FilesDB.instance.getFile(upload.value.file.generatedID);
  602. if (dbFile.uploadedFileID != null) {
  603. _logger.info("Background upload success detected");
  604. completer.complete(dbFile);
  605. } else {
  606. _logger.info("Background upload failure detected");
  607. completer.completeError(SilentlyCancelUploadsError());
  608. }
  609. }
  610. }
  611. Future.delayed(kBlockedUploadsPollFrequency, () async {
  612. await _pollBackgroundUploadStatus();
  613. });
  614. }
  615. }
  616. class FileUploadItem {
  617. final File file;
  618. final int collectionID;
  619. final Completer<File> completer;
  620. UploadStatus status;
  621. FileUploadItem(
  622. this.file,
  623. this.collectionID,
  624. this.completer, {
  625. this.status = UploadStatus.not_started,
  626. });
  627. }
  628. enum UploadStatus {
  629. not_started,
  630. in_progress,
  631. in_background,
  632. completed,
  633. }
  634. enum ProcessType {
  635. background,
  636. foreground,
  637. }