-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathshelf_flutter_asset.dart
156 lines (125 loc) · 4.75 KB
/
shelf_flutter_asset.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import 'dart:io';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';
/// Application start time used for Last-Modified headers
final _appStartTime = DateTime.now();
/// The default resolver for MIME types.
final _defaultMimeTypeResolver = MimeTypeResolver();
/// Default cache duration (1 hour)
const _defaultMaxAge = 3600;
/// Creates a Shelf [Handler] that serves files from Flutter assets.
///
/// If requested resource does not exist and [defaultDocument] is specified,
/// request path is checked for a resource with that name.
/// If it exists, it is served.
///
/// Specify a custom [contentTypeResolver] to customize automatic content type
/// detection.
///
/// If your assets are not in the standard `assets` directory,
/// or you want to share only subtree of the assets path structure,
/// you may use [rootPath] argument to set the root directory for your handler.
///
/// Set [enableCaching] to true to add cache-control headers with [maxAge] seconds
/// (defaults to 1 hour).
///
Handler createAssetHandler(
{String? defaultDocument,
String rootPath = 'assets',
MimeTypeResolver? contentTypeResolver,
bool enableCaching = false,
int maxAge = _defaultMaxAge}) {
final mimeResolver = contentTypeResolver ?? _defaultMimeTypeResolver;
return (Request request) async {
final segments = [rootPath, ...request.url.pathSegments];
String key = p.joinAll(segments);
Uint8List? body;
body = await _loadResource(key);
if (body == null && defaultDocument != null) {
key = p.join(key, defaultDocument);
body = await _loadResource(key);
}
if (body == null) {
return Response.notFound('Not Found');
}
final contentType = mimeResolver.lookup(key);
// Use a fixed last-modified timestamp for static assets
// This is based on the application start time
final lastModified = _appStartTime;
final lastModifiedHeader = HttpDate.format(lastModified);
final headers = {
HttpHeaders.contentLengthHeader: '${body.length}',
if (contentType != null) HttpHeaders.contentTypeHeader: contentType,
HttpHeaders.lastModifiedHeader: lastModifiedHeader,
};
// Check if the resource has been modified since the client's version
final ifModifiedSince = request.headers[HttpHeaders.ifModifiedSinceHeader];
if (ifModifiedSince != null) {
try {
final clientDate = HttpDate.parse(ifModifiedSince);
// Compare dates, treating them as equal if they are within 1 second
// This helps with test reliability and HTTP date precision issues
final difference = lastModified.difference(clientDate).inSeconds.abs();
if (difference < 1) {
return Response.notModified(headers: headers);
}
} catch (_) {
// Invalid date format, ignore header
}
}
// Add cache control headers if enabled
if (enableCaching) {
headers[HttpHeaders.cacheControlHeader] = 'max-age=$maxAge, public';
}
// Handle range requests
if (request.headers.containsKey('range')) {
return _handleRangeRequest(request, body, headers);
}
return Response.ok((request.method == 'HEAD') ? null : body,
headers: headers);
};
}
Future<Uint8List?> _loadResource(String key) async {
try {
final byteData = await rootBundle.load(key);
return byteData.buffer.asUint8List();
} catch (_) {}
return null;
}
/// Handles HTTP range requests by parsing the Range header and returning
/// the appropriate partial content response.
Response _handleRangeRequest(
Request request, Uint8List body, Map<String, String> headers) {
final rangeHeader = request.headers['range']!;
final match = RegExp(r'bytes=(\d*)-(\d*)').firstMatch(rangeHeader);
if (match == null) {
return Response(416, headers: headers); // Range Not Satisfiable
}
final startStr = match.group(1);
final endStr = match.group(2);
int start = startStr!.isEmpty ? 0 : int.parse(startStr);
int end = endStr!.isEmpty ? body.length - 1 : int.parse(endStr);
// Validate the range
if (start >= body.length || end >= body.length || start > end) {
return Response(416, headers: {
...headers,
'content-range': 'bytes */${body.length}',
});
}
// Limit the length to the actual body size
end = min(end, body.length - 1);
final length = end - start + 1;
final rangeBody = body.sublist(start, end + 1);
final rangeHeaders = {
...headers,
HttpHeaders.contentLengthHeader: '$length',
'content-range': 'bytes $start-$end/${body.length}',
'accept-ranges': 'bytes',
};
return Response(206, // Partial Content
body: (request.method == 'HEAD') ? null : rangeBody,
headers: rangeHeaders);
}