diff --git a/lib/src/http_manager_dio.dart b/lib/src/http_manager_dio.dart new file mode 100644 index 0000000..9392b3d --- /dev/null +++ b/lib/src/http_manager_dio.dart @@ -0,0 +1,256 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:dio_http_cache/src/core/config.dart'; +import 'package:dio_http_cache/src/core/manager.dart'; +import 'package:dio_http_cache/src/core/obj.dart'; + +const DIO_CACHE_KEY_TRY_CACHE = "dio_cache_try_cache"; +const DIO_CACHE_KEY_MAX_AGE = "dio_cache_max_age"; +const DIO_CACHE_KEY_MAX_STALE = "dio_cache_max_stale"; +const DIO_CACHE_KEY_PRIMARY_KEY = "dio_cache_primary_key"; +const DIO_CACHE_KEY_SUB_KEY = "dio_cache_sub_key"; +const DIO_CACHE_KEY_FORCE_REFRESH = "dio_cache_force_refresh"; + +typedef _ParseHeadCallback = void Function( + Duration _maxAge, Duration _maxStale); + +class HttpCacheManager { + CacheManager _manager; + InterceptorsWrapper _interceptor; + String _baseUrl; + + HttpCacheManager(CacheConfig config) { + _manager = CacheManager(config); + _baseUrl = config.baseUrl; + } + + /// interceptor for http cache. + get interceptor { + if (null == _interceptor) { + _interceptor = InterceptorsWrapper( + onRequest: _onRequest, onResponse: _onResponse, onError: _onError); + } + return _interceptor; + } + + _onRequest(RequestOptions options) async { + if ((options.extra[DIO_CACHE_KEY_TRY_CACHE] ?? false) != true) { + return options; + } + if (true == options.extra[DIO_CACHE_KEY_FORCE_REFRESH]) { + return options; + } + var responseDataFromCache = await _pullFromCacheBeforeMaxAge(options); + if (null != responseDataFromCache) { + final headers = jsonDecode(utf8.decode(responseDataFromCache.headers)); + options.headers[HttpHeaders.ifNoneMatchHeader] = headers[HttpHeaders.etagHeader]?.join(","); + } + return options; + + } + + _onResponse(Response response) async { + if ((response.request.extra[DIO_CACHE_KEY_TRY_CACHE] ?? false) == false) { + return response; + } + if (response.statusCode >= 200 && response.statusCode < 300) { + if (null != response.headers[HttpHeaders.etagHeader]) { + await _pushToCache(response); + } + return response; + } + if (response.statusCode == 304) { + var responseDataFromCache = await _pullFromCacheBeforeMaxAge(response.request); + return _buildResponse(responseDataFromCache, response.statusCode, response.request); + } + return response; + } + + _onError(DioError e) async { + if ((e.request.extra[DIO_CACHE_KEY_TRY_CACHE] ?? false) == true) { + var responseDataFromCache = await _pullFromCacheBeforeMaxStale(e.request); + if (null != responseDataFromCache) + return _buildResponse(responseDataFromCache, + responseDataFromCache?.statusCode, e.request); + } + return e; + } + + Response _buildResponse( + CacheObj obj, int statusCode, RequestOptions options) { + Headers headers; + if (null != obj.headers) { + headers = Headers.fromMap((Map>.from( + jsonDecode(utf8.decode(obj.headers)))) + .map((k, v) => MapEntry(k, List.from(v)))); + } + if (null == headers) { + headers = Headers(); + options.headers.forEach((k, v) => headers.add(k, v ?? "")); + } + dynamic data = obj.content; + if (options.responseType != ResponseType.bytes) { + data = jsonDecode(utf8.decode(data)); + } + return Response( + data: data, + headers: headers, + extra: options.extra..remove(DIO_CACHE_KEY_TRY_CACHE), + statusCode: statusCode ?? 200); + } + + Future _pullFromCacheBeforeMaxAge(RequestOptions options) { + return _manager?.pullFromCacheBeforeMaxAge( + _getPrimaryKeyFromOptions(options), + subKey: _getSubKeyFromOptions(options)); + } + + Future _pullFromCacheBeforeMaxStale(RequestOptions options) { + return _manager?.pullFromCacheBeforeMaxStale( + _getPrimaryKeyFromOptions(options), + subKey: _getSubKeyFromOptions(options)); + } + + Future _pushToCache(Response response) { + RequestOptions options = response.request; + Duration maxAge = options.extra[DIO_CACHE_KEY_MAX_AGE]; + Duration maxStale = options.extra[DIO_CACHE_KEY_MAX_STALE]; + if (null == maxAge) { + _tryParseHead(response, maxStale, (_maxAge, _maxStale) { + maxAge = _maxAge; + maxStale = _maxStale; + }); + } + if (null == maxAge) return Future.value(false); + + List data; + if (options.responseType == ResponseType.bytes) { + data = response.data; + } else { + data = utf8.encode(jsonEncode(response.data)); + } + var obj = CacheObj(_getPrimaryKeyFromOptions(options), data, + subKey: _getSubKeyFromOptions(options), + maxAge: maxAge, + maxStale: maxStale, + statusCode: response.statusCode, + headers: utf8.encode(jsonEncode(response.headers.map))); + return _manager?.pushToCache(obj); + } + + // try to get maxAge and maxStale from http headers + void _tryParseHead( + Response response, Duration maxStale, _ParseHeadCallback callback) { + Duration _maxAge; + var cacheControl = response.headers.value(HttpHeaders.cacheControlHeader); + if (null != cacheControl) { + // try to get maxAge and maxStale from cacheControl + var parameters; + try { + parameters = HeaderValue.parse(cacheControl, + parameterSeparator: ",", valueSeparator: "=") + .parameters; + } catch (e) { + print(e); + } + _maxAge = _tryGetDurationFromMap(parameters, "s-maxage"); + if (null == _maxAge) { + _maxAge = _tryGetDurationFromMap(parameters, "max-age"); + } + // if maxStale has valued, don't get max-stale anymore. + if (null == maxStale) { + maxStale = _tryGetDurationFromMap(parameters, "max-stale"); + } + } else { + // try to get maxAge from expires + var expires = response.headers.value(HttpHeaders.expiresHeader); + if (null != expires && expires.length > 4) { + DateTime endTime; + try { + endTime = HttpDate.parse(expires).toLocal(); + } catch (e) { + print(e); + } + if (null != endTime && endTime.compareTo(DateTime.now()) >= 0) { + _maxAge = endTime.difference(DateTime.now()); + } + } + } + callback(_maxAge, maxStale); + } + + Duration _tryGetDurationFromMap(Map parameters, String key) { + if (null != parameters && parameters.containsKey(key)) { + var value = int.tryParse(parameters[key]); + if (null != value && value >= 0) { + return Duration(seconds: value); + } + } + return null; + } + + String _getPrimaryKeyFromOptions(RequestOptions options) { + return options.extra.containsKey(DIO_CACHE_KEY_PRIMARY_KEY) + ? options.extra[DIO_CACHE_KEY_PRIMARY_KEY] + : _getPrimaryKeyFromUri(options.uri); + } + + String _getSubKeyFromOptions(RequestOptions options) { + return options.extra.containsKey(DIO_CACHE_KEY_SUB_KEY) + ? options.extra[DIO_CACHE_KEY_SUB_KEY] + : _getSubKeyFromUri(options.uri, data: options.data); + } + + String _getPrimaryKeyFromUri(Uri uri) => "${uri?.host}${uri?.path}"; + + String _getSubKeyFromUri(Uri uri, {dynamic data}) => + "${data?.toString()}_${uri?.query}"; + + /// delete local cache by primaryKey and optional subKey + Future delete(String primaryKey, {String subKey}) => + _manager?.delete(primaryKey, subKey: subKey); + + /// no matter what subKey is, delete local cache if primary matched. + Future deleteByPrimaryKeyWithUri(Uri uri) => + delete(_getPrimaryKeyFromUri(uri)); + + Future deleteByPrimaryKey(String path) => + deleteByPrimaryKeyWithUri(_getUriByPath(_baseUrl, path)); + + /// delete local cache when both primaryKey and subKey matched. + Future deleteByPrimaryKeyAndSubKeyWithUri(Uri uri, + {String subKey, dynamic data}) => + delete(_getPrimaryKeyFromUri(uri), + subKey: subKey ?? _getSubKeyFromUri(uri, data: data)); + + Future deleteByPrimaryKeyAndSubKey(String path, + {Map queryParameters, + String subKey, + dynamic data}) => + deleteByPrimaryKeyAndSubKeyWithUri( + _getUriByPath(_baseUrl, path, + data: data, queryParameters: queryParameters), + subKey: subKey, + data: data); + + /// clear all expired cache. + Future clearExpired() => _manager?.clearExpired(); + + /// empty local cache. + Future clearAll() => _manager?.clearAll(); + + Uri _getUriByPath(String baseUrl, String path, + {dynamic data, Map queryParameters}) { + if (!path.startsWith(RegExp(r"https?:"))) { + assert(null != baseUrl && baseUrl.length > 0); + } + return RequestOptions( + baseUrl: baseUrl, + path: path, + data: data, + queryParameters: queryParameters) + .uri; + } +}