Skip to content

Commit a798e78

Browse files
committed
Implement cache control support and range requests
1 parent a866cfd commit a798e78

11 files changed

+353
-37
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.1.0
2+
3+
* Added HTTP cache-control support
4+
* Added HTTP range request support for partial content
5+
16
## 0.0.4
27

38
* Update library metadata

README.md

+67
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77

88
A simple handler for the Shelf ecosystem to serve files from Flutter assets.
99

10+
## Features
11+
- Serves files from Flutter assets
12+
- Support for default documents (like index.html)
13+
- Content type detection
14+
- Optional caching support with customizable max-age
15+
- Optional support for HTTP range requests
16+
1017
## Usage
1118

1219
Bind as root handler:
@@ -67,3 +74,63 @@ void main() {
6774
io.serve(app, 'localhost', 8080);
6875
}
6976
```
77+
78+
## Advanced Usage
79+
80+
### Caching
81+
82+
Enable browser caching for better performance:
83+
84+
```dart
85+
import 'package:shelf/shelf_io.dart' as io;
86+
import 'package:shelf_flutter_asset/shelf_flutter_asset.dart';
87+
88+
void main() {
89+
var assetHandler = createAssetHandler(
90+
defaultDocument: 'index.html',
91+
enableCaching: true, // Enable caching
92+
maxAge: 3600, // Cache for 1 hour (in seconds)
93+
);
94+
95+
io.serve(assetHandler, 'localhost', 8080);
96+
}
97+
```
98+
99+
### Range Requests
100+
101+
Support HTTP range requests for media files:
102+
103+
```dart
104+
import 'package:shelf/shelf_io.dart' as io;
105+
import 'package:shelf_flutter_asset/shelf_flutter_asset.dart';
106+
107+
void main() {
108+
var assetHandler = createAssetHandler(
109+
defaultDocument: 'index.html',
110+
enableRangeRequests: true, // Enable range requests for media streaming
111+
);
112+
113+
io.serve(assetHandler, 'localhost', 8080);
114+
}
115+
```
116+
117+
### Combined Features
118+
119+
Use all features together:
120+
121+
```dart
122+
import 'package:shelf/shelf_io.dart' as io;
123+
import 'package:shelf_flutter_asset/shelf_flutter_asset.dart';
124+
125+
void main() {
126+
var assetHandler = createAssetHandler(
127+
defaultDocument: 'index.html',
128+
rootPath: 'assets/web',
129+
enableCaching: true,
130+
maxAge: 86400, // Cache for 1 day
131+
enableRangeRequests: true,
132+
);
133+
134+
io.serve(assetHandler, 'localhost', 8080);
135+
}
136+
```

example/pubspec.lock

+49-25
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ packages:
3737
dependency: transitive
3838
description:
3939
name: collection
40-
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
40+
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
4141
url: "https://pub.dev"
4242
source: hosted
43-
version: "1.17.1"
43+
version: "1.18.0"
4444
cupertino_icons:
4545
dependency: "direct main"
4646
description:
@@ -83,14 +83,30 @@ packages:
8383
url: "https://pub.dev"
8484
source: hosted
8585
version: "4.0.2"
86-
js:
86+
leak_tracker:
8787
dependency: transitive
8888
description:
89-
name: js
90-
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
89+
name: leak_tracker
90+
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
9191
url: "https://pub.dev"
9292
source: hosted
93-
version: "0.6.7"
93+
version: "10.0.5"
94+
leak_tracker_flutter_testing:
95+
dependency: transitive
96+
description:
97+
name: leak_tracker_flutter_testing
98+
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
99+
url: "https://pub.dev"
100+
source: hosted
101+
version: "3.0.5"
102+
leak_tracker_testing:
103+
dependency: transitive
104+
description:
105+
name: leak_tracker_testing
106+
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
107+
url: "https://pub.dev"
108+
source: hosted
109+
version: "3.0.1"
94110
lints:
95111
dependency: transitive
96112
description:
@@ -103,26 +119,26 @@ packages:
103119
dependency: transitive
104120
description:
105121
name: matcher
106-
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
122+
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
107123
url: "https://pub.dev"
108124
source: hosted
109-
version: "0.12.15"
125+
version: "0.12.16+1"
110126
material_color_utilities:
111127
dependency: transitive
112128
description:
113129
name: material_color_utilities
114-
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
130+
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
115131
url: "https://pub.dev"
116132
source: hosted
117-
version: "0.2.0"
133+
version: "0.11.1"
118134
meta:
119135
dependency: transitive
120136
description:
121137
name: meta
122-
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
138+
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
123139
url: "https://pub.dev"
124140
source: hosted
125-
version: "1.9.1"
141+
version: "1.15.0"
126142
mime:
127143
dependency: transitive
128144
description:
@@ -135,10 +151,10 @@ packages:
135151
dependency: transitive
136152
description:
137153
name: path
138-
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
154+
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
139155
url: "https://pub.dev"
140156
source: hosted
141-
version: "1.8.3"
157+
version: "1.9.0"
142158
plugin_platform_interface:
143159
dependency: transitive
144160
description:
@@ -161,7 +177,7 @@ packages:
161177
path: ".."
162178
relative: true
163179
source: path
164-
version: "0.0.3"
180+
version: "0.1.0"
165181
sky_engine:
166182
dependency: transitive
167183
description: flutter
@@ -171,26 +187,26 @@ packages:
171187
dependency: transitive
172188
description:
173189
name: source_span
174-
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
190+
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
175191
url: "https://pub.dev"
176192
source: hosted
177-
version: "1.9.1"
193+
version: "1.10.0"
178194
stack_trace:
179195
dependency: transitive
180196
description:
181197
name: stack_trace
182-
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
198+
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
183199
url: "https://pub.dev"
184200
source: hosted
185-
version: "1.11.0"
201+
version: "1.11.1"
186202
stream_channel:
187203
dependency: transitive
188204
description:
189205
name: stream_channel
190-
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
206+
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
191207
url: "https://pub.dev"
192208
source: hosted
193-
version: "2.1.1"
209+
version: "2.1.2"
194210
string_scanner:
195211
dependency: transitive
196212
description:
@@ -211,10 +227,10 @@ packages:
211227
dependency: transitive
212228
description:
213229
name: test_api
214-
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
230+
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
215231
url: "https://pub.dev"
216232
source: hosted
217-
version: "0.5.1"
233+
version: "0.7.2"
218234
typed_data:
219235
dependency: transitive
220236
description:
@@ -231,6 +247,14 @@ packages:
231247
url: "https://pub.dev"
232248
source: hosted
233249
version: "2.1.4"
250+
vm_service:
251+
dependency: transitive
252+
description:
253+
name: vm_service
254+
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
255+
url: "https://pub.dev"
256+
source: hosted
257+
version: "14.2.5"
234258
webview_flutter:
235259
dependency: "direct main"
236260
description:
@@ -264,5 +288,5 @@ packages:
264288
source: hosted
265289
version: "3.0.2"
266290
sdks:
267-
dart: ">=3.0.0-0 <4.0.0"
268-
flutter: ">=3.0.0"
291+
dart: ">=3.3.0 <4.0.0"
292+
flutter: ">=3.18.0-18.0.pre.54"

lib/shelf_flutter_asset.dart

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:io';
2+
import 'dart:math';
23

34
import 'package:flutter/services.dart';
45
import 'package:mime/mime.dart';
@@ -8,6 +9,9 @@ import 'package:shelf/shelf.dart';
89
/// The default resolver for MIME types.
910
final _defaultMimeTypeResolver = MimeTypeResolver();
1011

12+
/// Default cache duration (1 hour)
13+
const _defaultMaxAge = 3600;
14+
1115
/// Creates a Shelf [Handler] that serves files from Flutter assets.
1216
///
1317
/// If requested resource does not exist and [defaultDocument] is specified,
@@ -20,10 +24,18 @@ final _defaultMimeTypeResolver = MimeTypeResolver();
2024
/// If your assets are not in the standard `assets` directory,
2125
/// or you want to share only subtree of the assets path structure,
2226
/// you may use [rootPath] argument to set the root directory for your handler.
27+
///
28+
/// Set [enableCaching] to true to add cache-control headers with [maxAge] seconds
29+
/// (defaults to 1 hour).
30+
///
31+
/// Set [enableRangeRequests] to true to support HTTP range requests for partial content.
2332
Handler createAssetHandler(
2433
{String? defaultDocument,
2534
String rootPath = 'assets',
26-
MimeTypeResolver? contentTypeResolver}) {
35+
MimeTypeResolver? contentTypeResolver,
36+
bool enableCaching = false,
37+
int maxAge = _defaultMaxAge,
38+
bool enableRangeRequests = false}) {
2739
final mimeResolver = contentTypeResolver ?? _defaultMimeTypeResolver;
2840

2941
return (Request request) async {
@@ -52,6 +64,16 @@ Handler createAssetHandler(
5264
if (contentType != null) HttpHeaders.contentTypeHeader: contentType,
5365
};
5466

67+
// Add cache control headers if enabled
68+
if (enableCaching) {
69+
headers[HttpHeaders.cacheControlHeader] = 'max-age=$maxAge, public';
70+
}
71+
72+
// Handle range requests if enabled
73+
if (enableRangeRequests && request.headers.containsKey('range')) {
74+
return _handleRangeRequest(request, body, headers);
75+
}
76+
5577
return Response.ok((request.method == 'HEAD') ? null : body,
5678
headers: headers);
5779
};
@@ -66,3 +88,46 @@ Future<Uint8List?> _loadResource(String key) async {
6688

6789
return null;
6890
}
91+
92+
/// Handles HTTP range requests by parsing the Range header and returning
93+
/// the appropriate partial content response.
94+
Response _handleRangeRequest(
95+
Request request, Uint8List body, Map<String, String> headers) {
96+
final rangeHeader = request.headers['range']!;
97+
final match = RegExp(r'bytes=(\d*)-(\d*)').firstMatch(rangeHeader);
98+
99+
if (match == null) {
100+
return Response(416, headers: headers); // Range Not Satisfiable
101+
}
102+
103+
final startStr = match.group(1);
104+
final endStr = match.group(2);
105+
106+
int start = startStr!.isEmpty ? 0 : int.parse(startStr);
107+
int end = endStr!.isEmpty ? body.length - 1 : int.parse(endStr);
108+
109+
// Validate the range
110+
if (start >= body.length || end >= body.length || start > end) {
111+
return Response(416, headers: {
112+
...headers,
113+
'content-range': 'bytes */${body.length}',
114+
});
115+
}
116+
117+
// Limit the length to the actual body size
118+
end = min(end, body.length - 1);
119+
120+
final length = end - start + 1;
121+
final rangeBody = body.sublist(start, end + 1);
122+
123+
final rangeHeaders = {
124+
...headers,
125+
HttpHeaders.contentLengthHeader: '$length',
126+
'content-range': 'bytes $start-$end/${body.length}',
127+
'accept-ranges': 'bytes',
128+
};
129+
130+
return Response(206, // Partial Content
131+
body: (request.method == 'HEAD') ? null : rangeBody,
132+
headers: rangeHeaders);
133+
}

pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: shelf_flutter_asset
22
description: A simple handler for the Shelf ecosystem to serve files from Flutter assets.
3-
version: 0.0.4
3+
version: 0.1.0
44
homepage: https://github.com/r8/shelf_flutter_asset
55
repository: https://github.com/r8/shelf_flutter_asset
66
issue_tracker: https://github.com/r8/shelf_flutter_asset/issues

0 commit comments

Comments
 (0)