platform/lib/src/cache.dart

183 lines
5.5 KiB
Dart
Raw Normal View History

2017-02-27 00:19:34 +00:00
import 'dart:async';
import 'dart:convert';
2017-09-23 21:57:54 +00:00
import 'dart:typed_data';
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:async/async.dart';
import 'package:file/file.dart';
2017-02-27 00:19:34 +00:00
import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'virtual_directory.dart';
final DateFormat _fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
String formatDateForHttp(DateTime dt) => _fmt.format(dt.toUtc()) + ' GMT';
2017-09-23 21:57:54 +00:00
/// Generates a weak ETag from the given buffer.
String weakEtag(List<int> buf) {
return 'W/${buf.length}' + BASE64URL.encode(buf);
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,
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-09-23 21:57:54 +00:00
callback: callback);
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) {
2017-09-23 21:57:54 +00:00
var etags = req.headers['if-none-match'];
2017-02-27 00:19:34 +00:00
if (etags?.isNotEmpty == true) {
bool hasBeenModified = false;
for (var etag in etags) {
if (etag == '*')
hasBeenModified = true;
else {
hasBeenModified = _etags.containsKey(file.absolute.path) &&
_etags[file.absolute.path] == etag;
}
}
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-23 21:57:54 +00:00
var queue = new StreamQueue(file.openRead());
return new Future<bool>(() async {
var buf = new Uint8List(50), hanging = <int>[];
int added = 0;
while (added < 50) {
var deficit = 50 - added;
var next = await queue.next;
for (int i = 0; i < deficit; i++) {
buf[added + i] = next[i];
}
if (next.length > deficit) {
hanging.addAll(next.skip(deficit));
}
}
var etag = _etags[file.absolute.path] = weakEtag(buf);
res.statusCode = 200;
2017-02-27 00:19:34 +00:00
res.headers
2017-09-23 21:57:54 +00:00
..['ETag'] = etag
..['content-type'] =
2017-08-16 00:01:31 +00:00
lookupMimeType(file.path) ?? 'application/octet-stream';
2017-06-16 02:05:06 +00:00
setCachedHeaders(stat.modified, req, res);
2017-02-27 00:19:34 +00:00
2017-09-23 21:57:54 +00:00
res.add(buf);
res.add(hanging);
2017-02-27 00:19:34 +00:00
2017-09-23 21:57:54 +00:00
return queue.rest.pipe(res).then((_) => 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}'
..['last-modified'] = formatDateForHttp(modified);
2017-08-16 00:01:31 +00:00
if (maxAge != null) {
var expiry = new DateTime.now().add(new Duration(seconds: maxAge ?? 0));
2017-09-23 21:57:54 +00:00
res.headers['expires'] = formatDateForHttp(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 }