2017-02-27 00:19:34 +00:00
|
|
|
import 'dart:async';
|
2018-10-21 00:30:04 +00:00
|
|
|
import 'dart:convert';
|
2018-10-21 00:24:44 +00:00
|
|
|
import 'dart:io' show HttpDate;
|
2017-02-27 00:19:34 +00:00
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2017-09-23 21:57:54 +00:00
|
|
|
import 'package:file/file.dart';
|
2017-02-27 00:19:34 +00:00
|
|
|
import 'virtual_directory.dart';
|
|
|
|
|
2017-09-23 21:57:54 +00:00
|
|
|
/// Generates a weak ETag from the given buffer.
|
|
|
|
String weakEtag(List<int> buf) {
|
2018-07-09 17:38:22 +00:00
|
|
|
return 'W/${buf.length}' + base64Url.encode(buf.take(50).toList());
|
2017-02-27 00:19:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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:
|
|
|
|
throw new ArgumentError('Unrecognized cache access level: $accessLevel');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
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,
|
2017-02-27 00:19:34 +00:00
|
|
|
{this.accessLevel: CacheAccessLevel.PUBLIC,
|
|
|
|
Directory source,
|
|
|
|
bool debug,
|
|
|
|
Iterable<String> indexFileNames,
|
|
|
|
this.maxAge: 0,
|
|
|
|
this.noCache: false,
|
|
|
|
this.onlyInProduction: false,
|
|
|
|
this.useEtags: true,
|
2017-11-18 06:12:59 +00:00
|
|
|
bool allowDirectoryListing,
|
2018-10-21 00:24:44 +00:00
|
|
|
bool useBuffer: false,
|
2017-02-27 00:19:34 +00:00
|
|
|
String publicPath,
|
2017-09-23 21:57:54 +00:00
|
|
|
callback(File file, RequestContext req, ResponseContext res)})
|
|
|
|
: super(app, fileSystem,
|
2017-02-27 00:19:34 +00:00
|
|
|
source: source,
|
|
|
|
indexFileNames: indexFileNames ?? ['index.html'],
|
|
|
|
publicPath: publicPath ?? '/',
|
2017-11-18 06:12:59 +00:00
|
|
|
callback: callback,
|
2018-07-09 17:38:22 +00:00
|
|
|
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) {
|
2017-08-16 00:01:31 +00:00
|
|
|
if (onlyInProduction == true && req.app.isProduction != true) {
|
2017-02-27 00:19:34 +00:00
|
|
|
return super.serveFile(file, stat, req, res);
|
|
|
|
}
|
|
|
|
|
2017-06-16 02:20:58 +00:00
|
|
|
bool shouldNotCache = noCache == true;
|
|
|
|
|
|
|
|
if (!shouldNotCache) {
|
2017-09-23 21:57:54 +00:00
|
|
|
shouldNotCache = req.headers.value('cache-control') == 'no-cache' ||
|
|
|
|
req.headers.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 {
|
|
|
|
if (useEtags == true) {
|
2018-11-13 21:25:17 +00:00
|
|
|
var etagsToMatchAgainst = req.headers['if-none-match'];
|
2017-02-27 00:19:34 +00:00
|
|
|
|
2018-11-13 21:25:17 +00:00
|
|
|
if (etagsToMatchAgainst?.isNotEmpty == true) {
|
2017-02-27 00:19:34 +00:00
|
|
|
bool hasBeenModified = false;
|
|
|
|
|
2018-11-13 21:25:17 +00:00
|
|
|
for (var etag in etagsToMatchAgainst) {
|
2017-02-27 00:19:34 +00:00
|
|
|
if (etag == '*')
|
|
|
|
hasBeenModified = true;
|
|
|
|
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-13 21:25:17 +00:00
|
|
|
if (!hasBeenModified) {
|
2017-09-23 21:57:54 +00:00
|
|
|
res.statusCode = 304;
|
2017-06-16 02:05:06 +00:00
|
|
|
setCachedHeaders(stat.modified, req, res);
|
2017-02-27 00:19:34 +00:00
|
|
|
return new Future.value(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-16 03:30:54 +00:00
|
|
|
if (req.headers.ifModifiedSince != null) {
|
2017-02-27 00:19:34 +00:00
|
|
|
try {
|
2017-03-27 12:59:02 +00:00
|
|
|
var ifModifiedSince = req.headers.ifModifiedSince;
|
2017-02-27 00:19:34 +00:00
|
|
|
|
2017-03-27 12:59:02 +00:00
|
|
|
if (ifModifiedSince.compareTo(stat.modified) >= 0) {
|
2017-09-23 21:57:54 +00:00
|
|
|
res.statusCode = 304;
|
2017-06-16 02:05:06 +00:00
|
|
|
setCachedHeaders(stat.modified, req, res);
|
2017-02-27 00:19:34 +00:00
|
|
|
|
|
|
|
if (_etags.containsKey(file.absolute.path))
|
2017-09-23 21:57:54 +00:00
|
|
|
res.headers['ETag'] = _etags[file.absolute.path];
|
2017-02-27 00:19:34 +00:00
|
|
|
|
|
|
|
return new Future.value(false);
|
|
|
|
}
|
|
|
|
} catch (_) {
|
|
|
|
throw new AngelHttpException.badRequest(
|
|
|
|
message: 'Invalid date for If-Modified-Since header.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-24 05:02:10 +00:00
|
|
|
return file.readAsBytes().then((buf) {
|
2017-09-23 21:57:54 +00:00
|
|
|
var etag = _etags[file.absolute.path] = weakEtag(buf);
|
2018-11-13 21:25:17 +00:00
|
|
|
//res.statusCode = 200;
|
2017-02-27 00:19:34 +00:00
|
|
|
res.headers
|
2017-09-23 21:57:54 +00:00
|
|
|
..['ETag'] = etag
|
2018-11-13 21:25:17 +00:00
|
|
|
..['content-type'] = res.app.mimeTypeResolver.lookup(file.path) ??
|
|
|
|
'application/octet-stream';
|
2017-06-16 02:05:06 +00:00
|
|
|
setCachedHeaders(stat.modified, req, res);
|
2017-09-23 21:57:54 +00:00
|
|
|
res.add(buf);
|
2017-09-24 05:02:10 +00:00
|
|
|
return false;
|
2017-02-27 00:19:34 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void setCachedHeaders(
|
2017-06-16 02:05:06 +00:00
|
|
|
DateTime modified, RequestContext req, ResponseContext res) {
|
2017-02-27 00:19:34 +00:00
|
|
|
var privacy = accessLevelToString(accessLevel ?? CacheAccessLevel.PUBLIC);
|
|
|
|
|
|
|
|
res.headers
|
2017-09-23 21:57:54 +00:00
|
|
|
..['cache-control'] = '$privacy, max-age=${maxAge ?? 0}'
|
2018-10-21 00:24:44 +00:00
|
|
|
..['last-modified'] = HttpDate.format(modified);
|
2017-08-16 00:01:31 +00:00
|
|
|
|
|
|
|
if (maxAge != null) {
|
|
|
|
var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0));
|
2018-10-21 00:24:44 +00:00
|
|
|
res.headers['expires'] = HttpDate.format(expiry);
|
2017-08-16 00:01:31 +00:00
|
|
|
}
|
2017-06-16 02:05:06 +00:00
|
|
|
}
|
2017-02-27 00:19:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
enum CacheAccessLevel { PUBLIC, PRIVATE }
|