file_uploader.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  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/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/file_uploader_util.dart';
  27. import 'package:photos/utils/crypto_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.length == 0) {
  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 = await getUploadDataFromEnteFile(file).catchError((e) async {
  259. if (e is InvalidFileError) {
  260. _onInvalidFileError(file);
  261. } else {
  262. throw e;
  263. }
  264. });
  265. var key;
  266. var isAlreadyUploadedFile = file.uploadedFileID != null;
  267. if (isAlreadyUploadedFile) {
  268. key = decryptFileKey(file);
  269. } else {
  270. key = null;
  271. }
  272. if (io.File(encryptedFilePath).existsSync()) {
  273. io.File(encryptedFilePath).deleteSync();
  274. }
  275. final encryptedFile = io.File(encryptedFilePath);
  276. final fileAttributes = await CryptoUtil.encryptFile(
  277. mediaUploadData.sourceFile.path,
  278. encryptedFilePath,
  279. key: key,
  280. );
  281. var thumbnailData = mediaUploadData.thumbnail;
  282. final encryptedThumbnailData =
  283. await CryptoUtil.encryptChaCha(thumbnailData, fileAttributes.key);
  284. if (io.File(encryptedThumbnailPath).existsSync()) {
  285. io.File(encryptedThumbnailPath).deleteSync();
  286. }
  287. final encryptedThumbnailFile = io.File(encryptedThumbnailPath);
  288. encryptedThumbnailFile
  289. .writeAsBytesSync(encryptedThumbnailData.encryptedData);
  290. final thumbnailUploadURL = await _getUploadURL();
  291. String thumbnailObjectKey =
  292. await _putFile(thumbnailUploadURL, encryptedThumbnailFile);
  293. final fileUploadURL = await _getUploadURL();
  294. String fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
  295. final encryptedMetadataData = await CryptoUtil.encryptChaCha(
  296. utf8.encode(jsonEncode(file.getMetadata())), fileAttributes.key);
  297. final fileDecryptionHeader = Sodium.bin2base64(fileAttributes.header);
  298. final thumbnailDecryptionHeader =
  299. Sodium.bin2base64(encryptedThumbnailData.header);
  300. final encryptedMetadata =
  301. Sodium.bin2base64(encryptedMetadataData.encryptedData);
  302. final metadataDecryptionHeader =
  303. Sodium.bin2base64(encryptedMetadataData.header);
  304. if (SyncService.instance.shouldStopSync()) {
  305. throw SyncStopRequestedError();
  306. }
  307. File remoteFile;
  308. if (isAlreadyUploadedFile) {
  309. remoteFile = await _updateFile(
  310. file,
  311. fileObjectKey,
  312. fileDecryptionHeader,
  313. thumbnailObjectKey,
  314. thumbnailDecryptionHeader,
  315. encryptedMetadata,
  316. metadataDecryptionHeader,
  317. );
  318. // Update across all collections
  319. await FilesDB.instance.updateUploadedFileAcrossCollections(remoteFile);
  320. } else {
  321. remoteFile = await _uploadFile(
  322. file,
  323. collectionID,
  324. fileAttributes,
  325. fileObjectKey,
  326. fileDecryptionHeader,
  327. thumbnailObjectKey,
  328. thumbnailDecryptionHeader,
  329. encryptedMetadata,
  330. metadataDecryptionHeader,
  331. );
  332. if (mediaUploadData.isDeleted) {
  333. _logger.info("File found to be deleted");
  334. remoteFile.localID = null;
  335. }
  336. await FilesDB.instance.update(remoteFile);
  337. }
  338. if (!_isBackground) {
  339. Bus.instance.fire(LocalPhotosUpdatedEvent([remoteFile]));
  340. }
  341. _logger.info("File upload complete for " + remoteFile.toString());
  342. return remoteFile;
  343. } catch (e, s) {
  344. if (!(e is NoActiveSubscriptionError || e is StorageLimitExceededError)) {
  345. _logger.severe("File upload failed for " + file.toString(), e, s);
  346. }
  347. throw e;
  348. } finally {
  349. if (io.Platform.isIOS && mediaUploadData != null && mediaUploadData.sourceFile != null) {
  350. mediaUploadData.sourceFile.deleteSync();
  351. }
  352. if (io.File(encryptedFilePath).existsSync()) {
  353. io.File(encryptedFilePath).deleteSync();
  354. }
  355. if (io.File(encryptedThumbnailPath).existsSync()) {
  356. io.File(encryptedThumbnailPath).deleteSync();
  357. }
  358. await _uploadLocks.releaseLock(file.localID, _processType.toString());
  359. }
  360. }
  361. Future _onInvalidFileError(File file) async {
  362. _logger.warning("Invalid file encountered: " + file.toString());
  363. await FilesDB.instance.deleteLocalFile(file.localID);
  364. await LocalSyncService.instance.trackInvalidFile(file);
  365. throw InvalidFileError();
  366. }
  367. Future<File> _uploadFile(
  368. File file,
  369. int collectionID,
  370. EncryptionResult fileAttributes,
  371. String fileObjectKey,
  372. String fileDecryptionHeader,
  373. String thumbnailObjectKey,
  374. String thumbnailDecryptionHeader,
  375. String encryptedMetadata,
  376. String metadataDecryptionHeader,
  377. ) async {
  378. final encryptedFileKeyData = CryptoUtil.encryptSync(
  379. fileAttributes.key,
  380. CollectionsService.instance.getCollectionKey(collectionID),
  381. );
  382. final encryptedKey = Sodium.bin2base64(encryptedFileKeyData.encryptedData);
  383. final keyDecryptionNonce = Sodium.bin2base64(encryptedFileKeyData.nonce);
  384. final request = {
  385. "collectionID": collectionID,
  386. "encryptedKey": encryptedKey,
  387. "keyDecryptionNonce": keyDecryptionNonce,
  388. "file": {
  389. "objectKey": fileObjectKey,
  390. "decryptionHeader": fileDecryptionHeader,
  391. },
  392. "thumbnail": {
  393. "objectKey": thumbnailObjectKey,
  394. "decryptionHeader": thumbnailDecryptionHeader,
  395. },
  396. "metadata": {
  397. "encryptedData": encryptedMetadata,
  398. "decryptionHeader": metadataDecryptionHeader,
  399. }
  400. };
  401. try {
  402. final response = await _dio.post(
  403. Configuration.instance.getHttpEndpoint() + "/files",
  404. options: Options(
  405. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  406. data: request,
  407. );
  408. final data = response.data;
  409. file.uploadedFileID = data["id"];
  410. file.collectionID = collectionID;
  411. file.updationTime = data["updationTime"];
  412. file.ownerID = data["ownerID"];
  413. file.encryptedKey = encryptedKey;
  414. file.keyDecryptionNonce = keyDecryptionNonce;
  415. file.fileDecryptionHeader = fileDecryptionHeader;
  416. file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
  417. file.metadataDecryptionHeader = metadataDecryptionHeader;
  418. return file;
  419. } on DioError catch (e) {
  420. if (e.response?.statusCode == 426) {
  421. _onStorageLimitExceeded();
  422. }
  423. throw e;
  424. }
  425. }
  426. Future<File> _updateFile(
  427. File file,
  428. String fileObjectKey,
  429. String fileDecryptionHeader,
  430. String thumbnailObjectKey,
  431. String thumbnailDecryptionHeader,
  432. String encryptedMetadata,
  433. String metadataDecryptionHeader,
  434. ) async {
  435. final request = {
  436. "id": file.uploadedFileID,
  437. "file": {
  438. "objectKey": fileObjectKey,
  439. "decryptionHeader": fileDecryptionHeader,
  440. },
  441. "thumbnail": {
  442. "objectKey": thumbnailObjectKey,
  443. "decryptionHeader": thumbnailDecryptionHeader,
  444. },
  445. "metadata": {
  446. "encryptedData": encryptedMetadata,
  447. "decryptionHeader": metadataDecryptionHeader,
  448. }
  449. };
  450. try {
  451. final response = await _dio.post(
  452. Configuration.instance.getHttpEndpoint() + "/files",
  453. options: Options(
  454. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  455. data: request,
  456. );
  457. final data = response.data;
  458. file.uploadedFileID = data["id"];
  459. file.updationTime = data["updationTime"];
  460. file.fileDecryptionHeader = fileDecryptionHeader;
  461. file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
  462. file.metadataDecryptionHeader = metadataDecryptionHeader;
  463. return file;
  464. } on DioError catch (e) {
  465. if (e.response?.statusCode == 426) {
  466. _onStorageLimitExceeded();
  467. }
  468. throw e;
  469. }
  470. }
  471. Future<UploadURL> _getUploadURL() async {
  472. if (_uploadURLs.isEmpty) {
  473. await _fetchUploadURLs();
  474. }
  475. return _uploadURLs.removeFirst();
  476. }
  477. Future<void> _uploadURLFetchInProgress;
  478. Future<void> _fetchUploadURLs() async {
  479. if (_uploadURLFetchInProgress == null) {
  480. _uploadURLFetchInProgress = Future<void>(() async {
  481. try {
  482. final response = await _dio.get(
  483. Configuration.instance.getHttpEndpoint() + "/files/upload-urls",
  484. queryParameters: {
  485. "count": min(42, 2 * _queue.length), // m4gic number
  486. },
  487. options: Options(
  488. headers: {"X-Auth-Token": Configuration.instance.getToken()}),
  489. );
  490. final urls = (response.data["urls"] as List)
  491. .map((e) => UploadURL.fromMap(e))
  492. .toList();
  493. _uploadURLs.addAll(urls);
  494. _uploadURLFetchInProgress = null;
  495. } on DioError catch (e) {
  496. _uploadURLFetchInProgress = null;
  497. if (e.response != null) {
  498. if (e.response.statusCode == 402) {
  499. final error = NoActiveSubscriptionError();
  500. clearQueue(error);
  501. throw error;
  502. } else if (e.response.statusCode == 426) {
  503. final error = StorageLimitExceededError();
  504. clearQueue(error);
  505. throw error;
  506. }
  507. }
  508. throw e;
  509. }
  510. });
  511. }
  512. return _uploadURLFetchInProgress;
  513. }
  514. void _onStorageLimitExceeded() {
  515. clearQueue(StorageLimitExceededError());
  516. throw StorageLimitExceededError();
  517. }
  518. Future<String> _putFile(
  519. UploadURL uploadURL,
  520. io.File file, {
  521. int contentLength,
  522. int attempt = 1,
  523. }) async {
  524. final fileSize = contentLength ?? file.lengthSync();
  525. final startTime = DateTime.now().millisecondsSinceEpoch;
  526. try {
  527. await _dio.put(
  528. uploadURL.url,
  529. data: file.openRead(),
  530. options: Options(
  531. headers: {
  532. Headers.contentLengthHeader: fileSize,
  533. },
  534. ),
  535. );
  536. _logger.info("Upload speed : " +
  537. (file.lengthSync() /
  538. (DateTime.now().millisecondsSinceEpoch - startTime))
  539. .toString() +
  540. " kilo bytes per second");
  541. return uploadURL.objectKey;
  542. } on DioError catch (e) {
  543. if (e.message.startsWith(
  544. "HttpException: Content size exceeds specified contentLength.") &&
  545. attempt == 1) {
  546. return _putFile(uploadURL, file,
  547. contentLength: file.readAsBytesSync().length, attempt: 2);
  548. } else if (attempt < kMaximumUploadAttempts) {
  549. final newUploadURL = await _getUploadURL();
  550. return _putFile(newUploadURL, file,
  551. contentLength: file.readAsBytesSync().length, attempt: attempt++);
  552. } else {
  553. _logger.info(
  554. "Upload failed for file with size " + fileSize.toString(), e);
  555. throw e;
  556. }
  557. }
  558. }
  559. Future<void> _pollBackgroundUploadStatus() async {
  560. final blockedUploads = _queue.entries
  561. .where((e) => e.value.status == UploadStatus.in_background)
  562. .toList();
  563. for (final upload in blockedUploads) {
  564. final file = upload.value.file;
  565. final isStillLocked = await _uploadLocks.isLocked(
  566. file.localID, ProcessType.background.toString());
  567. if (!isStillLocked) {
  568. final completer = _queue.remove(upload.key).completer;
  569. final dbFile =
  570. await FilesDB.instance.getFile(upload.value.file.generatedID);
  571. if (dbFile.uploadedFileID != null) {
  572. _logger.info("Background upload success detected");
  573. completer.complete(dbFile);
  574. } else {
  575. _logger.info("Background upload failure detected");
  576. completer.completeError(SilentlyCancelUploadsError());
  577. }
  578. }
  579. }
  580. Future.delayed(kBlockedUploadsPollFrequency, () async {
  581. await _pollBackgroundUploadStatus();
  582. });
  583. }
  584. }
  585. class FileUploadItem {
  586. final File file;
  587. final int collectionID;
  588. final Completer<File> completer;
  589. UploadStatus status;
  590. FileUploadItem(
  591. this.file,
  592. this.collectionID,
  593. this.completer, {
  594. this.status = UploadStatus.not_started,
  595. });
  596. }
  597. enum UploadStatus {
  598. not_started,
  599. in_progress,
  600. in_background,
  601. completed,
  602. }
  603. enum ProcessType {
  604. background,
  605. foreground,
  606. }