platform/packages/static/lib/src/cache.dart

198 lines
6.1 KiB
Dart
Raw Normal View History

2017-02-27 00:19:34 +00:00
import 'dart:async';
2018-10-21 00:24:44 +00:00
import 'dart:io' show HttpDate;
2021-05-15 13:28:26 +00:00
import 'package:angel3_framework/angel3_framework.dart';
2017-09-23 21:57:54 +00:00
import 'package:file/file.dart';
2021-07-04 03:22:19 +00:00
import 'package:logging/logging.dart';
2017-02-27 00:19:34 +00:00
import 'virtual_directory.dart';
/// Returns a string representation of the given [CacheAccessLevel].
String accessLevelToString(CacheAccessLevel accessLevel) {
switch (accessLevel) {
case CacheAccessLevel.PRIVATE:
return 'private';
case CacheAccessLevel.PUBLIC:
return 'public';
default:
2019-05-02 23:29:09 +00:00
throw ArgumentError('Unrecognized cache access level: $accessLevel');
2017-02-27 00:19:34 +00:00
}
}
2017-09-23 21:57:54 +00:00
/// A `VirtualDirectory` that also sets `Cache-Control` headers.
2017-02-27 00:19:34 +00:00
class CachingVirtualDirectory extends VirtualDirectory {
2021-07-04 03:22:19 +00:00
final _log = Logger('CachingVirtualDirectory');
2017-02-27 00:19:34 +00:00
final Map<String, String> _etags = {};
/// Either `PUBLIC` or `PRIVATE`.
final CacheAccessLevel accessLevel;
/// If `true`, responses will always have `private, max-age=0` as their `Cache-Control` header.
final bool noCache;
/// If `true` (default), `Cache-Control` headers will only be set if the application is in production mode.
final bool onlyInProduction;
/// If `true` (default), ETags will be computed and sent along with responses.
final bool useEtags;
/// The `max-age` for `Cache-Control`.
2017-08-16 00:01:31 +00:00
///
/// Set this to `null` to leave no `Expires` header on responses.
2017-02-27 00:19:34 +00:00
final int maxAge;
2017-09-23 21:57:54 +00:00
CachingVirtualDirectory(Angel app, FileSystem fileSystem,
2019-05-02 23:29:09 +00:00
{this.accessLevel = CacheAccessLevel.PUBLIC,
2021-05-01 03:53:04 +00:00
Directory? source,
2021-07-04 03:22:19 +00:00
bool debug = false,
Iterable<String> indexFileNames = const ['index.html'],
2019-05-02 23:29:09 +00:00
this.maxAge = 0,
this.noCache = false,
this.onlyInProduction = false,
this.useEtags = true,
2021-07-04 03:22:19 +00:00
bool allowDirectoryListing = false,
2019-05-02 23:29:09 +00:00
bool useBuffer = false,
2021-07-04 03:22:19 +00:00
String publicPath = '/',
2021-05-01 03:53:04 +00:00
Function(File file, RequestContext req, ResponseContext res)? callback})
2017-09-23 21:57:54 +00:00
: super(app, fileSystem,
2017-02-27 00:19:34 +00:00
source: source,
2021-07-04 03:22:19 +00:00
indexFileNames: indexFileNames,
publicPath: publicPath,
2017-11-18 06:12:59 +00:00
callback: callback,
allowDirectoryListing: allowDirectoryListing,
2018-08-28 14:58:28 +00:00
useBuffer: useBuffer);
2017-02-27 00:19:34 +00:00
@override
Future<bool> serveFile(
File file, FileStat stat, RequestContext req, ResponseContext res) {
2018-11-14 05:43:47 +00:00
res.headers['accept-ranges'] = 'bytes';
2021-07-04 03:22:19 +00:00
if (onlyInProduction == true && req.app?.environment.isProduction != true) {
2017-02-27 00:19:34 +00:00
return super.serveFile(file, stat, req, res);
}
2021-07-04 03:22:19 +00:00
if (req.headers == null) {
_log.severe('Missing headers in the RequestContext');
throw ArgumentError('Missing headers in the RequestContext');
}
var reqHeaders = req.headers!;
2021-05-01 02:48:36 +00:00
var shouldNotCache = noCache == true;
2017-06-16 02:20:58 +00:00
if (!shouldNotCache) {
2021-07-04 03:22:19 +00:00
shouldNotCache = reqHeaders.value('cache-control') == 'no-cache' ||
reqHeaders.value('pragma') == 'no-cache';
2017-06-16 02:20:58 +00:00
}
if (shouldNotCache) {
2017-09-23 21:57:54 +00:00
res.headers['cache-control'] = 'private, max-age=0, no-cache';
2017-02-27 00:19:34 +00:00
return super.serveFile(file, stat, req, res);
} else {
2021-07-04 03:22:19 +00:00
var ifModified = reqHeaders.ifModifiedSince;
2021-05-01 02:48:36 +00:00
var ifRange = false;
2018-11-14 05:43:47 +00:00
try {
2021-07-04 03:22:19 +00:00
if (reqHeaders.value('if-range') != null) {
ifModified = HttpDate.parse(reqHeaders.value('if-range')!);
ifRange = true;
}
2018-11-14 05:43:47 +00:00
} catch (_) {
// Fail silently...
}
if (ifModified != null) {
try {
var ifModifiedSince = ifModified;
if (ifModifiedSince.compareTo(stat.modified) >= 0) {
res.statusCode = 304;
setCachedHeaders(stat.modified, req, res);
2019-06-06 14:33:40 +00:00
if (useEtags && _etags.containsKey(file.absolute.path)) {
2021-07-04 03:22:19 +00:00
if (_etags[file.absolute.path] != null) {
res.headers['ETag'] = _etags[file.absolute.path]!;
}
2019-06-06 14:33:40 +00:00
}
2018-11-14 05:43:47 +00:00
if (ifRange) {
// Send the 206 like normal
res.statusCode = 206;
return super.serveFile(file, stat, req, res);
}
2019-05-02 23:29:09 +00:00
return Future.value(false);
2018-11-14 05:43:47 +00:00
} else if (ifRange) {
2019-01-27 22:14:54 +00:00
return super.serveFile(file, stat, req, res);
2018-11-14 05:43:47 +00:00
}
} catch (_) {
2021-07-04 03:22:19 +00:00
_log.severe(
'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.');
2019-05-02 23:29:09 +00:00
throw AngelHttpException.badRequest(
2018-11-14 05:43:47 +00:00
message:
'Invalid date for ${ifRange ? 'if-range' : 'if-not-modified-since'} header.');
}
}
// If-modified didn't work; try etags
2017-02-27 00:19:34 +00:00
if (useEtags == true) {
2021-07-04 03:22:19 +00:00
var etagsToMatchAgainst = reqHeaders['if-none-match'] ?? [];
2018-11-14 05:43:47 +00:00
ifRange = false;
2021-07-04 03:22:19 +00:00
if (etagsToMatchAgainst.isEmpty) {
etagsToMatchAgainst = reqHeaders['if-range'] ?? [];
ifRange = etagsToMatchAgainst.isNotEmpty;
2018-11-14 05:43:47 +00:00
}
2017-02-27 00:19:34 +00:00
2021-07-04 03:22:19 +00:00
if (etagsToMatchAgainst.isNotEmpty) {
2021-05-01 02:48:36 +00:00
var hasBeenModified = false;
2017-02-27 00:19:34 +00:00
2021-07-04 03:22:19 +00:00
for (var etag in etagsToMatchAgainst) {
2019-06-06 14:33:40 +00:00
if (etag == '*') {
2017-02-27 00:19:34 +00:00
hasBeenModified = true;
2019-06-06 14:33:40 +00:00
} else {
2018-11-13 21:25:17 +00:00
hasBeenModified = !_etags.containsKey(file.absolute.path) ||
_etags[file.absolute.path] != etag;
2017-02-27 00:19:34 +00:00
}
}
2018-11-14 05:43:47 +00:00
if (!ifRange) {
if (!hasBeenModified) {
res.statusCode = 304;
setCachedHeaders(stat.modified, req, res);
2019-05-02 23:29:09 +00:00
return Future.value(false);
2018-11-14 05:43:47 +00:00
}
} else {
2019-01-27 22:14:54 +00:00
return super.serveFile(file, stat, req, res);
2017-02-27 00:19:34 +00:00
}
}
}
2019-01-27 22:14:54 +00:00
return file.lastModified().then((stamp) {
2018-11-14 05:43:47 +00:00
if (useEtags) {
2019-01-27 22:15:02 +00:00
res.headers['ETag'] = _etags[file.absolute.path] =
stamp.millisecondsSinceEpoch.toString();
2018-11-14 05:43:47 +00:00
}
2019-01-27 22:15:02 +00:00
2017-06-16 02:05:06 +00:00
setCachedHeaders(stat.modified, req, res);
2019-01-28 00:30:40 +00:00
return super.serveFile(file, stat, req, res);
2017-02-27 00:19:34 +00:00
});
}
}
void setCachedHeaders(
2017-06-16 02:05:06 +00:00
DateTime modified, RequestContext req, ResponseContext res) {
2021-05-01 03:53:04 +00:00
var privacy = accessLevelToString(accessLevel);
2017-02-27 00:19:34 +00:00
res.headers
2021-05-01 03:53:04 +00:00
..['cache-control'] = '$privacy, max-age=$maxAge'
2018-10-21 00:24:44 +00:00
..['last-modified'] = HttpDate.format(modified);
2017-08-16 00:01:31 +00:00
2021-05-01 03:53:04 +00:00
//if (maxAge != null) {
var expiry = DateTime.now().add(Duration(seconds: maxAge));
res.headers['expires'] = HttpDate.format(expiry);
//}
2017-06-16 02:05:06 +00:00
}
2017-02-27 00:19:34 +00:00
}
enum CacheAccessLevel { PUBLIC, PRIVATE }