From 7d8ffc0fae5aef7d3a920c325259b89c6cd419c2 Mon Sep 17 00:00:00 2001 From: iota9star Date: Thu, 22 Jul 2021 23:20:22 +0800 Subject: [PATCH 1/9] :sparkles: Add http cache support. --- lib/src/_network_image_io.dart | 367 ++++++++++--------- lib/src/extended_network_image_provider.dart | 7 +- pubspec.lock | 29 +- pubspec.yaml | 6 +- 4 files changed, 217 insertions(+), 192 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index a686c26..3ef2ee9 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui show Codec; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:http_client_helper/http_client_helper.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; + import 'extended_image_provider.dart'; import 'extended_network_image_provider.dart' as image_provider; import 'platform.dart'; @@ -31,7 +33,6 @@ class ExtendedNetworkImageProvider this.cacheRawData = false, this.cancelToken, this.imageCacheName, - this.cacheMaxAge, }); /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. @@ -85,11 +86,6 @@ class ExtendedNetworkImageProvider @override final bool printError; - /// The max duration to cahce image. - /// After this time the cache is expired and the image is reloaded. - @override - final Duration? cacheMaxAge; - @override ImageStreamCompleter load( image_provider.ExtendedNetworkImageProvider key, DecoderCallback decode) { @@ -129,169 +125,18 @@ class ExtendedNetworkImageProvider DecoderCallback decode, ) async { assert(key == this); - final String md5Key = cacheKey ?? keyToMd5(key.url); - ui.Codec? result; - if (cache) { - try { - final Uint8List? data = await _loadCache( - key, - chunkEvents, - md5Key, - ); - if (data != null) { - result = await instantiateImageCodec(data, decode); - } - } catch (e) { - if (printError) { - print(e); - } - } - } - - if (result == null) { - try { - final Uint8List? data = await _loadNetwork( - key, - chunkEvents, - ); - if (data != null) { - result = await instantiateImageCodec(data, decode); - } - } catch (e) { - if (printError) { - print(e); - } - } - } - - //Failed to load - if (result == null) { - //result = await ui.instantiateImageCodec(kTransparentImage); + final Uint8List? uint8list = await _(key.url, chunkEvents); + if (uint8list == null) { return Future.error(StateError('Failed to load $url.')); } - - return result; - } - - /// Get the image from cache folder. - Future _loadCache( - ExtendedNetworkImageProvider key, - StreamController? chunkEvents, - String md5Key, - ) async { - final Directory _cacheImagesDirectory = Directory( - join((await getTemporaryDirectory()).path, cacheImageFolderName)); - Uint8List? data; - // exist, try to find cache image file - if (_cacheImagesDirectory.existsSync()) { - final File cacheFlie = File(join(_cacheImagesDirectory.path, md5Key)); - if (cacheFlie.existsSync()) { - if (key.cacheMaxAge != null) { - final DateTime now = DateTime.now(); - final FileStat fs = cacheFlie.statSync(); - if (now.subtract(key.cacheMaxAge!).isAfter(fs.changed)) { - cacheFlie.deleteSync(recursive: true); - } else { - data = await cacheFlie.readAsBytes(); - } - } else { - data = await cacheFlie.readAsBytes(); - } - } - } - // create folder - else { - await _cacheImagesDirectory.create(); - } - // load from network - if (data == null) { - data = await _loadNetwork( - key, - chunkEvents, - ); - if (data != null) { - // cache image file - await File(join(_cacheImagesDirectory.path, md5Key)).writeAsBytes(data); - } - } - - return data; - } - - /// Get the image from network. - Future _loadNetwork( - ExtendedNetworkImageProvider key, - StreamController? chunkEvents, - ) async { try { - final Uri resolved = Uri.base.resolve(key.url); - final HttpClientResponse? response = await _tryGetResponse(resolved); - if (response == null || response.statusCode != HttpStatus.ok) { - return null; - } - - final Uint8List bytes = await consolidateHttpClientResponseBytes( - response, - onBytesReceived: chunkEvents != null - ? (int cumulative, int? total) { - chunkEvents.add(ImageChunkEvent( - cumulativeBytesLoaded: cumulative, - expectedTotalBytes: total, - )); - } - : null, - ); - if (bytes.lengthInBytes == 0) { - return Future.error( - StateError('NetworkImage is an empty file: $resolved')); - } - - return bytes; - } on OperationCanceledError catch (_) { - if (printError) { - print('User cancel request $url.'); - } - return Future.error(StateError('User cancel request $url.')); + return await instantiateImageCodec(uint8list, decode); } catch (e) { if (printError) { print(e); } - } finally { - await chunkEvents?.close(); - } - return null; - } - - Future _getResponse(Uri resolved) async { - final HttpClientRequest request = await httpClient.getUrl(resolved); - headers?.forEach((String name, String value) { - request.headers.add(name, value); - }); - final HttpClientResponse response = await request.close(); - if (timeLimit != null) { - response.timeout( - timeLimit!, - ); + return Future.error(StateError('Failed to load $url.')); } - return response; - } - - // Http get with cancel, delay try again - Future _tryGetResponse( - Uri resolved, - ) async { - cancelToken?.throwIfCancellationRequested(); - return await RetryHelper.tryRun( - () { - return CancellationTokenSource.register( - cancelToken, - _getResponse(resolved), - ); - }, - cancelToken: cancelToken, - timeRetry: timeRetry, - retries: retries, - ); } @override @@ -310,8 +155,7 @@ class ExtendedNetworkImageProvider cacheKey == other.cacheKey && headers == other.headers && retries == other.retries && - imageCacheName == other.imageCacheName && - cacheMaxAge == other.cacheMaxAge; + imageCacheName == other.imageCacheName; } @override @@ -327,7 +171,6 @@ class ExtendedNetworkImageProvider headers, retries, imageCacheName, - cacheMaxAge, ); @override @@ -339,19 +182,195 @@ class ExtendedNetworkImageProvider Future getNetworkImageData({ StreamController? chunkEvents, }) async { - final String uId = cacheKey ?? keyToMd5(url); + return await _(url, chunkEvents); + } - if (cache) { - return await _loadCache( - this, - chunkEvents, - uId, + Future _getCacheDir() async { + final Directory dir = Directory( + join((await getTemporaryDirectory()).path, cacheImageFolderName)); + // ignore: avoid_slow_async_io + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + + File _childFile(Directory parentDir, String fileName) { + return File(join(parentDir.path, fileName)); + } + + Future _( + String url, + StreamController? chunkEvents, + ) async { + final Uri uri = Uri.parse(url); + // create req + final HttpClientResponse? checkResp = await _retryRequest(uri); + if (checkResp == null) { + return null; + } + if (!cache) { + return await _rw(checkResp, chunkEvents, null); + } + final String rawFileName = cacheKey ?? keyToMd5(url); + final Directory parentDir = await _getCacheDir(); + final File rawFile = _childFile(parentDir, rawFileName); + + bool isExpired = false; + final String? cacheControl = + checkResp.headers.value(HttpHeaders.cacheControlHeader); + if (cacheControl != null) { + if (cacheControl.contains('no-store')) { + // no cache, download now. + return await _rw(checkResp, chunkEvents, rawFile); + } else { + final File lockFile = _childFile(parentDir, '$rawFileName.lock'); + // ignore: avoid_slow_async_io + final bool exist = await lockFile.exists(); + String maxAgeKey = 'max-age'; + if (cacheControl.contains(maxAgeKey)) { + if (cacheControl.contains('s-maxage')) { + maxAgeKey = 's-maxage'; + } + final String maxAgeStr = cacheControl + .split(' ') + .firstWhere((String element) => element.contains(maxAgeKey)) + .split('=')[1] + .trim(); + final String seconds = RegExp(r'\d+').stringMatch(maxAgeStr)!; + final int maxAge = int.parse(seconds) * 1000; + final String newFlag = + '${checkResp.headers.value(HttpHeaders.etagHeader).toString()}_${checkResp.headers.value(HttpHeaders.lastModifiedHeader).toString()}'; + final int now = DateTime.now().millisecondsSinceEpoch; + if (exist) { + final String lockStr = await lockFile.readAsString(); + if (lockStr.isNotEmpty) { + final List split = lockStr.split('@'); + final String flag = split[1]; + final int lastReqAt = int.parse(split[0]); + if (flag != newFlag || lastReqAt + maxAge < now) { + isExpired = true; + } + } + } else { + await lockFile.create(); + } + await lockFile.writeAsString([now, newFlag].join('@')); + } + } + } + if (!isExpired) { + // if not expired and exist file, just return. + // ignore: avoid_slow_async_io + if (await rawFile.exists()) { + return await rawFile.readAsBytes(); + } + } + // request error + if (checkResp.statusCode != HttpStatus.ok) { + return null; + } + final bool breakpointTransmission = + checkResp.headers.value(HttpHeaders.acceptRangesHeader) == 'bytes' && + checkResp.contentLength > 0; + final File tempFile = _childFile(parentDir, '$rawFileName.temp'); + Uint8List? bytes; + // if not expired and is support breakpoint transmission and temp file exists + // ignore: avoid_slow_async_io + if (!isExpired && breakpointTransmission && await tempFile.exists()) { + final int length = await tempFile.length(); + final HttpClientResponse? resp = + await _retryRequest(uri, callRequest: (HttpClientRequest req) { + req.headers.add(HttpHeaders.rangeHeader, 'bytes=$length-'); + final String? flag = checkResp.headers.value(HttpHeaders.etagHeader) ?? + checkResp.headers.value(HttpHeaders.lastModifiedHeader); + if (flag != null) { + req.headers.add(HttpHeaders.ifRangeHeader, flag); + } + }); + if (resp == null) { + return null; + } + if (resp.statusCode == HttpStatus.partialContent) { + // is ok, continue download. + bytes = await _rw( + resp, + chunkEvents, + tempFile, + loaded: length, + fileMode: FileMode.append, + ); + } else if (resp.statusCode == HttpStatus.requestedRangeNotSatisfiable) { + // 416 Requested Range Not Satisfiable + bytes = await _rw(checkResp, chunkEvents, tempFile); + } else if (resp.statusCode == HttpStatus.ok) { + bytes = await _rw(resp, chunkEvents, tempFile); + } else { + // request error. + return null; + } + } else { + bytes = await _rw(checkResp, chunkEvents, tempFile); + } + await tempFile.rename(rawFile.path); + return bytes; + } + + Future _rw( + HttpClientResponse response, + StreamController? chunkEvents, + File? file, { + int loaded = 0, + FileMode fileMode = FileMode.write, + }) async { + final Uint8List bytes = await consolidateHttpClientResponseBytes( + response, + onBytesReceived: chunkEvents != null + ? (int cumulative, int? total) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: cumulative + loaded, + expectedTotalBytes: total == null ? null : total + loaded, + )); + } + : null, + ); + await file?.writeAsBytes(bytes, mode: fileMode); + return bytes; + } + + Future _createNewRequest( + Uri uri, { + _CallRequest? callRequest, + }) async { + final HttpClientRequest request = await httpClient.getUrl(uri); + headers?.forEach((String key, Object value) { + request.headers.add(key, value); + }); + callRequest?.call(request); + final HttpClientResponse response = await request.close(); + if (timeLimit != null) { + response.timeout( + timeLimit!, ); } + return response; + } - return await _loadNetwork( - this, - chunkEvents, + Future _retryRequest( + Uri uri, { + _CallRequest? callRequest, + }) async { + cancelToken?.throwIfCancellationRequested(); + return await RetryHelper.tryRun( + () { + return CancellationTokenSource.register( + cancelToken, + _createNewRequest(uri, callRequest: callRequest), + ); + }, + cancelToken: cancelToken, + timeRetry: timeRetry, + retries: retries, ); } @@ -373,3 +392,5 @@ class ExtendedNetworkImageProvider return client; } } + +typedef _CallRequest = void Function(HttpClientRequest request); diff --git a/lib/src/extended_network_image_provider.dart b/lib/src/extended_network_image_provider.dart index b607cb4..9113ca2 100644 --- a/lib/src/extended_network_image_provider.dart +++ b/lib/src/extended_network_image_provider.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:typed_data'; + import 'package:flutter/painting.dart'; import 'package:http_client_helper/http_client_helper.dart'; + import '_network_image_io.dart' if (dart.library.html) '_network_image_web.dart' as network_image; @@ -23,7 +25,6 @@ abstract class ExtendedNetworkImageProvider bool printError, bool cacheRawData, String? imageCacheName, - Duration? cacheMaxAge, }) = network_image.ExtendedNetworkImageProvider; /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. @@ -67,10 +68,6 @@ abstract class ExtendedNetworkImageProvider /// print error bool get printError; - /// The max duration to cahce image. - /// After this time the cache is expired and the image is reloaded. - Duration? get cacheMaxAge; - @override ImageStreamCompleter load( ExtendedNetworkImageProvider key, DecoderCallback decode); diff --git a/pubspec.lock b/pubspec.lock index afcab64..238667c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" characters: dependency: transitive description: @@ -14,7 +21,7 @@ packages: name: charcode url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.3.1" collection: dependency: transitive description: @@ -28,21 +35,21 @@ packages: name: crypto url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.0" + version: "3.0.1" ffi: dependency: transitive description: name: ffi url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.0" + version: "1.1.2" file: dependency: transitive description: name: file url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.0" + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -54,7 +61,7 @@ packages: name: http url: "https://pub.flutter-io.cn" source: hosted - version: "0.13.1" + version: "0.13.3" http_client_helper: dependency: "direct main" description: @@ -89,7 +96,7 @@ packages: name: path_provider url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.1" + version: "2.0.2" path_provider_linux: dependency: transitive description: @@ -117,14 +124,14 @@ packages: name: path_provider_windows url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.0.1" pedantic: dependency: transitive description: name: pedantic url: "https://pub.flutter-io.cn" source: hosted - version: "1.11.0" + version: "1.11.1" platform: dependency: transitive description: @@ -145,7 +152,7 @@ packages: name: process url: "https://pub.flutter-io.cn" source: hosted - version: "4.2.0" + version: "4.2.1" sky_engine: dependency: transitive description: flutter @@ -192,7 +199,7 @@ packages: name: win32 url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.4" + version: "2.2.2" xdg_directories: dependency: transitive description: @@ -201,5 +208,5 @@ packages: source: hosted version: "0.2.0" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.13.0 <3.0.0" flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index 823cf6d..695c58c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,12 +7,12 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: - crypto: ^3.0.0 + crypto: ^3.0.1 flutter: sdk: flutter http_client_helper: ^2.0.2 - path: ^1.7.0 - path_provider: ^2.0.1 + path: ^1.8.0 + path_provider: ^2.0.2 From c8e3f86cc0479d8664289c7b16812e39c3a18a86 Mon Sep 17 00:00:00 2001 From: iota9star Date: Fri, 23 Jul 2021 00:13:21 +0800 Subject: [PATCH 2/9] :construction: Change base cache dir. --- lib/src/platform.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/platform.dart b/lib/src/platform.dart index 0c44e05..7ea34ca 100644 --- a/lib/src/platform.dart +++ b/lib/src/platform.dart @@ -12,7 +12,7 @@ export '_extended_network_image_utils_io.dart' if (dart.library.html) '_extended_network_image_utils_web.dart'; export '_platform_io.dart' if (dart.library.html) '_platform_web.dart'; -const String cacheImageFolderName = 'cacheimage'; +const String cacheImageFolderName = 'extended_image_cache'; ///clear all of image in memory void clearMemoryImageCache([String? name]) { From 9e92cd1d3eafbcb1728a261a7de397b8a9fa2e4e Mon Sep 17 00:00:00 2001 From: iota9star Date: Fri, 23 Jul 2021 16:48:24 +0800 Subject: [PATCH 3/9] :art: Restore `cacheMaxAge` field and improve structure. --- lib/src/_network_image_io.dart | 55 +++++++++++++------- lib/src/extended_network_image_provider.dart | 5 ++ 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index 3ef2ee9..bf995d9 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -33,6 +33,7 @@ class ExtendedNetworkImageProvider this.cacheRawData = false, this.cancelToken, this.imageCacheName, + this.cacheMaxAge, }); /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. @@ -86,6 +87,11 @@ class ExtendedNetworkImageProvider @override final bool printError; + /// The max duration to cache image. + /// After this time the cache is expired and the image is reloaded. + @override + final Duration? cacheMaxAge; + @override ImageStreamCompleter load( image_provider.ExtendedNetworkImageProvider key, DecoderCallback decode) { @@ -188,8 +194,7 @@ class ExtendedNetworkImageProvider Future _getCacheDir() async { final Directory dir = Directory( join((await getTemporaryDirectory()).path, cacheImageFolderName)); - // ignore: avoid_slow_async_io - if (!await dir.exists()) { + if (!dir.existsSync()) { await dir.create(recursive: true); } return dir; @@ -206,15 +211,22 @@ class ExtendedNetworkImageProvider final Uri uri = Uri.parse(url); // create req final HttpClientResponse? checkResp = await _retryRequest(uri); - if (checkResp == null) { + + final String rawFileName = cacheKey ?? keyToMd5(url); + final Directory parentDir = await _getCacheDir(); + final File rawFile = _childFile(parentDir, rawFileName); + + if (checkResp == null || checkResp.statusCode != HttpStatus.ok) { + // if request error, use cache. + if (cache && rawFile.existsSync()) { + return await rawFile.readAsBytes(); + } return null; } + if (!cache) { return await _rw(checkResp, chunkEvents, null); } - final String rawFileName = cacheKey ?? keyToMd5(url); - final Directory parentDir = await _getCacheDir(); - final File rawFile = _childFile(parentDir, rawFileName); bool isExpired = false; final String? cacheControl = @@ -222,19 +234,19 @@ class ExtendedNetworkImageProvider if (cacheControl != null) { if (cacheControl.contains('no-store')) { // no cache, download now. - return await _rw(checkResp, chunkEvents, rawFile); + return await _rw(checkResp, chunkEvents, null); } else { final File lockFile = _childFile(parentDir, '$rawFileName.lock'); - // ignore: avoid_slow_async_io - final bool exist = await lockFile.exists(); + final bool exist = lockFile.existsSync(); String maxAgeKey = 'max-age'; if (cacheControl.contains(maxAgeKey)) { + // if exist s-maxage, override max-age, use cdn max-age if (cacheControl.contains('s-maxage')) { maxAgeKey = 's-maxage'; } final String maxAgeStr = cacheControl .split(' ') - .firstWhere((String element) => element.contains(maxAgeKey)) + .firstWhere((String str) => str.contains(maxAgeKey)) .split('=')[1] .trim(); final String seconds = RegExp(r'\d+').stringMatch(maxAgeStr)!; @@ -261,23 +273,26 @@ class ExtendedNetworkImageProvider } if (!isExpired) { // if not expired and exist file, just return. - // ignore: avoid_slow_async_io - if (await rawFile.exists()) { - return await rawFile.readAsBytes(); + if (rawFile.existsSync()) { + if (cacheMaxAge != null) { + final DateTime now = DateTime.now(); + final FileStat fs = rawFile.statSync(); + if (now.subtract(cacheMaxAge!).isBefore(fs.changed)) { + return await rawFile.readAsBytes(); + } + } else { + return await rawFile.readAsBytes(); + } } } - // request error - if (checkResp.statusCode != HttpStatus.ok) { - return null; - } + final bool breakpointTransmission = checkResp.headers.value(HttpHeaders.acceptRangesHeader) == 'bytes' && checkResp.contentLength > 0; final File tempFile = _childFile(parentDir, '$rawFileName.temp'); Uint8List? bytes; - // if not expired and is support breakpoint transmission and temp file exists - // ignore: avoid_slow_async_io - if (!isExpired && breakpointTransmission && await tempFile.exists()) { + // if not expired && is support breakpoint transmission && temp file exists + if (!isExpired && breakpointTransmission && tempFile.existsSync()) { final int length = await tempFile.length(); final HttpClientResponse? resp = await _retryRequest(uri, callRequest: (HttpClientRequest req) { diff --git a/lib/src/extended_network_image_provider.dart b/lib/src/extended_network_image_provider.dart index 9113ca2..71bcd54 100644 --- a/lib/src/extended_network_image_provider.dart +++ b/lib/src/extended_network_image_provider.dart @@ -25,6 +25,7 @@ abstract class ExtendedNetworkImageProvider bool printError, bool cacheRawData, String? imageCacheName, + Duration? cacheMaxAge, }) = network_image.ExtendedNetworkImageProvider; /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. @@ -68,6 +69,10 @@ abstract class ExtendedNetworkImageProvider /// print error bool get printError; + /// The max duration to cache image. + /// After this time the cache is expired and the image is reloaded. + Duration? get cacheMaxAge; + @override ImageStreamCompleter load( ExtendedNetworkImageProvider key, DecoderCallback decode); From 45cd2fcc2dbf6764e618beaf504c4f9f45e7b4b5 Mon Sep 17 00:00:00 2001 From: iota9star Date: Fri, 23 Jul 2021 18:27:02 +0800 Subject: [PATCH 4/9] :zap: Add lock cache, improve performance. --- lib/src/_network_image_io.dart | 64 +++++++++++++------- lib/src/extended_network_image_provider.dart | 5 ++ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index bf995d9..580254d 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -36,6 +36,8 @@ class ExtendedNetworkImageProvider this.cacheMaxAge, }); + static final Map _lockCache = {}; + /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. @override final String? imageCacheName; @@ -125,6 +127,19 @@ class ExtendedNetworkImageProvider return SynchronousFuture(this); } + @override + Future evict({ + ImageCache? cache, + ImageConfiguration configuration = ImageConfiguration.empty, + bool includeLive = true, + }) async { + rawImageDataMap.remove(this); + cache ??= this.imageCache; + final ExtendedNetworkImageProvider key = await obtainKey(configuration); + _lockCache.remove(key.url); + return cache.evict(key, includeLive: includeLive); + } + Future _loadAsync( ExtendedNetworkImageProvider key, StreamController chunkEvents, @@ -182,9 +197,8 @@ class ExtendedNetworkImageProvider @override String toString() => '$runtimeType("$url", scale: $scale)'; - @override - /// Get network image data from cached + @override Future getNetworkImageData({ StreamController? chunkEvents, }) async { @@ -212,9 +226,9 @@ class ExtendedNetworkImageProvider // create req final HttpClientResponse? checkResp = await _retryRequest(uri); - final String rawFileName = cacheKey ?? keyToMd5(url); + final String rawFileKey = cacheKey ?? keyToMd5(url); final Directory parentDir = await _getCacheDir(); - final File rawFile = _childFile(parentDir, rawFileName); + final File rawFile = _childFile(parentDir, rawFileKey); if (checkResp == null || checkResp.statusCode != HttpStatus.ok) { // if request error, use cache. @@ -236,8 +250,6 @@ class ExtendedNetworkImageProvider // no cache, download now. return await _rw(checkResp, chunkEvents, null); } else { - final File lockFile = _childFile(parentDir, '$rawFileName.lock'); - final bool exist = lockFile.existsSync(); String maxAgeKey = 'max-age'; if (cacheControl.contains(maxAgeKey)) { // if exist s-maxage, override max-age, use cdn max-age @@ -253,21 +265,30 @@ class ExtendedNetworkImageProvider final int maxAge = int.parse(seconds) * 1000; final String newFlag = '${checkResp.headers.value(HttpHeaders.etagHeader).toString()}_${checkResp.headers.value(HttpHeaders.lastModifiedHeader).toString()}'; - final int now = DateTime.now().millisecondsSinceEpoch; - if (exist) { - final String lockStr = await lockFile.readAsString(); - if (lockStr.isNotEmpty) { - final List split = lockStr.split('@'); - final String flag = split[1]; - final int lastReqAt = int.parse(split[0]); - if (flag != newFlag || lastReqAt + maxAge < now) { - isExpired = true; - } + final File lockFile = _childFile(parentDir, '$rawFileKey.lock'); + String? lockStr = _lockCache[url]; + if (lockStr == null) { + // never empty or blank. + if (lockFile.existsSync()) { + lockStr = await lockFile.readAsString(); + } else { + await lockFile.create(); + } + } + final int millis = DateTime.now().millisecondsSinceEpoch; + if (lockStr != null) { + //never empty or blank + final List split = lockStr.split('@'); + final String flag = split[1]; + final int lastReqAt = int.parse(split[0]); + if (flag != newFlag || lastReqAt + maxAge < millis) { + isExpired = true; } - } else { - await lockFile.create(); } - await lockFile.writeAsString([now, newFlag].join('@')); + final String newLockStr = [millis, newFlag].join('@'); + _lockCache[url] = newLockStr; + // we don't care lock str already written in file. + lockFile.writeAsString(newLockStr); } } } @@ -275,9 +296,8 @@ class ExtendedNetworkImageProvider // if not expired and exist file, just return. if (rawFile.existsSync()) { if (cacheMaxAge != null) { - final DateTime now = DateTime.now(); final FileStat fs = rawFile.statSync(); - if (now.subtract(cacheMaxAge!).isBefore(fs.changed)) { + if (DateTime.now().subtract(cacheMaxAge!).isBefore(fs.changed)) { return await rawFile.readAsBytes(); } } else { @@ -289,7 +309,7 @@ class ExtendedNetworkImageProvider final bool breakpointTransmission = checkResp.headers.value(HttpHeaders.acceptRangesHeader) == 'bytes' && checkResp.contentLength > 0; - final File tempFile = _childFile(parentDir, '$rawFileName.temp'); + final File tempFile = _childFile(parentDir, '$rawFileKey.temp'); Uint8List? bytes; // if not expired && is support breakpoint transmission && temp file exists if (!isExpired && breakpointTransmission && tempFile.existsSync()) { diff --git a/lib/src/extended_network_image_provider.dart b/lib/src/extended_network_image_provider.dart index 71bcd54..7b661bf 100644 --- a/lib/src/extended_network_image_provider.dart +++ b/lib/src/extended_network_image_provider.dart @@ -82,6 +82,11 @@ abstract class ExtendedNetworkImageProvider StreamController? chunkEvents, }); + @override + Future evict( + {ImageCache? cache, + ImageConfiguration configuration = ImageConfiguration.empty}); + ///HttpClient for network, it's null on web static dynamic get httpClient => network_image.ExtendedNetworkImageProvider.httpClient; From 8a3c797cd6c15567def54d62ded24c7a9c7457f9 Mon Sep 17 00:00:00 2001 From: iota9star Date: Sat, 24 Jul 2021 13:57:56 +0800 Subject: [PATCH 5/9] :bug: Fixed http leak. --- lib/src/_network_image_io.dart | 111 ++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 36 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index 580254d..c4203a6 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -223,32 +223,37 @@ class ExtendedNetworkImageProvider StreamController? chunkEvents, ) async { final Uri uri = Uri.parse(url); - // create req - final HttpClientResponse? checkResp = await _retryRequest(uri); + + final bool noCache = !cache; + final HttpClientResponse? checkResp = + await _retryRequest(uri, withBody: noCache); final String rawFileKey = cacheKey ?? keyToMd5(url); final Directory parentDir = await _getCacheDir(); final File rawFile = _childFile(parentDir, rawFileKey); + // if request error, use cache. if (checkResp == null || checkResp.statusCode != HttpStatus.ok) { - // if request error, use cache. - if (cache && rawFile.existsSync()) { + if (rawFile.existsSync()) { return await rawFile.readAsBytes(); } return null; } - if (!cache) { - return await _rw(checkResp, chunkEvents, null); + if (noCache) { + return await _rw(checkResp, rawFile, null, chunkEvents: chunkEvents); } + // consuming response. + checkResp.listen(null); + bool isExpired = false; final String? cacheControl = checkResp.headers.value(HttpHeaders.cacheControlHeader); if (cacheControl != null) { if (cacheControl.contains('no-store')) { // no cache, download now. - return await _rw(checkResp, chunkEvents, null); + return await _nrw(uri, rawFile, null, chunkEvents: chunkEvents); } else { String maxAgeKey = 'max-age'; if (cacheControl.contains(maxAgeKey)) { @@ -310,52 +315,76 @@ class ExtendedNetworkImageProvider checkResp.headers.value(HttpHeaders.acceptRangesHeader) == 'bytes' && checkResp.contentLength > 0; final File tempFile = _childFile(parentDir, '$rawFileKey.temp'); - Uint8List? bytes; // if not expired && is support breakpoint transmission && temp file exists if (!isExpired && breakpointTransmission && tempFile.existsSync()) { final int length = await tempFile.length(); - final HttpClientResponse? resp = - await _retryRequest(uri, callRequest: (HttpClientRequest req) { - req.headers.add(HttpHeaders.rangeHeader, 'bytes=$length-'); - final String? flag = checkResp.headers.value(HttpHeaders.etagHeader) ?? - checkResp.headers.value(HttpHeaders.lastModifiedHeader); - if (flag != null) { - req.headers.add(HttpHeaders.ifRangeHeader, flag); - } - }); + final HttpClientResponse? resp = await _retryRequest( + uri, + beforeRequest: (HttpClientRequest req) { + req.headers.add(HttpHeaders.rangeHeader, 'bytes=$length-'); + final String? flag = + checkResp.headers.value(HttpHeaders.etagHeader) ?? + checkResp.headers.value(HttpHeaders.lastModifiedHeader); + if (flag != null) { + req.headers.add(HttpHeaders.ifRangeHeader, flag); + } + }, + ); if (resp == null) { return null; } if (resp.statusCode == HttpStatus.partialContent) { // is ok, continue download. - bytes = await _rw( + return await _rw( resp, - chunkEvents, + rawFile, tempFile, - loaded: length, + chunkEvents: chunkEvents, + loadedLength: length, fileMode: FileMode.append, ); } else if (resp.statusCode == HttpStatus.requestedRangeNotSatisfiable) { // 416 Requested Range Not Satisfiable - bytes = await _rw(checkResp, chunkEvents, tempFile); + return await _nrw( + uri, + rawFile, + tempFile, + chunkEvents: chunkEvents, + ); } else if (resp.statusCode == HttpStatus.ok) { - bytes = await _rw(resp, chunkEvents, tempFile); + return await _rw(resp, rawFile, tempFile, chunkEvents: chunkEvents); } else { // request error. return null; } } else { - bytes = await _rw(checkResp, chunkEvents, tempFile); + return await _nrw( + uri, + rawFile, + tempFile, + chunkEvents: chunkEvents, + ); } - await tempFile.rename(rawFile.path); - return bytes; + } + + Future _nrw( + Uri uri, + File rawFile, + File? tempFile, { + StreamController? chunkEvents, + }) async { + final HttpClientResponse? resp = await _retryRequest(uri); + return resp == null + ? null + : await _rw(resp, rawFile, tempFile, chunkEvents: chunkEvents); } Future _rw( HttpClientResponse response, + File rawFile, + File? tempFile, { StreamController? chunkEvents, - File? file, { - int loaded = 0, + int loadedLength = 0, FileMode fileMode = FileMode.write, }) async { final Uint8List bytes = await consolidateHttpClientResponseBytes( @@ -363,25 +392,30 @@ class ExtendedNetworkImageProvider onBytesReceived: chunkEvents != null ? (int cumulative, int? total) { chunkEvents.add(ImageChunkEvent( - cumulativeBytesLoaded: cumulative + loaded, - expectedTotalBytes: total == null ? null : total + loaded, + cumulativeBytesLoaded: cumulative + loadedLength, + expectedTotalBytes: total == null ? null : total + loadedLength, )); } : null, ); - await file?.writeAsBytes(bytes, mode: fileMode); + if (tempFile != null) { + await tempFile.writeAsBytes(bytes, mode: fileMode); + await tempFile.rename(rawFile.path); + } return bytes; } Future _createNewRequest( Uri uri, { - _CallRequest? callRequest, + bool withBody = true, + _BeforeRequest? beforeRequest, }) async { - final HttpClientRequest request = await httpClient.getUrl(uri); + final HttpClientRequest request = + await (withBody ? httpClient.getUrl(uri) : httpClient.headUrl(uri)); headers?.forEach((String key, Object value) { request.headers.add(key, value); }); - callRequest?.call(request); + beforeRequest?.call(request); final HttpClientResponse response = await request.close(); if (timeLimit != null) { response.timeout( @@ -393,14 +427,19 @@ class ExtendedNetworkImageProvider Future _retryRequest( Uri uri, { - _CallRequest? callRequest, + bool withBody = true, + _BeforeRequest? beforeRequest, }) async { cancelToken?.throwIfCancellationRequested(); return await RetryHelper.tryRun( () { return CancellationTokenSource.register( cancelToken, - _createNewRequest(uri, callRequest: callRequest), + _createNewRequest( + uri, + withBody: withBody, + beforeRequest: beforeRequest, + ), ); }, cancelToken: cancelToken, @@ -428,4 +467,4 @@ class ExtendedNetworkImageProvider } } -typedef _CallRequest = void Function(HttpClientRequest request); +typedef _BeforeRequest = void Function(HttpClientRequest request); From 28d6437210fc14066b38a50c15951d6e732dbfdc Mon Sep 17 00:00:00 2001 From: iota9star Date: Sat, 24 Jul 2021 14:11:42 +0800 Subject: [PATCH 6/9] :art: Rearrange methods and fields. --- lib/src/_network_image_io.dart | 121 +++++++++++++++++---------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index c4203a6..e45cd1c 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -38,6 +38,24 @@ class ExtendedNetworkImageProvider static final Map _lockCache = {}; + // Do not access this field directly; use [_httpClient] instead. + // We set `autoUncompress` to false to ensure that we can trust the value of + // the `Content-Length` HTTP header. We automatically uncompress the content + // in our call to [consolidateHttpClientResponseBytes]. + static final HttpClient _sharedHttpClient = HttpClient() + ..autoUncompress = false; + + static HttpClient get httpClient { + HttpClient client = _sharedHttpClient; + assert(() { + if (debugNetworkImageHttpClientProvider != null) { + client = debugNetworkImageHttpClientProvider!(); + } + return true; + }()); + return client; + } + /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. @override final String? imageCacheName; @@ -160,51 +178,6 @@ class ExtendedNetworkImageProvider } } - @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) { - return false; - } - return other is ExtendedNetworkImageProvider && - url == other.url && - scale == other.scale && - cacheRawData == other.cacheRawData && - timeLimit == other.timeLimit && - cancelToken == other.cancelToken && - timeRetry == other.timeRetry && - cache == other.cache && - cacheKey == other.cacheKey && - headers == other.headers && - retries == other.retries && - imageCacheName == other.imageCacheName; - } - - @override - int get hashCode => hashValues( - url, - scale, - cacheRawData, - timeLimit, - cancelToken, - timeRetry, - cache, - cacheKey, - headers, - retries, - imageCacheName, - ); - - @override - String toString() => '$runtimeType("$url", scale: $scale)'; - - /// Get network image data from cached - @override - Future getNetworkImageData({ - StreamController? chunkEvents, - }) async { - return await _(url, chunkEvents); - } - Future _getCacheDir() async { final Directory dir = Directory( join((await getTemporaryDirectory()).path, cacheImageFolderName)); @@ -355,6 +328,7 @@ class ExtendedNetworkImageProvider return await _rw(resp, rawFile, tempFile, chunkEvents: chunkEvents); } else { // request error. + resp.listen(null); return null; } } else { @@ -448,23 +422,50 @@ class ExtendedNetworkImageProvider ); } - // Do not access this field directly; use [_httpClient] instead. - // We set `autoUncompress` to false to ensure that we can trust the value of - // the `Content-Length` HTTP header. We automatically uncompress the content - // in our call to [consolidateHttpClientResponseBytes]. - static final HttpClient _sharedHttpClient = HttpClient() - ..autoUncompress = false; + /// Get network image data from cached + @override + Future getNetworkImageData({ + StreamController? chunkEvents, + }) async { + return await _(url, chunkEvents); + } - static HttpClient get httpClient { - HttpClient client = _sharedHttpClient; - assert(() { - if (debugNetworkImageHttpClientProvider != null) { - client = debugNetworkImageHttpClientProvider!(); - } - return true; - }()); - return client; + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is ExtendedNetworkImageProvider && + url == other.url && + scale == other.scale && + cacheRawData == other.cacheRawData && + timeLimit == other.timeLimit && + cancelToken == other.cancelToken && + timeRetry == other.timeRetry && + cache == other.cache && + cacheKey == other.cacheKey && + headers == other.headers && + retries == other.retries && + imageCacheName == other.imageCacheName; } + + @override + int get hashCode => hashValues( + url, + scale, + cacheRawData, + timeLimit, + cancelToken, + timeRetry, + cache, + cacheKey, + headers, + retries, + imageCacheName, + ); + + @override + String toString() => '$runtimeType("$url", scale: $scale)'; } typedef _BeforeRequest = void Function(HttpClientRequest request); From d8c8e4e4a46767788f28cb06bd760c04e2d587e7 Mon Sep 17 00:00:00 2001 From: iota9star Date: Sun, 25 Jul 2021 01:39:06 +0800 Subject: [PATCH 7/9] :sparkles: New impl. --- lib/src/_network_image_io.dart | 85 ++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index e45cd1c..636725f 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -128,6 +128,7 @@ class ExtendedNetworkImageProvider decode, ), scale: key.scale, + debugLabel: key.url, chunkEvents: chunkEvents.stream, informationCollector: () { return [ @@ -353,6 +354,8 @@ class ExtendedNetworkImageProvider : await _rw(resp, rawFile, tempFile, chunkEvents: chunkEvents); } + late StreamSubscription> _subscription; + Future _rw( HttpClientResponse response, File rawFile, @@ -361,22 +364,74 @@ class ExtendedNetworkImageProvider int loadedLength = 0, FileMode fileMode = FileMode.write, }) async { - final Uint8List bytes = await consolidateHttpClientResponseBytes( - response, - onBytesReceived: chunkEvents != null - ? (int cumulative, int? total) { - chunkEvents.add(ImageChunkEvent( - cumulativeBytesLoaded: cumulative + loadedLength, - expectedTotalBytes: total == null ? null : total + loadedLength, + final Completer completer = Completer(); + if (tempFile != null) { + int received = loadedLength; + final bool compressed = response.compressionState == + HttpClientResponseCompressionState.compressed; + final int? total = compressed || response.contentLength < 0 + ? null + : response.contentLength; + final RandomAccessFile raf = await tempFile.open(mode: fileMode); + _subscription = response.listen( + (List bytes) { + _subscription.pause(); + raf.setPositionSync(received); + raf.writeFrom(bytes).then((RandomAccessFile _raf) { + received += bytes.length; + chunkEvents?.add(ImageChunkEvent( + cumulativeBytesLoaded: received, + expectedTotalBytes: total, + )); + _subscription.resume(); + }).catchError((dynamic err, StackTrace stackTrace) async { + await _subscription.cancel(); + }); + }, + onDone: () async { + try { + await raf.close(); + Uint8List buffer = await tempFile.readAsBytes(); + if (compressed) { + final List convert = gzip.decoder.convert(buffer); + buffer = Uint8List.fromList(convert); + await tempFile.writeAsBytes(convert); + chunkEvents?.add(ImageChunkEvent( + cumulativeBytesLoaded: buffer.length, + expectedTotalBytes: buffer.length, )); } - : null, - ); - if (tempFile != null) { - await tempFile.writeAsBytes(bytes, mode: fileMode); - await tempFile.rename(rawFile.path); + await tempFile.rename(rawFile.path); + completer.complete(buffer); + } catch (e) { + completer.completeError(e); + } + }, + onError: (Object err, StackTrace stackTrace) async { + try { + await raf.close(); + } finally { + completer.completeError(err, stackTrace); + } + }, + cancelOnError: true, + ); + } else { + final Uint8List bytes = await consolidateHttpClientResponseBytes( + response, + onBytesReceived: chunkEvents != null + ? (int cumulative, int? total) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: cumulative + loadedLength, + expectedTotalBytes: + total == null ? null : total + loadedLength, + )); + } + : null, + ); + completer.complete(bytes); } - return bytes; + return completer.future; } Future _createNewRequest( @@ -446,7 +501,8 @@ class ExtendedNetworkImageProvider cacheKey == other.cacheKey && headers == other.headers && retries == other.retries && - imageCacheName == other.imageCacheName; + imageCacheName == other.imageCacheName && + cacheMaxAge == other.cacheMaxAge; } @override @@ -462,6 +518,7 @@ class ExtendedNetworkImageProvider headers, retries, imageCacheName, + cacheMaxAge, ); @override From 255162f623cc4fdfc843dbd4bca03ac7294269bd Mon Sep 17 00:00:00 2001 From: iota9star Date: Sun, 25 Jul 2021 01:44:45 +0800 Subject: [PATCH 8/9] :bug: Fixed no cache progress value. --- lib/src/_network_image_io.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index 636725f..fe8b9cb 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -422,9 +422,8 @@ class ExtendedNetworkImageProvider onBytesReceived: chunkEvents != null ? (int cumulative, int? total) { chunkEvents.add(ImageChunkEvent( - cumulativeBytesLoaded: cumulative + loadedLength, - expectedTotalBytes: - total == null ? null : total + loadedLength, + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, )); } : null, From 97eca127059c088198eaa618c3a25bd0194a82e3 Mon Sep 17 00:00:00 2001 From: iota9star Date: Sun, 25 Jul 2021 17:09:00 +0800 Subject: [PATCH 9/9] :art: Restore `ExtendedNetworkImageProvider`, add new `ExtendedHttpCacheImageProvider`. --- lib/src/_network_image_io.dart | 370 ++++++++++++++++++++++++++++++++- 1 file changed, 367 insertions(+), 3 deletions(-) diff --git a/lib/src/_network_image_io.dart b/lib/src/_network_image_io.dart index fe8b9cb..526102b 100644 --- a/lib/src/_network_image_io.dart +++ b/lib/src/_network_image_io.dart @@ -36,6 +36,370 @@ class ExtendedNetworkImageProvider this.cacheMaxAge, }); + /// The name of [ImageCache], you can define custom [ImageCache] to store this provider. + @override + final String? imageCacheName; + + /// Whether cache raw data if you need to get raw data directly. + /// For example, we need raw image data to edit, + /// but [ui.Image.toByteData()] is very slow. So we cache the image + /// data here. + @override + final bool cacheRawData; + + /// The time limit to request image + @override + final Duration? timeLimit; + + /// The time to retry to request + @override + final int retries; + + /// The time duration to retry to request + @override + final Duration timeRetry; + + /// Whether cache image to local + @override + final bool cache; + + /// The URL from which the image will be fetched. + @override + final String url; + + /// The scale to place in the [ImageInfo] object of the image. + @override + final double scale; + + /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network. + @override + final Map? headers; + + /// The token to cancel network request + @override + final CancellationToken? cancelToken; + + /// Custom cache key + @override + final String? cacheKey; + + /// print error + @override + final bool printError; + + /// The max duration to cache image. + /// After this time the cache is expired and the image is reloaded. + @override + final Duration? cacheMaxAge; + + @override + ImageStreamCompleter load( + image_provider.ExtendedNetworkImageProvider key, DecoderCallback decode) { + // Ownership of this controller is handed off to [_loadAsync]; it is that + // method's responsibility to close the controller's stream when the image + // has been loaded or an error is thrown. + final StreamController chunkEvents = + StreamController(); + + return MultiFrameImageStreamCompleter( + codec: _loadAsync( + key as ExtendedNetworkImageProvider, + chunkEvents, + decode, + ), + scale: key.scale, + debugLabel: key.url, + chunkEvents: chunkEvents.stream, + informationCollector: () { + return [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty( + 'Image key', key), + ]; + }, + ); + } + + @override + Future obtainKey( + ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + Future _loadAsync( + ExtendedNetworkImageProvider key, + StreamController chunkEvents, + DecoderCallback decode, + ) async { + assert(key == this); + final String md5Key = cacheKey ?? keyToMd5(key.url); + ui.Codec? result; + if (cache) { + try { + final Uint8List? data = await _loadCache( + key, + chunkEvents, + md5Key, + ); + if (data != null) { + result = await instantiateImageCodec(data, decode); + } + } catch (e) { + if (printError) { + print(e); + } + } + } + + if (result == null) { + try { + final Uint8List? data = await _loadNetwork( + key, + chunkEvents, + ); + if (data != null) { + result = await instantiateImageCodec(data, decode); + } + } catch (e) { + if (printError) { + print(e); + } + } + } + + //Failed to load + if (result == null) { + //result = await ui.instantiateImageCodec(kTransparentImage); + return Future.error(StateError('Failed to load $url.')); + } + + return result; + } + + /// Get the image from cache folder. + Future _loadCache( + ExtendedNetworkImageProvider key, + StreamController? chunkEvents, + String md5Key, + ) async { + final Directory _cacheImagesDirectory = Directory( + join((await getTemporaryDirectory()).path, cacheImageFolderName)); + Uint8List? data; + // exist, try to find cache image file + if (_cacheImagesDirectory.existsSync()) { + final File cacheFlie = File(join(_cacheImagesDirectory.path, md5Key)); + if (cacheFlie.existsSync()) { + if (key.cacheMaxAge != null) { + final DateTime now = DateTime.now(); + final FileStat fs = cacheFlie.statSync(); + if (now.subtract(key.cacheMaxAge!).isAfter(fs.changed)) { + cacheFlie.deleteSync(recursive: true); + } else { + data = await cacheFlie.readAsBytes(); + } + } else { + data = await cacheFlie.readAsBytes(); + } + } + } + // create folder + else { + await _cacheImagesDirectory.create(); + } + // load from network + if (data == null) { + data = await _loadNetwork( + key, + chunkEvents, + ); + if (data != null) { + // cache image file + await File(join(_cacheImagesDirectory.path, md5Key)).writeAsBytes(data); + } + } + + return data; + } + + /// Get the image from network. + Future _loadNetwork( + ExtendedNetworkImageProvider key, + StreamController? chunkEvents, + ) async { + try { + final Uri resolved = Uri.base.resolve(key.url); + final HttpClientResponse? response = await _tryGetResponse(resolved); + if (response == null || response.statusCode != HttpStatus.ok) { + return null; + } + + final Uint8List bytes = await consolidateHttpClientResponseBytes( + response, + onBytesReceived: chunkEvents != null + ? (int cumulative, int? total) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, + )); + } + : null, + ); + if (bytes.lengthInBytes == 0) { + return Future.error( + StateError('NetworkImage is an empty file: $resolved')); + } + + return bytes; + } on OperationCanceledError catch (_) { + if (printError) { + print('User cancel request $url.'); + } + return Future.error(StateError('User cancel request $url.')); + } catch (e) { + if (printError) { + print(e); + } + } finally { + await chunkEvents?.close(); + } + return null; + } + + Future _getResponse(Uri resolved) async { + final HttpClientRequest request = await httpClient.getUrl(resolved); + headers?.forEach((String name, String value) { + request.headers.add(name, value); + }); + final HttpClientResponse response = await request.close(); + if (timeLimit != null) { + response.timeout( + timeLimit!, + ); + } + return response; + } + + // Http get with cancel, delay try again + Future _tryGetResponse( + Uri resolved, + ) async { + cancelToken?.throwIfCancellationRequested(); + return await RetryHelper.tryRun( + () { + return CancellationTokenSource.register( + cancelToken, + _getResponse(resolved), + ); + }, + cancelToken: cancelToken, + timeRetry: timeRetry, + retries: retries, + ); + } + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is ExtendedNetworkImageProvider && + url == other.url && + scale == other.scale && + cacheRawData == other.cacheRawData && + timeLimit == other.timeLimit && + cancelToken == other.cancelToken && + timeRetry == other.timeRetry && + cache == other.cache && + cacheKey == other.cacheKey && + headers == other.headers && + retries == other.retries && + imageCacheName == other.imageCacheName && + cacheMaxAge == other.cacheMaxAge; + } + + @override + int get hashCode => hashValues( + url, + scale, + cacheRawData, + timeLimit, + cancelToken, + timeRetry, + cache, + cacheKey, + headers, + retries, + imageCacheName, + cacheMaxAge, + ); + + @override + String toString() => '$runtimeType("$url", scale: $scale)'; + + @override + + /// Get network image data from cached + Future getNetworkImageData({ + StreamController? chunkEvents, + }) async { + final String uId = cacheKey ?? keyToMd5(url); + + if (cache) { + return await _loadCache( + this, + chunkEvents, + uId, + ); + } + + return await _loadNetwork( + this, + chunkEvents, + ); + } + + // Do not access this field directly; use [_httpClient] instead. + // We set `autoUncompress` to false to ensure that we can trust the value of + // the `Content-Length` HTTP header. We automatically uncompress the content + // in our call to [consolidateHttpClientResponseBytes]. + static final HttpClient _sharedHttpClient = HttpClient() + ..autoUncompress = false; + + static HttpClient get httpClient { + HttpClient client = _sharedHttpClient; + assert(() { + if (debugNetworkImageHttpClientProvider != null) { + client = debugNetworkImageHttpClientProvider!(); + } + return true; + }()); + return client; + } +} + +class ExtendedHttpCacheImageProvider + extends ImageProvider + with ExtendedImageProvider + implements image_provider.ExtendedNetworkImageProvider { + /// Creates an object that fetches the image at the given URL. + /// + /// The arguments must not be null. + ExtendedHttpCacheImageProvider( + this.url, { + this.scale = 1.0, + this.headers, + this.cache = false, + this.retries = 3, + this.timeLimit, + this.timeRetry = const Duration(milliseconds: 100), + this.cacheKey, + this.printError = true, + this.cacheRawData = false, + this.cancelToken, + this.imageCacheName, + this.cacheMaxAge, + }); + static final Map _lockCache = {}; // Do not access this field directly; use [_httpClient] instead. @@ -141,9 +505,9 @@ class ExtendedNetworkImageProvider } @override - Future obtainKey( + Future obtainKey( ImageConfiguration configuration) { - return SynchronousFuture(this); + return SynchronousFuture(this); } @override @@ -154,7 +518,7 @@ class ExtendedNetworkImageProvider }) async { rawImageDataMap.remove(this); cache ??= this.imageCache; - final ExtendedNetworkImageProvider key = await obtainKey(configuration); + final ExtendedHttpCacheImageProvider key = await obtainKey(configuration); _lockCache.remove(key.url); return cache.evict(key, includeLive: includeLive); }