file_uploader.dart 22 KB

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