|
@@ -10,46 +10,43 @@ import 'package:photos/core/cache/image_cache.dart';
|
|
|
import 'package:photos/core/cache/thumbnail_cache_manager.dart';
|
|
|
import 'package:photos/core/configuration.dart';
|
|
|
import 'package:photos/core/constants.dart';
|
|
|
+import 'package:photos/core/errors.dart';
|
|
|
import 'package:photos/core/network.dart';
|
|
|
import 'package:photos/models/file.dart';
|
|
|
import 'package:photos/utils/crypto_util.dart';
|
|
|
import 'package:photos/utils/file_util.dart';
|
|
|
|
|
|
final _logger = Logger("ThumbnailUtil");
|
|
|
-final _thumbnailQueue = LinkedHashMap<int, FileDownloadItem>();
|
|
|
-int _currentlyDownloading = 0;
|
|
|
-const int kMaximumConcurrentDownloads = 500;
|
|
|
+final _map = LinkedHashMap<int, FileDownloadItem>();
|
|
|
+final _queue = Queue<int>();
|
|
|
+const int kMaximumConcurrentDownloads = 2500;
|
|
|
|
|
|
class FileDownloadItem {
|
|
|
final File file;
|
|
|
final Completer<io.File> completer;
|
|
|
- DownloadStatus status;
|
|
|
+ final CancelToken cancelToken;
|
|
|
|
|
|
- FileDownloadItem(
|
|
|
- this.file,
|
|
|
- this.completer, {
|
|
|
- this.status = DownloadStatus.not_started,
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-enum DownloadStatus {
|
|
|
- not_started,
|
|
|
- in_progress,
|
|
|
+ FileDownloadItem(this.file, this.completer, this.cancelToken);
|
|
|
}
|
|
|
|
|
|
Future<io.File> getThumbnailFromServer(File file) async {
|
|
|
- return ThumbnailCacheManager.instance
|
|
|
- .getFileFromCache(file.getThumbnailUrl())
|
|
|
- .then((info) {
|
|
|
+ return ThumbnailCacheManager.instance.getFileFromCache("asd").then((info) {
|
|
|
if (info == null) {
|
|
|
- if (!_thumbnailQueue.containsKey(file.uploadedFileID)) {
|
|
|
- final completer = Completer<io.File>();
|
|
|
- _thumbnailQueue[file.uploadedFileID] =
|
|
|
- FileDownloadItem(file, completer);
|
|
|
- _pollQueue();
|
|
|
- return completer.future;
|
|
|
+ if (!_map.containsKey(file.uploadedFileID)) {
|
|
|
+ if (_queue.length == kMaximumConcurrentDownloads) {
|
|
|
+ final id = _queue.removeFirst();
|
|
|
+ final item = _map.remove(id);
|
|
|
+ item.cancelToken.cancel();
|
|
|
+ item.completer.completeError(RequestCancelledError());
|
|
|
+ }
|
|
|
+ final item =
|
|
|
+ FileDownloadItem(file, Completer<io.File>(), CancelToken());
|
|
|
+ _map[file.uploadedFileID] = item;
|
|
|
+ _queue.add(file.uploadedFileID);
|
|
|
+ _downloadItem(item);
|
|
|
+ return item.completer.future;
|
|
|
} else {
|
|
|
- return _thumbnailQueue[file.uploadedFileID].completer.future;
|
|
|
+ return _map[file.uploadedFileID].completer.future;
|
|
|
}
|
|
|
} else {
|
|
|
ThumbnailFileLruCache.put(file, info.file);
|
|
@@ -59,71 +56,69 @@ Future<io.File> getThumbnailFromServer(File file) async {
|
|
|
}
|
|
|
|
|
|
void removePendingGetThumbnailRequestIfAny(File file) {
|
|
|
- if (_thumbnailQueue[file.uploadedFileID] != null &&
|
|
|
- _thumbnailQueue[file.uploadedFileID].status ==
|
|
|
- DownloadStatus.not_started) {
|
|
|
- _thumbnailQueue.remove(file.uploadedFileID);
|
|
|
+ if (_map.containsKey(file.uploadedFileID)) {
|
|
|
+ final item = _map.remove(file.uploadedFileID);
|
|
|
+ item.cancelToken.cancel();
|
|
|
+ _queue.removeWhere((element) => element == file.uploadedFileID);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-void _pollQueue() async {
|
|
|
- if (_thumbnailQueue.length > 0 &&
|
|
|
- _currentlyDownloading < kMaximumConcurrentDownloads) {
|
|
|
- final firstPendingEntry = _thumbnailQueue.entries.firstWhere(
|
|
|
- (entry) => entry.value.status == DownloadStatus.not_started,
|
|
|
- orElse: () => null);
|
|
|
- if (firstPendingEntry != null) {
|
|
|
- final item = firstPendingEntry.value;
|
|
|
- _currentlyDownloading++;
|
|
|
- item.status = DownloadStatus.in_progress;
|
|
|
- try {
|
|
|
- final data = await _downloadAndDecryptThumbnail(item.file);
|
|
|
- ThumbnailFileLruCache.put(item.file, data);
|
|
|
- item.completer.complete(data);
|
|
|
- } catch (e, s) {
|
|
|
- _logger.severe(
|
|
|
- "Failed to download thumbnail " + item.file.toString(), e, s);
|
|
|
- item.completer.completeError(e);
|
|
|
- }
|
|
|
- _currentlyDownloading--;
|
|
|
- _thumbnailQueue.remove(firstPendingEntry.key);
|
|
|
- _pollQueue();
|
|
|
- }
|
|
|
+void _downloadItem(FileDownloadItem item) async {
|
|
|
+ try {
|
|
|
+ await _downloadAndDecryptThumbnail(item);
|
|
|
+ } catch (e, s) {
|
|
|
+ _logger.severe(
|
|
|
+ "Failed to download thumbnail " + item.file.toString(), e, s);
|
|
|
+ item.completer.completeError(e);
|
|
|
}
|
|
|
+ _queue.removeWhere((element) => element == item.file.uploadedFileID);
|
|
|
+ _map.remove(item.file.uploadedFileID);
|
|
|
}
|
|
|
|
|
|
-Future<io.File> _downloadAndDecryptThumbnail(File file) async {
|
|
|
- final temporaryPath = Configuration.instance.getTempDirectory() +
|
|
|
- file.generatedID.toString() +
|
|
|
- "_thumbnail.decrypted";
|
|
|
- await Network.instance.getDio().download(
|
|
|
- file.getThumbnailUrl(),
|
|
|
- temporaryPath,
|
|
|
- options: Options(
|
|
|
- headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
|
|
- ),
|
|
|
- );
|
|
|
- final encryptedFile = io.File(temporaryPath);
|
|
|
+Future<void> _downloadAndDecryptThumbnail(FileDownloadItem item) async {
|
|
|
+ final file = item.file;
|
|
|
+ var encryptedThumbnail;
|
|
|
+ try {
|
|
|
+ encryptedThumbnail = (await Network.instance.getDio().get(
|
|
|
+ file.getThumbnailUrl(),
|
|
|
+ options: Options(
|
|
|
+ headers: {"X-Auth-Token": Configuration.instance.getToken()},
|
|
|
+ responseType: ResponseType.bytes,
|
|
|
+ ),
|
|
|
+ cancelToken: item.cancelToken,
|
|
|
+ )).data;
|
|
|
+ } catch (e) {
|
|
|
+ if (e is DioError && CancelToken.isCancel(e)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ if (!_map.containsKey(file.uploadedFileID)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
final thumbnailDecryptionKey = decryptFileKey(file);
|
|
|
var data = CryptoUtil.decryptChaCha(
|
|
|
- encryptedFile.readAsBytesSync(),
|
|
|
+ encryptedThumbnail,
|
|
|
thumbnailDecryptionKey,
|
|
|
Sodium.base642bin(file.thumbnailDecryptionHeader),
|
|
|
);
|
|
|
final thumbnailSize = data.length;
|
|
|
if (thumbnailSize > THUMBNAIL_DATA_LIMIT) {
|
|
|
data = await compressThumbnail(data);
|
|
|
- _logger.info("Compressed thumbnail from " +
|
|
|
- thumbnailSize.toString() +
|
|
|
- " to " +
|
|
|
- data.length.toString());
|
|
|
}
|
|
|
- encryptedFile.deleteSync();
|
|
|
- final cachedThumbnail = ThumbnailCacheManager.instance.putFile(
|
|
|
+ final cachedThumbnail = await ThumbnailCacheManager.instance.putFile(
|
|
|
file.getThumbnailUrl(),
|
|
|
data,
|
|
|
eTag: file.getThumbnailUrl(),
|
|
|
maxAge: Duration(days: 365),
|
|
|
);
|
|
|
- return cachedThumbnail;
|
|
|
+ ThumbnailFileLruCache.put(item.file, cachedThumbnail);
|
|
|
+ if (_map.containsKey(file.uploadedFileID)) {
|
|
|
+ try {
|
|
|
+ item.completer.complete(cachedThumbnail);
|
|
|
+ } catch (e) {
|
|
|
+ _logger.severe("Error while completing request for " +
|
|
|
+ file.uploadedFileID.toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|