ente/lib/utils/thumbnail_util.dart

183 lines
5.5 KiB
Dart
Raw Normal View History

2021-05-02 20:52:56 +00:00
import 'dart:async';
import 'dart:collection';
2021-07-21 20:47:43 +00:00
import 'dart:io' as io;
import 'dart:typed_data';
2021-05-02 20:52:56 +00:00
import 'package:dio/dio.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
2022-05-12 03:27:32 +00:00
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/cache/thumbnail_cache.dart';
2021-05-02 20:52:56 +00:00
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
2021-05-03 15:18:56 +00:00
import 'package:photos/core/errors.dart';
2021-05-02 20:52:56 +00:00
import 'package:photos/core/network.dart';
import 'package:photos/models/file.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_download_util.dart';
2021-10-30 05:51:23 +00:00
import 'package:photos/utils/file_uploader_util.dart';
2021-08-09 06:21:09 +00:00
import 'package:photos/utils/file_util.dart';
2021-05-02 20:52:56 +00:00
final _logger = Logger("ThumbnailUtil");
2021-07-22 18:33:35 +00:00
final _map = <int, FileDownloadItem>{};
2021-05-03 15:18:56 +00:00
final _queue = Queue<int>();
const int kMaximumConcurrentDownloads = 500;
2021-05-02 20:52:56 +00:00
class FileDownloadItem {
final File file;
final Completer<Uint8List> completer;
2021-05-03 15:18:56 +00:00
final CancelToken cancelToken;
int counter = 0; // number of times file download was requested
2021-05-02 20:52:56 +00:00
FileDownloadItem(this.file, this.completer, this.cancelToken, this.counter);
2021-05-02 20:52:56 +00:00
}
Future<Uint8List> getThumbnailFromServer(File file) async {
final cachedThumbnail = getCachedThumbnail(file);
if (cachedThumbnail.existsSync()) {
2021-08-09 06:30:30 +00:00
final data = await cachedThumbnail.readAsBytes();
ThumbnailLruCache.put(file, data);
return data;
}
if (!_map.containsKey(file.uploadedFileID)) {
if (_queue.length > kMaximumConcurrentDownloads) {
final id = _queue.removeFirst();
2022-12-29 13:37:53 +00:00
final item = _map.remove(id)!;
item.cancelToken.cancel();
item.completer.completeError(RequestCancelledError());
2021-05-02 20:52:56 +00:00
}
final item =
FileDownloadItem(file, Completer<Uint8List>(), CancelToken(), 1);
2022-12-29 13:37:53 +00:00
_map[file.uploadedFileID!] = item;
_queue.add(file.uploadedFileID!);
_downloadItem(item);
return item.completer.future;
} else {
2022-12-29 13:37:53 +00:00
_map[file.uploadedFileID]!.counter++;
return _map[file.uploadedFileID]!.completer.future;
}
2021-05-02 20:52:56 +00:00
}
2022-12-29 13:37:53 +00:00
Future<Uint8List?> getThumbnailFromLocal(
2022-06-11 08:23:52 +00:00
File file, {
int size = thumbnailSmallSize,
int quality = thumbnailQuality,
2022-06-11 08:23:52 +00:00
}) async {
2021-08-04 23:11:58 +00:00
final lruCachedThumbnail = ThumbnailLruCache.get(file, size);
if (lruCachedThumbnail != null) {
return lruCachedThumbnail;
}
final cachedThumbnail = getCachedThumbnail(file);
if (cachedThumbnail.existsSync()) {
2021-08-09 06:30:30 +00:00
final data = await cachedThumbnail.readAsBytes();
ThumbnailLruCache.put(file, data);
return data;
}
2022-09-23 01:48:25 +00:00
if (file.isSharedMediaToAppSandbox) {
//todo:neeraj support specifying size/quality
return getThumbnailFromInAppCacheFile(file).then((data) {
if (data != null) {
ThumbnailLruCache.put(file, data, size);
}
return data;
});
} else {
2022-09-23 01:48:25 +00:00
return file.getAsset.then((asset) async {
if (asset == null || !(await asset.exists)) {
return null;
}
2022-05-12 03:27:32 +00:00
return asset
.thumbnailDataWithSize(ThumbnailSize(size, size), quality: quality)
.then((data) {
ThumbnailLruCache.put(file, data, size);
return data;
});
});
}
}
2021-05-02 20:52:56 +00:00
void removePendingGetThumbnailRequestIfAny(File file) {
2021-05-03 15:18:56 +00:00
if (_map.containsKey(file.uploadedFileID)) {
2022-12-29 13:37:53 +00:00
final item = _map[file.uploadedFileID]!;
item.counter--;
if (item.counter <= 0) {
_map.remove(file.uploadedFileID);
item.cancelToken.cancel();
_queue.removeWhere((element) => element == file.uploadedFileID);
}
2021-05-02 20:52:56 +00:00
}
}
2021-05-03 15:18:56 +00:00
void _downloadItem(FileDownloadItem item) async {
try {
await _downloadAndDecryptThumbnail(item);
} catch (e, s) {
_logger.severe(
2022-06-11 08:23:52 +00:00
"Failed to download thumbnail " + item.file.toString(),
e,
s,
);
2021-05-03 15:18:56 +00:00
item.completer.completeError(e);
2021-05-02 20:52:56 +00:00
}
2021-05-03 15:18:56 +00:00
_queue.removeWhere((element) => element == item.file.uploadedFileID);
_map.remove(item.file.uploadedFileID);
2021-05-02 20:52:56 +00:00
}
2021-05-03 15:18:56 +00:00
Future<void> _downloadAndDecryptThumbnail(FileDownloadItem item) async {
final file = item.file;
2021-07-22 18:33:35 +00:00
Uint8List encryptedThumbnail;
2021-05-03 15:18:56 +00:00
try {
encryptedThumbnail = (await Network.instance.getDio().get(
2022-09-23 01:48:25 +00:00
file.thumbnailUrl,
2021-05-03 18:48:13 +00:00
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()},
responseType: ResponseType.bytes,
),
cancelToken: item.cancelToken,
))
.data;
2021-05-03 15:18:56 +00:00
} catch (e) {
if (e is DioError && CancelToken.isCancel(e)) {
return;
}
2021-07-22 18:33:35 +00:00
rethrow;
2021-05-03 15:18:56 +00:00
}
if (!_map.containsKey(file.uploadedFileID)) {
return;
}
2021-05-02 20:52:56 +00:00
final thumbnailDecryptionKey = decryptFileKey(file);
var data = await CryptoUtil.decryptChaCha(
2021-05-03 15:18:56 +00:00
encryptedThumbnail,
2021-05-02 20:52:56 +00:00
thumbnailDecryptionKey,
2022-12-29 13:37:53 +00:00
Sodium.base642bin(file.thumbnailDecryptionHeader!),
2021-05-02 20:52:56 +00:00
);
final thumbnailSize = data.length;
if (thumbnailSize > thumbnailDataLimit) {
2021-05-02 20:52:56 +00:00
data = await compressThumbnail(data);
}
ThumbnailLruCache.put(item.file, data);
final cachedThumbnail = getCachedThumbnail(item.file);
if (cachedThumbnail.existsSync()) {
2021-08-09 06:21:09 +00:00
await cachedThumbnail.delete();
}
// data is already cached in-memory, no need to await on dist write
unawaited(cachedThumbnail.writeAsBytes(data));
2021-05-03 15:18:56 +00:00
if (_map.containsKey(file.uploadedFileID)) {
try {
item.completer.complete(data);
2021-05-03 15:18:56 +00:00
} catch (e) {
2022-06-11 08:23:52 +00:00
_logger.severe(
"Error while completing request for " + file.uploadedFileID.toString(),
);
2021-05-03 15:18:56 +00:00
}
}
2021-05-02 20:52:56 +00:00
}
io.File getCachedThumbnail(File file) {
final thumbnailCacheDirectory =
Configuration.instance.getThumbnailCacheDirectory();
return io.File(
2022-06-11 08:23:52 +00:00
thumbnailCacheDirectory + "/" + file.uploadedFileID.toString(),
);
}