file_uploader.dart 22 KB

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