Added range_header, user_agent and body_parser
This commit is contained in:
parent
6fb188b265
commit
7a2bb8f5da
51 changed files with 2104 additions and 7 deletions
|
@ -6,9 +6,13 @@ This repository contains the common utility packages required for developing dar
|
|||
|
||||
## Available Packages
|
||||
|
||||
* Html Builder
|
||||
* Body Parser
|
||||
* Code Buffer
|
||||
* Combinator
|
||||
* Html Builder
|
||||
* Merge Map
|
||||
* Pub Sub
|
||||
* Range Header
|
||||
* Symbol Table
|
||||
* User Agent
|
||||
|
4
packages/body_parser/.travis.yml
Normal file
4
packages/body_parser/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
dart:
|
||||
- dev
|
||||
- stable
|
12
packages/body_parser/AUTHORS.md
Normal file
12
packages/body_parser/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
32
packages/body_parser/CHANGELOG.md
Normal file
32
packages/body_parser/CHANGELOG.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Change Log
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Upgraded from `pendantic` to `lints` linter
|
||||
* Published as `belatuk_body_parser` package
|
||||
* Fixed linter warnings
|
||||
|
||||
## 2.1.1
|
||||
|
||||
* Fixed calling deprecated methods in unit test
|
||||
|
||||
## 2.1.0
|
||||
|
||||
* Replaced `http_server` with `belatuk_http_server`
|
||||
|
||||
## 2.0.1
|
||||
|
||||
* Fixed source code formating warning
|
||||
* Updated README
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
|
||||
## 1.1.1
|
||||
|
||||
* Dart 2 updates; should fix Angel in Travis.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Add `parseBodyFromStream`
|
29
packages/body_parser/LICENSE
Normal file
29
packages/body_parser/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
75
packages/body_parser/README.md
Normal file
75
packages/body_parser/README.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Belatuk Body Parser
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dev/packages/belatuk_body_parser)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/body_parser/LICENSE)
|
||||
|
||||
**Replacement of `package:body_parser` with breaking changes to support NNBD.**
|
||||
|
||||
Parse request bodies and query strings in Dart, as well multipart/form-data uploads. No external dependencies required.
|
||||
|
||||
This is the request body parser powering the [Angel3 framework](https://pub.dev/packages/angel3_framework). If you are looking for a server-side solution with dependency injection, WebSockets, and more, then I highly recommend it as your first choice. Bam!
|
||||
|
||||
## Contents
|
||||
|
||||
- [Belatuk Body Parser](#belatuk-body-parser)
|
||||
- [Contents](#contents)
|
||||
- [About](#about)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Custom Body Parsing](#custom-body-parsing)
|
||||
|
||||
### About
|
||||
|
||||
I needed something like Express.js's `body-parser` module, so I made it here. It fully supports JSON requests. x-www-form-urlencoded fully supported, as well as query strings. You can also include arrays in your query, in the same way you would for a PHP application. Full file upload support will also be present by the production 1.0.0 release.
|
||||
|
||||
A benefit of this is that primitive types are automatically deserialized correctly. As in, if you have a `hello=1.5` request, then `body['hello']` will equal `1.5` and not `'1.5'`. A very semantic difference, yes, but it relieves stress in my head.
|
||||
|
||||
### Installation
|
||||
|
||||
To install Body Parser for your Dart project, simply add body_parser to your pub dependencies.
|
||||
|
||||
dependencies:
|
||||
belatuk_body_parser: ^3.0.0
|
||||
|
||||
### Usage
|
||||
|
||||
Body Parser exposes a simple class called `BodyParseResult`. You can easily parse the query string and request body for a request by calling `Future<BodyParseResult> parseBody`.
|
||||
|
||||
```dart
|
||||
import 'dart:convert';
|
||||
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
|
||||
|
||||
main() async {
|
||||
// ...
|
||||
await for (HttpRequest request in server) {
|
||||
request.response.write(JSON.encode(await parseBody(request).body));
|
||||
await request.response.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also use `buildMapFromUri(Map, String)` to populate a map from a URL encoded string.
|
||||
|
||||
This can easily be used with a library like [Angel3 JSON God](https://pub.dev/packages/angel3_json_god) to build structured JSON/REST APIs. Add validation and you've got an instant backend.
|
||||
|
||||
```dart
|
||||
MyClass create(HttpRequest request) async {
|
||||
return god.deserialize(await parseBody(request).body, MyClass);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Body Parsing
|
||||
|
||||
In cases where you need to parse unrecognized content types, `body_parser` won't be of any help to you on its own. However, you can use the `originalBuffer` property of a `BodyParseResult` to see the original request buffer. To get this functionality, pass `storeOriginalBuffer` as `true` when calling `parseBody`.
|
||||
|
||||
For example, if you wanted to [parse GraphQL queries within your server](https://github.com/dukefirehawk/graphql_dart)...
|
||||
|
||||
```dart
|
||||
app.get('/graphql', (req, res) async {
|
||||
if (req.headers.contentType.mimeType == 'application/graphql') {
|
||||
var graphQlString = String.fromCharCodes(req.originalBuffer);
|
||||
// ...
|
||||
}
|
||||
});
|
||||
```
|
1
packages/body_parser/analysis_options.yaml
Normal file
1
packages/body_parser/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
61
packages/body_parser/example/main.dart
Normal file
61
packages/body_parser/example/main.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
|
||||
|
||||
void main() async {
|
||||
var address = '127.0.0.1';
|
||||
var port = 3000;
|
||||
var futures = <Future>[];
|
||||
|
||||
for (var i = 1; i < Platform.numberOfProcessors; i++) {
|
||||
futures.add(Isolate.spawn(start, [address, port, i]));
|
||||
}
|
||||
|
||||
await Future.wait(futures).then((_) {
|
||||
print('All instances started.');
|
||||
print(
|
||||
'Test with "wrk -t12 -c400 -d30s -s ./example/post.lua http://localhost:3000" or similar');
|
||||
start([address, port, 0]);
|
||||
});
|
||||
}
|
||||
|
||||
void start(List args) {
|
||||
var address = InternetAddress(args[0] as String);
|
||||
var port = 8080;
|
||||
if (args[1] is int) {
|
||||
args[1];
|
||||
}
|
||||
|
||||
var id = 0;
|
||||
if (args[2] is int) {
|
||||
args[2];
|
||||
}
|
||||
|
||||
HttpServer.bind(address, port, shared: true).then((server) {
|
||||
server.listen((request) async {
|
||||
// ignore: deprecated_member_use
|
||||
var body = await defaultParseBody(request);
|
||||
request.response
|
||||
..headers.contentType = ContentType('application', 'json')
|
||||
..write(json.encode(body.body));
|
||||
await request.response.close();
|
||||
});
|
||||
|
||||
print(
|
||||
'Server #$id listening at http://${server.address.address}:${server.port}');
|
||||
});
|
||||
}
|
||||
|
||||
Future<BodyParseResult> defaultParseBody(HttpRequest request,
|
||||
{bool storeOriginalBuffer = false}) {
|
||||
return parseBodyFromStream(
|
||||
request,
|
||||
request.headers.contentType != null
|
||||
? MediaType.parse(request.headers.contentType.toString())
|
||||
: null,
|
||||
request.uri,
|
||||
storeOriginalBuffer: storeOriginalBuffer);
|
||||
}
|
6
packages/body_parser/example/post.lua
Normal file
6
packages/body_parser/example/post.lua
Normal file
|
@ -0,0 +1,6 @@
|
|||
-- example HTTP POST script which demonstrates setting the
|
||||
-- HTTP method, body, and adding a header
|
||||
|
||||
wrk.method = "POST"
|
||||
wrk.body = "foo=bar&baz=quux"
|
||||
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
6
packages/body_parser/lib/belatuk_body_parser.dart
Normal file
6
packages/body_parser/lib/belatuk_body_parser.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
/// A library for parsing HTTP request bodies and queries.
|
||||
library angel3_body_parser;
|
||||
|
||||
export 'src/body_parse_result.dart';
|
||||
export 'src/file_upload_info.dart';
|
||||
export 'src/parse_body.dart';
|
28
packages/body_parser/lib/src/body_parse_result.dart
Normal file
28
packages/body_parser/lib/src/body_parse_result.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
import 'file_upload_info.dart';
|
||||
|
||||
/// A representation of data from an incoming request.
|
||||
abstract class BodyParseResult {
|
||||
/// The parsed body.
|
||||
Map<String?, dynamic> get body;
|
||||
|
||||
/// The parsed query string.
|
||||
Map<String?, dynamic> get query;
|
||||
|
||||
/// All files uploaded within this request.
|
||||
List<FileUploadInfo> get files;
|
||||
|
||||
/// The original body bytes sent with this request.
|
||||
///
|
||||
/// You must set [storeOriginalBuffer] to `true` to see this.
|
||||
List<int>? get originalBuffer;
|
||||
|
||||
/// If an error was encountered while parsing the body, it will appear here.
|
||||
///
|
||||
/// Otherwise, this is `null`.
|
||||
dynamic get error;
|
||||
|
||||
/// If an error was encountered while parsing the body, the call stack will appear here.
|
||||
///
|
||||
/// Otherwise, this is `null`.
|
||||
StackTrace? get stack;
|
||||
}
|
7
packages/body_parser/lib/src/chunk.dart
Normal file
7
packages/body_parser/lib/src/chunk.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'file_upload_info.dart';
|
||||
|
||||
List<FileUploadInfo> getFileDataFromChunk(
|
||||
String chunk, String boundary, String fileUploadName, Map body) {
|
||||
var result = <FileUploadInfo>[];
|
||||
return result;
|
||||
}
|
17
packages/body_parser/lib/src/file_upload_info.dart
Normal file
17
packages/body_parser/lib/src/file_upload_info.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
/// Represents a file uploaded to the server.
|
||||
class FileUploadInfo {
|
||||
/// The MIME type of the uploaded file.
|
||||
String? mimeType;
|
||||
|
||||
/// The name of the file field from the request.
|
||||
String? name;
|
||||
|
||||
/// The filename of the file.
|
||||
String? filename;
|
||||
|
||||
/// The bytes that make up this file.
|
||||
List<int> data;
|
||||
|
||||
FileUploadInfo(
|
||||
{this.mimeType, this.name, this.filename, this.data = const []});
|
||||
}
|
22
packages/body_parser/lib/src/get_value.dart
Normal file
22
packages/body_parser/lib/src/get_value.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'dart:convert';
|
||||
|
||||
dynamic getValue(String value) {
|
||||
try {
|
||||
var numValue = num.parse(value);
|
||||
if (!numValue.isNaN) {
|
||||
return numValue;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
} on FormatException {
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
return json.decode(value);
|
||||
} else if (value.startsWith('{') && value.endsWith('}')) {
|
||||
return json.decode(value);
|
||||
} else if (value.trim().toLowerCase() == 'null') {
|
||||
return null;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
44
packages/body_parser/lib/src/map_from_uri.dart
Normal file
44
packages/body_parser/lib/src/map_from_uri.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
import 'get_value.dart';
|
||||
|
||||
/// Parses a URI-encoded string into real data! **Wow!**
|
||||
///
|
||||
/// Whichever map you provide will be automatically populated from the urlencoded body string you provide.
|
||||
void buildMapFromUri(Map map, String body) {
|
||||
var parseArrayRgx = RegExp(r'^(.+)\[\]$');
|
||||
|
||||
for (var keyValuePair in body.split('&')) {
|
||||
if (keyValuePair.contains('=')) {
|
||||
var equals = keyValuePair.indexOf('=');
|
||||
var key = Uri.decodeQueryComponent(keyValuePair.substring(0, equals));
|
||||
var value = Uri.decodeQueryComponent(keyValuePair.substring(equals + 1));
|
||||
|
||||
if (parseArrayRgx.hasMatch(key)) {
|
||||
Match queryMatch = parseArrayRgx.firstMatch(key)!;
|
||||
key = queryMatch.group(1)!;
|
||||
if (map[key] is! List) {
|
||||
map[key] = [];
|
||||
}
|
||||
|
||||
map[key].add(getValue(value));
|
||||
} else if (key.contains('.')) {
|
||||
// i.e. map.foo.bar => [map, foo, bar]
|
||||
var keys = key.split('.');
|
||||
|
||||
var targetMap = map[keys[0]] != null ? map[keys[0]] as Map? : {};
|
||||
map[keys[0]] = targetMap;
|
||||
for (var i = 1; i < keys.length; i++) {
|
||||
if (i < keys.length - 1) {
|
||||
targetMap![keys[i]] = targetMap[keys[i]] ?? {};
|
||||
targetMap = targetMap[keys[i]] as Map?;
|
||||
} else {
|
||||
targetMap![keys[i]] = getValue(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
map[key] = getValue(value);
|
||||
}
|
||||
} else {
|
||||
map[Uri.decodeQueryComponent(keyValuePair)] = true;
|
||||
}
|
||||
}
|
||||
}
|
147
packages/body_parser/lib/src/parse_body.dart
Normal file
147
packages/body_parser/lib/src/parse_body.dart
Normal file
|
@ -0,0 +1,147 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:belatuk_http_server/belatuk_http_server.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import 'body_parse_result.dart';
|
||||
import 'file_upload_info.dart';
|
||||
import 'map_from_uri.dart';
|
||||
|
||||
/// Forwards to [parseBodyFromStream].
|
||||
@deprecated
|
||||
Future<BodyParseResult> parseBody(HttpRequest request,
|
||||
{bool storeOriginalBuffer = false}) {
|
||||
return parseBodyFromStream(
|
||||
request,
|
||||
request.headers.contentType != null
|
||||
? MediaType.parse(request.headers.contentType.toString())
|
||||
: null,
|
||||
request.uri,
|
||||
storeOriginalBuffer: storeOriginalBuffer);
|
||||
}
|
||||
|
||||
/// Grabs data from an incoming request.
|
||||
///
|
||||
/// Supports URL-encoded and JSON, as well as multipart/* forms.
|
||||
/// On a file upload request, only fields with the name **'file'** are processed
|
||||
/// as files. Anything else is put in the body. You can change the upload file name
|
||||
/// via the *fileUploadName* parameter. :)
|
||||
///
|
||||
/// Use [storeOriginalBuffer] to add the original request bytes to the result.
|
||||
Future<BodyParseResult> parseBodyFromStream(
|
||||
Stream<Uint8List> data, MediaType? contentType, Uri requestUri,
|
||||
{bool storeOriginalBuffer = false}) async {
|
||||
var result = _BodyParseResultImpl();
|
||||
|
||||
Future<Uint8List> getBytes() {
|
||||
return data
|
||||
.fold<BytesBuilder>(BytesBuilder(copy: false), (a, b) => a..add(b))
|
||||
.then((b) => b.takeBytes());
|
||||
}
|
||||
|
||||
Future<String> getBody() {
|
||||
if (storeOriginalBuffer) {
|
||||
return getBytes().then((bytes) {
|
||||
result.originalBuffer = bytes;
|
||||
return utf8.decode(bytes);
|
||||
});
|
||||
} else {
|
||||
return utf8.decoder.bind(data).join();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (contentType != null) {
|
||||
if (contentType.type == 'multipart' &&
|
||||
contentType.parameters.containsKey('boundary')) {
|
||||
Stream<Uint8List> stream;
|
||||
|
||||
if (storeOriginalBuffer) {
|
||||
var bytes = result.originalBuffer = await getBytes();
|
||||
var ctrl = StreamController<Uint8List>()..add(bytes);
|
||||
await ctrl.close();
|
||||
stream = ctrl.stream;
|
||||
} else {
|
||||
stream = data;
|
||||
}
|
||||
|
||||
var parts =
|
||||
MimeMultipartTransformer(contentType.parameters['boundary']!)
|
||||
.bind(stream)
|
||||
.map((part) =>
|
||||
HttpMultipartFormData.parse(part, defaultEncoding: utf8));
|
||||
|
||||
await for (HttpMultipartFormData part in parts) {
|
||||
if (part.isBinary ||
|
||||
part.contentDisposition.parameters.containsKey('filename')) {
|
||||
var builder = await part.fold(
|
||||
BytesBuilder(copy: false),
|
||||
(BytesBuilder b, d) =>
|
||||
b..add(d is! String ? (d as List<int>?)! : d.codeUnits));
|
||||
var upload = FileUploadInfo(
|
||||
mimeType: part.contentType!.mimeType,
|
||||
name: part.contentDisposition.parameters['name'],
|
||||
filename:
|
||||
part.contentDisposition.parameters['filename'] ?? 'file',
|
||||
data: builder.takeBytes());
|
||||
result.files.add(upload);
|
||||
} else if (part.isText) {
|
||||
var text = await part.join();
|
||||
buildMapFromUri(result.body,
|
||||
'${part.contentDisposition.parameters["name"]}=$text');
|
||||
}
|
||||
}
|
||||
} else if (contentType.mimeType == 'application/json') {
|
||||
result.body.addAll(
|
||||
_foldToStringDynamic(json.decode(await getBody()) as Map?)!);
|
||||
} else if (contentType.mimeType == 'application/x-www-form-urlencoded') {
|
||||
var body = await getBody();
|
||||
buildMapFromUri(result.body, body);
|
||||
} else if (storeOriginalBuffer == true) {
|
||||
result.originalBuffer = await getBytes();
|
||||
}
|
||||
} else {
|
||||
if (requestUri.hasQuery) {
|
||||
buildMapFromUri(result.query, requestUri.query);
|
||||
}
|
||||
|
||||
if (storeOriginalBuffer == true) {
|
||||
result.originalBuffer = await getBytes();
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
result.error = e;
|
||||
result.stack = st;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class _BodyParseResultImpl implements BodyParseResult {
|
||||
@override
|
||||
Map<String?, dynamic> body = {};
|
||||
|
||||
@override
|
||||
List<FileUploadInfo> files = [];
|
||||
|
||||
@override
|
||||
List<int>? originalBuffer;
|
||||
|
||||
@override
|
||||
Map<String?, dynamic> query = {};
|
||||
|
||||
@override
|
||||
var error;
|
||||
|
||||
@override
|
||||
StackTrace? stack;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _foldToStringDynamic(Map? map) {
|
||||
return map?.keys.fold<Map<String, dynamic>>(
|
||||
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
|
||||
}
|
14
packages/body_parser/pubspec.yaml
Normal file
14
packages/body_parser/pubspec.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: belatuk_body_parser
|
||||
version: 3.0.0
|
||||
description: Parse request bodies and query strings in Dart. Supports JSON, URL-encoded, and multi-part bodies.
|
||||
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/body_parser
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
http_parser: ^4.0.0
|
||||
belatuk_http_server: ^2.0.0
|
||||
mime: ^1.0.0
|
||||
dev_dependencies:
|
||||
http: ^0.13.0
|
||||
test: ^1.17.8
|
||||
lints: ^1.0.0
|
156
packages/body_parser/test/form_data_test.dart
Normal file
156
packages/body_parser/test/form_data_test.dart
Normal file
|
@ -0,0 +1,156 @@
|
|||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'server_test.dart';
|
||||
|
||||
Future<BodyParseResult> _parseBody(HttpRequest request) {
|
||||
return parseBodyFromStream(
|
||||
request,
|
||||
request.headers.contentType != null
|
||||
? MediaType.parse(request.headers.contentType.toString())
|
||||
: null,
|
||||
request.uri,
|
||||
storeOriginalBuffer: false);
|
||||
}
|
||||
|
||||
void main() {
|
||||
HttpServer? server;
|
||||
String? url;
|
||||
http.Client? client;
|
||||
|
||||
setUp(() async {
|
||||
server = await HttpServer.bind('127.0.0.1', 0);
|
||||
server!.listen((HttpRequest request) async {
|
||||
//Server will simply return a JSON representation of the parsed body
|
||||
// ignore: deprecated_member_use
|
||||
request.response.write(jsonEncodeBody(await _parseBody(request)));
|
||||
await request.response.close();
|
||||
});
|
||||
url = 'http://localhost:${server!.port}';
|
||||
print('Test server listening on $url');
|
||||
client = http.Client();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await server!.close(force: true);
|
||||
client!.close();
|
||||
server = null;
|
||||
url = null;
|
||||
client = null;
|
||||
});
|
||||
|
||||
test('No upload', () async {
|
||||
var boundary = 'myBoundary';
|
||||
var headers = <String, String>{
|
||||
'content-type': 'multipart/form-data; boundary=$boundary'
|
||||
};
|
||||
var postData = '''
|
||||
--$boundary
|
||||
Content-Disposition: form-data; name="hello"
|
||||
|
||||
world
|
||||
--$boundary--
|
||||
'''
|
||||
.replaceAll('\n', '\r\n');
|
||||
|
||||
print(
|
||||
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
|
||||
var response =
|
||||
await client!.post(Uri.parse(url!), headers: headers, body: postData);
|
||||
print('Response: ${response.body}');
|
||||
var jsons = json.decode(response.body);
|
||||
var files = jsons['files'].map((map) {
|
||||
return map == null
|
||||
? null
|
||||
: map.keys.fold<Map<String, dynamic>>(
|
||||
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
|
||||
});
|
||||
expect(files.length, equals(0));
|
||||
expect(jsons['body']['hello'], equals('world'));
|
||||
});
|
||||
|
||||
test('Single upload', () async {
|
||||
var boundary = 'myBoundary';
|
||||
var headers = <String, String>{
|
||||
'content-type': ContentType('multipart', 'form-data',
|
||||
parameters: {'boundary': boundary}).toString()
|
||||
};
|
||||
var postData = '''
|
||||
--$boundary
|
||||
Content-Disposition: form-data; name="hello"
|
||||
|
||||
world
|
||||
--$boundary
|
||||
Content-Disposition: form-data; name="file"; filename="app.dart"
|
||||
Content-Type: application/dart
|
||||
|
||||
Hello world
|
||||
--$boundary--
|
||||
'''
|
||||
.replaceAll('\n', '\r\n');
|
||||
|
||||
print(
|
||||
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
|
||||
var response =
|
||||
await client!.post(Uri.parse(url!), headers: headers, body: postData);
|
||||
print('Response: ${response.body}');
|
||||
var jsons = json.decode(response.body);
|
||||
var files = jsons['files'];
|
||||
expect(files.length, equals(1));
|
||||
expect(files[0]['name'], equals('file'));
|
||||
expect(files[0]['mimeType'], equals('application/dart'));
|
||||
expect(files[0]['data'].length, equals(11));
|
||||
expect(files[0]['filename'], equals('app.dart'));
|
||||
expect(jsons['body']['hello'], equals('world'));
|
||||
});
|
||||
|
||||
test('Multiple upload', () async {
|
||||
var boundary = 'myBoundary';
|
||||
var headers = <String, String>{
|
||||
'content-type': 'multipart/form-data; boundary=$boundary'
|
||||
};
|
||||
var postData = '''
|
||||
--$boundary
|
||||
Content-Disposition: form-data; name="json"
|
||||
|
||||
god
|
||||
--$boundary
|
||||
Content-Disposition: form-data; name="num"
|
||||
|
||||
14.50000
|
||||
--$boundary
|
||||
Content-Disposition: form-data; name="file"; filename="app.dart"
|
||||
Content-Type: text/plain
|
||||
|
||||
Hello world
|
||||
--$boundary
|
||||
Content-Disposition: form-data; name="entry-point"; filename="main.js"
|
||||
Content-Type: text/javascript
|
||||
|
||||
function main() {
|
||||
console.log("Hello, world!");
|
||||
}
|
||||
--$boundary--
|
||||
'''
|
||||
.replaceAll('\n', '\r\n');
|
||||
|
||||
print(
|
||||
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
|
||||
var response =
|
||||
await client!.post(Uri.parse(url!), headers: headers, body: postData);
|
||||
print('Response: ${response.body}');
|
||||
var jsons = json.decode(response.body);
|
||||
var files = jsons['files'];
|
||||
expect(files.length, equals(2));
|
||||
expect(files[0]['name'], equals('file'));
|
||||
expect(files[0]['mimeType'], equals('text/plain'));
|
||||
expect(files[0]['data'].length, equals(11));
|
||||
expect(files[1]['name'], equals('entry-point'));
|
||||
expect(files[1]['mimeType'], equals('text/javascript'));
|
||||
expect(jsons['body']['json'], equals('god'));
|
||||
expect(jsons['body']['num'], equals(14.5));
|
||||
});
|
||||
}
|
174
packages/body_parser/test/server_test.dart
Normal file
174
packages/body_parser/test/server_test.dart
Normal file
|
@ -0,0 +1,174 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io' show HttpRequest, HttpServer;
|
||||
|
||||
import 'package:belatuk_body_parser/belatuk_body_parser.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const TOKEN =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjcuMC4wLjEiLCJleHAiOi0xLCJpYXQiOiIyMDE2LTEyLTIyVDEyOjQ5OjUwLjM2MTQ0NiIsImlzcyI6ImFuZ2VsX2F1dGgiLCJzdWIiOiIxMDY2OTQ4Mzk2MDIwMjg5ODM2NTYifQ==.PYw7yUb-cFWD7N0sSLztP7eeRvO44nu1J2OgDNyT060=';
|
||||
|
||||
String jsonEncodeBody(BodyParseResult result) {
|
||||
return json.encode({
|
||||
'query': result.query,
|
||||
'body': result.body,
|
||||
'error': result.error?.toString(),
|
||||
'files': result.files.map((f) {
|
||||
return {
|
||||
'name': f.name,
|
||||
'mimeType': f.mimeType,
|
||||
'filename': f.filename,
|
||||
'data': f.data,
|
||||
};
|
||||
}).toList(),
|
||||
'originalBuffer': result.originalBuffer,
|
||||
'stack': null, //result.stack.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future<BodyParseResult> _parseBody(HttpRequest request) {
|
||||
return parseBodyFromStream(
|
||||
request,
|
||||
request.headers.contentType != null
|
||||
? MediaType.parse(request.headers.contentType.toString())
|
||||
: null,
|
||||
request.uri,
|
||||
storeOriginalBuffer: true);
|
||||
}
|
||||
|
||||
void main() {
|
||||
HttpServer? server;
|
||||
String? url;
|
||||
http.Client? client;
|
||||
|
||||
setUp(() async {
|
||||
server = await HttpServer.bind('127.0.0.1', 0);
|
||||
server!.listen((HttpRequest request) async {
|
||||
//Server will simply return a JSON representation of the parsed body
|
||||
request.response.write(
|
||||
// ignore: deprecated_member_use
|
||||
jsonEncodeBody(await _parseBody(request)));
|
||||
await request.response.close();
|
||||
});
|
||||
url = 'http://localhost:${server!.port}';
|
||||
print('Test server listening on $url');
|
||||
client = http.Client();
|
||||
});
|
||||
tearDown(() async {
|
||||
await server!.close(force: true);
|
||||
client!.close();
|
||||
server = null;
|
||||
url = null;
|
||||
client = null;
|
||||
});
|
||||
|
||||
group('query string', () {
|
||||
test('GET Simple', () async {
|
||||
print('GET $url/?hello=world');
|
||||
var response = await client!.get(Uri.parse('$url/?hello=world'));
|
||||
print('Response: ${response.body}');
|
||||
var result = json.decode(response.body);
|
||||
expect(result['body'], equals({}));
|
||||
expect(result['query'], equals({'hello': 'world'}));
|
||||
expect(result['files'], equals([]));
|
||||
//expect(result['originalBuffer'], isNull);
|
||||
});
|
||||
|
||||
test('GET Complex', () async {
|
||||
var postData =
|
||||
'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - 1}&map.foo.bar=baz';
|
||||
print('Body: $postData');
|
||||
var response = await client!.get(Uri.parse('$url/?$postData'));
|
||||
print('Response: ${response.body}');
|
||||
var query = json.decode(response.body)['query'];
|
||||
expect(query['hello'], equals('world'));
|
||||
expect(query['nums'][2], equals(2));
|
||||
expect(query['map'] is Map, equals(true));
|
||||
expect(query['map']['foo'], equals({'bar': 'baz'}));
|
||||
});
|
||||
|
||||
test('JWT', () async {
|
||||
var postData = 'token=$TOKEN';
|
||||
print('Body: $postData');
|
||||
var response = await client!.get(Uri.parse('$url/?$postData'));
|
||||
print('Response: ${response.body}');
|
||||
var query = json.decode(response.body)['query'];
|
||||
expect(query['token'], equals(TOKEN));
|
||||
});
|
||||
});
|
||||
|
||||
group('urlencoded', () {
|
||||
var headers = <String, String>{
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
test('POST Simple', () async {
|
||||
print('Body: hello=world');
|
||||
var response = await client!
|
||||
.post(Uri.parse(url!), headers: headers, body: 'hello=world');
|
||||
print('Response: ${response.body}');
|
||||
var result = json.decode(response.body);
|
||||
expect(result['query'], equals({}));
|
||||
expect(result['body'], equals({'hello': 'world'}));
|
||||
expect(result['files'], equals([]));
|
||||
expect(result['originalBuffer'], isList);
|
||||
expect(result['originalBuffer'], isNotEmpty);
|
||||
});
|
||||
|
||||
test('Post Complex', () async {
|
||||
var postData =
|
||||
'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - 1}&map.foo.bar=baz';
|
||||
var response =
|
||||
await client!.post(Uri.parse(url!), headers: headers, body: postData);
|
||||
print('Response: ${response.body}');
|
||||
var body = json.decode(response.body)['body'];
|
||||
expect(body['hello'], equals('world'));
|
||||
expect(body['nums'][2], equals(2));
|
||||
expect(body['map'] is Map, equals(true));
|
||||
expect(body['map']['foo'], equals({'bar': 'baz'}));
|
||||
});
|
||||
|
||||
test('JWT', () async {
|
||||
var postData = 'token=$TOKEN';
|
||||
var response =
|
||||
await client!.post(Uri.parse(url!), headers: headers, body: postData);
|
||||
var body = json.decode(response.body)['body'];
|
||||
expect(body['token'], equals(TOKEN));
|
||||
});
|
||||
});
|
||||
|
||||
group('json', () {
|
||||
var headers = <String, String>{'content-type': 'application/json'};
|
||||
test('Post Simple', () async {
|
||||
var postData = json.encode({'hello': 'world'});
|
||||
print('Body: $postData');
|
||||
var response =
|
||||
await client!.post(Uri.parse(url!), headers: headers, body: postData);
|
||||
print('Response: ${response.body}');
|
||||
var result = json.decode(response.body);
|
||||
expect(result['body'], equals({'hello': 'world'}));
|
||||
expect(result['query'], equals({}));
|
||||
expect(result['files'], equals([]));
|
||||
expect(result['originalBuffer'], allOf(isList, isNotEmpty));
|
||||
});
|
||||
|
||||
test('Post Complex', () async {
|
||||
var postData = json.encode({
|
||||
'hello': 'world',
|
||||
'nums': [1, 2.0, 3 - 1],
|
||||
'map': {
|
||||
'foo': {'bar': 'baz'}
|
||||
}
|
||||
});
|
||||
print('Body: $postData');
|
||||
var response =
|
||||
await client!.post(Uri.parse(url!), headers: headers, body: postData);
|
||||
print('Response: ${response.body}');
|
||||
var body = json.decode(response.body)['body'];
|
||||
expect(body['hello'], equals('world'));
|
||||
expect(body['nums'][2], equals(2));
|
||||
expect(body['map'] is Map, equals(true));
|
||||
expect(body['map']['foo'], equals({'bar': 'baz'}));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
# Belatuk Code Buffer
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_code_buffer)
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_code_buffer)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/code_buffer/LICENSE)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Belatuk Combinator
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dartlang.org/packages/belatuk_combinator)
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dev/packages/belatuk_combinator)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/combinator/LICENSE)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Betaluk Html Builder
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_html_builder)
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_html_builder)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/html_builder/LICENSE)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Belatuk Merge Map
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_merge_map)
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_merge_map)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/merge_map/LICENSE)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Belatuk Pub Sub
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v4.0.2-brightgreen)](https://pub.dartlang.org/packages/belatuk_pub_sub)
|
||||
[![version](https://img.shields.io/badge/pub-v4.0.2-brightgreen)](https://pub.dev/packages/belatuk_pub_sub)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/pub_sub/LICENSE)
|
||||
|
||||
|
|
12
packages/range_header/AUTHORS.md
Normal file
12
packages/range_header/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
34
packages/range_header/CHANGELOG.md
Normal file
34
packages/range_header/CHANGELOG.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Change Log
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Upgraded from `pendantic` to `lints` linter
|
||||
* Published as `belatuk_range_header` package
|
||||
|
||||
## 3.0.2
|
||||
|
||||
* Updated README
|
||||
|
||||
## 3.0.1
|
||||
|
||||
* Resolve static analysis warnings
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to work with Dart SDK 2.12.x NNBD
|
||||
|
||||
## 2.0.2
|
||||
|
||||
* Fix bug in `toContentRange` that printed invalid indices.
|
||||
* Fold header items by default.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
* Adjust `RangeHeaderTransformer` to properly print the content range of each item,
|
||||
when multiple are present.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Dart 2 update.
|
||||
* Add `RangeHeaderTransformer`.
|
||||
* Overall restructuring/refactoring.
|
29
packages/range_header/LICENSE
Normal file
29
packages/range_header/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
40
packages/range_header/README.md
Normal file
40
packages/range_header/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Belatuk Range Header
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dev/packages/belatuk_range_header)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/range_header/LICENSE)
|
||||
|
||||
**Replacement of `package:range_header` with breaking changes to support NNBD.**
|
||||
|
||||
Range header parser for belatuk. Can be used by any dart backend.
|
||||
|
||||
## Installation
|
||||
|
||||
In your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
belatuk_range_header: ^4.0.0
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```dart
|
||||
handleRequest(HttpRequest request) async {
|
||||
// Parse the header
|
||||
var header = RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader));
|
||||
|
||||
// Optimize/canonicalize it
|
||||
var items = RangeHeader.foldItems(header.items);
|
||||
header = RangeHeader(items);
|
||||
|
||||
// Get info
|
||||
header.items;
|
||||
header.rangeUnit;
|
||||
print(header.items[0].toContentRange(fileSize));
|
||||
|
||||
// Serve the file
|
||||
var transformer = RangeHeaderTransformer(header);
|
||||
await file.openRead().transform(transformer).pipe(request.response);
|
||||
}
|
||||
```
|
1
packages/range_header/analysis_options.yaml
Normal file
1
packages/range_header/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
28
packages/range_header/example/main.dart
Normal file
28
packages/range_header/example/main.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel3_range_header/angel3_range_header.dart';
|
||||
|
||||
var file = File('some_video.mp4');
|
||||
|
||||
void handleRequest(HttpRequest request) async {
|
||||
// Parse the header
|
||||
var header =
|
||||
RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)!);
|
||||
|
||||
// Optimize/canonicalize it
|
||||
var items = RangeHeader.foldItems(header.items);
|
||||
header = RangeHeader(items);
|
||||
|
||||
// Get info
|
||||
header.items;
|
||||
header.rangeUnit;
|
||||
header.items.forEach((item) => item.toContentRange(400));
|
||||
|
||||
// Serve the file
|
||||
var transformer =
|
||||
RangeHeaderTransformer(header, 'video/mp4', await file.length());
|
||||
await file
|
||||
.openRead()
|
||||
.cast<List<int>>()
|
||||
.transform(transformer)
|
||||
.pipe(request.response);
|
||||
}
|
103
packages/range_header/example/server.dart
Normal file
103
packages/range_header/example/server.dart
Normal file
|
@ -0,0 +1,103 @@
|
|||
//import 'package:angel_framework/angel_framework.dart';
|
||||
//import 'package:angel_framework/http.dart';
|
||||
//import 'package:angel_static/angel_static.dart';
|
||||
|
||||
void main() async {
|
||||
/*
|
||||
var app = new Angel();
|
||||
var http = new AngelHttp(app);
|
||||
var fs = const LocalFileSystem();
|
||||
var vDir = new _RangingVirtualDirectory(app, fs.currentDirectory);
|
||||
app.logger = new Logger('range_header')
|
||||
..onRecord.listen((rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
});
|
||||
app.mimeTypeResolver
|
||||
..addExtension('dart', 'text/dart')
|
||||
..addExtension('lock', 'text/plain')
|
||||
..addExtension('md', 'text/plain')
|
||||
..addExtension('packages', 'text/plain')
|
||||
..addExtension('yaml', 'text/plain')
|
||||
..addExtension('yml', 'text/plain');
|
||||
app.fallback(vDir.handleRequest);
|
||||
app.fallback((req, res) => throw new AngelHttpException.notFound());
|
||||
await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http.uri}');
|
||||
*/
|
||||
}
|
||||
/*
|
||||
class _RangingVirtualDirectory extends VirtualDirectory {
|
||||
_RangingVirtualDirectory(Angel app, Directory source)
|
||||
: super(app, source.fileSystem,
|
||||
source: source, allowDirectoryListing: true);
|
||||
|
||||
@override
|
||||
Future<bool> serveFile(
|
||||
File file, FileStat stat, RequestContext req, ResponseContext res) async {
|
||||
res.headers[HttpHeaders.acceptRangesHeader] = 'bytes';
|
||||
|
||||
if (req.headers.value(HttpHeaders.rangeHeader)?.startsWith('bytes') ==
|
||||
true) {
|
||||
var header =
|
||||
new RangeHeader.parse(req.headers.value(HttpHeaders.rangeHeader));
|
||||
header = new RangeHeader(RangeHeader.foldItems(header.items));
|
||||
|
||||
if (header.items.length == 1) {
|
||||
var item = header.items[0];
|
||||
Stream<Uint8List> stream;
|
||||
int len = 0, total = await file.length();
|
||||
|
||||
if (item.start == -1) {
|
||||
if (item.end == -1) {
|
||||
len = total;
|
||||
stream = file.openRead();
|
||||
} else {
|
||||
len = item.end + 1;
|
||||
stream = file.openRead(0, item.end + 1);
|
||||
}
|
||||
} else {
|
||||
if (item.end == -1) {
|
||||
len = total - item.start;
|
||||
stream = file.openRead(item.start);
|
||||
} else {
|
||||
len = item.end - item.start + 1;
|
||||
stream = file.openRead(item.start, item.end + 1);
|
||||
}
|
||||
}
|
||||
|
||||
res.contentType = new MediaType.parse(
|
||||
app.mimeTypeResolver.lookup(file.path) ??
|
||||
'application/octet-stream');
|
||||
res.statusCode = HttpStatus.partialContent;
|
||||
res.headers[HttpHeaders.contentLengthHeader] = len.toString();
|
||||
res.headers[HttpHeaders.contentRangeHeader] =
|
||||
'bytes ' + item.toContentRange(total);
|
||||
await stream.cast<List<int>>().pipe(res);
|
||||
return false;
|
||||
} else {
|
||||
var totalFileSize = await file.length();
|
||||
var transformer = new RangeHeaderTransformer(
|
||||
header,
|
||||
app.mimeTypeResolver.lookup(file.path) ??
|
||||
'application/octet-stream',
|
||||
await file.length());
|
||||
res.statusCode = HttpStatus.partialContent;
|
||||
res.headers[HttpHeaders.contentLengthHeader] =
|
||||
transformer.computeContentLength(totalFileSize).toString();
|
||||
res.contentType = new MediaType(
|
||||
'multipart', 'byteranges', {'boundary': transformer.boundary});
|
||||
await file
|
||||
.openRead()
|
||||
.cast<List<int>>()
|
||||
.transform(transformer)
|
||||
.pipe(res);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return await super.serveFile(file, stat, req, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
4
packages/range_header/lib/angel3_range_header.dart
Normal file
4
packages/range_header/lib/angel3_range_header.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
export 'src/converter.dart';
|
||||
export 'src/exception.dart';
|
||||
export 'src/range_header.dart';
|
||||
export 'src/range_header_item.dart';
|
171
packages/range_header/lib/src/converter.dart
Normal file
171
packages/range_header/lib/src/converter.dart
Normal file
|
@ -0,0 +1,171 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show BytesBuilder;
|
||||
import 'dart:math';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:charcode/ascii.dart';
|
||||
import 'range_header.dart';
|
||||
|
||||
/// A [StreamTransformer] that uses a parsed [RangeHeader] and transforms an input stream
|
||||
/// into one compatible with the `multipart/byte-ranges` specification.
|
||||
class RangeHeaderTransformer
|
||||
extends StreamTransformerBase<List<int>, List<int>> {
|
||||
final RangeHeader header;
|
||||
final String boundary, mimeType;
|
||||
final int totalLength;
|
||||
|
||||
RangeHeaderTransformer(this.header, this.mimeType, this.totalLength,
|
||||
{String? boundary})
|
||||
: boundary = boundary ?? _randomString() {
|
||||
if (header.items.isEmpty) {
|
||||
throw ArgumentError('`header` cannot be null or empty.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the content length that will be written to a response, given a stream of the given [totalFileSize].
|
||||
int computeContentLength(int totalFileSize) {
|
||||
var len = 0;
|
||||
|
||||
for (var item in header.items) {
|
||||
if (item.start == -1) {
|
||||
if (item.end == -1) {
|
||||
len += totalFileSize;
|
||||
} else {
|
||||
//len += item.end + 1;
|
||||
len += item.end + 1;
|
||||
}
|
||||
} else if (item.end == -1) {
|
||||
len += totalFileSize - item.start;
|
||||
//len += totalFileSize - item.start - 1;
|
||||
} else {
|
||||
len += item.end - item.start;
|
||||
}
|
||||
|
||||
// Take into consideration the fact that delimiters are written.
|
||||
len += utf8.encode('--$boundary\r\n').length;
|
||||
len += utf8.encode('Content-Type: $mimeType\r\n').length;
|
||||
len += utf8
|
||||
.encode(
|
||||
'Content-Range: ${header.rangeUnit} ${item.toContentRange(totalLength)}/$totalLength\r\n\r\n')
|
||||
.length;
|
||||
len += 2; // CRLF
|
||||
}
|
||||
|
||||
len += utf8.encode('--$boundary--\r\n').length;
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<int>> bind(Stream<List<int>> stream) {
|
||||
var ctrl = StreamController<List<int>>();
|
||||
|
||||
Future(() async {
|
||||
var index = 0;
|
||||
var enqueued = Queue<List<int>>();
|
||||
var q = StreamQueue(stream);
|
||||
|
||||
Future<List<int>> absorb(int length) async {
|
||||
var out = BytesBuilder();
|
||||
|
||||
while (out.length < length) {
|
||||
var remaining = length - out.length;
|
||||
|
||||
while (out.length < length && enqueued.isNotEmpty) {
|
||||
remaining = length - out.length;
|
||||
var blob = enqueued.removeFirst();
|
||||
|
||||
if (blob.length > remaining) {
|
||||
enqueued.addFirst(blob.skip(remaining).toList());
|
||||
blob = blob.take(remaining).toList();
|
||||
}
|
||||
|
||||
out.add(blob);
|
||||
index += blob.length;
|
||||
}
|
||||
|
||||
if (out.length < length && await q.hasNext) {
|
||||
var blob = await q.next;
|
||||
remaining = length - out.length;
|
||||
|
||||
if (blob.length > remaining) {
|
||||
enqueued.addFirst(blob.skip(remaining).toList());
|
||||
blob = blob.take(remaining).toList();
|
||||
}
|
||||
|
||||
out.add(blob);
|
||||
index += blob.length;
|
||||
}
|
||||
|
||||
// If we get this far, and the stream is EMPTY, the user requested
|
||||
// too many bytes.
|
||||
if (out.length < length && enqueued.isEmpty && !(await q.hasNext)) {
|
||||
throw StateError(
|
||||
'The range denoted is bigger than the size of the input stream.');
|
||||
}
|
||||
}
|
||||
|
||||
return out.takeBytes();
|
||||
}
|
||||
|
||||
for (var item in header.items) {
|
||||
var chunk = BytesBuilder();
|
||||
|
||||
// Skip until we reach the start index.
|
||||
while (index < item.start) {
|
||||
var remaining = item.start - index;
|
||||
await absorb(remaining);
|
||||
}
|
||||
|
||||
// Next, absorb until we reach the end.
|
||||
if (item.end == -1) {
|
||||
while (enqueued.isNotEmpty) {
|
||||
chunk.add(enqueued.removeFirst());
|
||||
}
|
||||
while (await q.hasNext) {
|
||||
chunk.add(await q.next);
|
||||
}
|
||||
} else {
|
||||
var remaining = item.end - index;
|
||||
chunk.add(await absorb(remaining));
|
||||
}
|
||||
|
||||
// Next, write the boundary and data.
|
||||
ctrl.add(utf8.encode('--$boundary\r\n'));
|
||||
ctrl.add(utf8.encode('Content-Type: $mimeType\r\n'));
|
||||
ctrl.add(utf8.encode(
|
||||
'Content-Range: ${header.rangeUnit} ${item.toContentRange(totalLength)}/$totalLength\r\n\r\n'));
|
||||
ctrl.add(chunk.takeBytes());
|
||||
ctrl.add(const [$cr, $lf]);
|
||||
|
||||
// If this range was unbounded, don't bother looping any further.
|
||||
if (item.end == -1) break;
|
||||
}
|
||||
|
||||
ctrl.add(utf8.encode('--$boundary--\r\n'));
|
||||
|
||||
await ctrl.close();
|
||||
}).catchError((e) {
|
||||
ctrl.addError(e as Object);
|
||||
return null;
|
||||
});
|
||||
|
||||
return ctrl.stream;
|
||||
}
|
||||
}
|
||||
|
||||
var _rnd = Random();
|
||||
String _randomString(
|
||||
{int length = 32,
|
||||
String validChars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'}) {
|
||||
var len = _rnd.nextInt((length - 10)) + 10;
|
||||
var buf = StringBuffer();
|
||||
|
||||
while (buf.length < len) {
|
||||
buf.writeCharCode(validChars.codeUnitAt(_rnd.nextInt(validChars.length)));
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
18
packages/range_header/lib/src/exception.dart
Normal file
18
packages/range_header/lib/src/exception.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
class RangeHeaderParseException extends FormatException {
|
||||
@override
|
||||
final String message;
|
||||
|
||||
RangeHeaderParseException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'Range header parse exception: $message';
|
||||
}
|
||||
|
||||
class InvalidRangeHeaderException implements Exception {
|
||||
final String message;
|
||||
|
||||
InvalidRangeHeaderException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'Range header parse exception: $message';
|
||||
}
|
152
packages/range_header/lib/src/parser.dart
Normal file
152
packages/range_header/lib/src/parser.dart
Normal file
|
@ -0,0 +1,152 @@
|
|||
import 'package:charcode/charcode.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
import 'exception.dart';
|
||||
import 'range_header.dart';
|
||||
import 'range_header_impl.dart';
|
||||
import 'range_header_item.dart';
|
||||
|
||||
final RegExp _rgxInt = RegExp(r'[0-9]+');
|
||||
final RegExp _rgxWs = RegExp(r'[ \n\r\t]');
|
||||
|
||||
enum TokenType { RANGE_UNIT, COMMA, INT, DASH, EQUALS }
|
||||
|
||||
class Token {
|
||||
final TokenType type;
|
||||
final SourceSpan? span;
|
||||
|
||||
Token(this.type, this.span);
|
||||
}
|
||||
|
||||
List<Token> scan(String text, List<String> allowedRangeUnits) {
|
||||
var tokens = <Token>[];
|
||||
var scanner = SpanScanner(text);
|
||||
|
||||
while (!scanner.isDone) {
|
||||
// Skip whitespace
|
||||
scanner.scan(_rgxWs);
|
||||
|
||||
if (scanner.scanChar($comma)) {
|
||||
tokens.add(Token(TokenType.COMMA, scanner.lastSpan));
|
||||
} else if (scanner.scanChar($dash)) {
|
||||
tokens.add(Token(TokenType.DASH, scanner.lastSpan));
|
||||
} else if (scanner.scan(_rgxInt)) {
|
||||
tokens.add(Token(TokenType.INT, scanner.lastSpan));
|
||||
} else if (scanner.scanChar($equal)) {
|
||||
tokens.add(Token(TokenType.EQUALS, scanner.lastSpan));
|
||||
} else {
|
||||
var matched = false;
|
||||
|
||||
for (var unit in allowedRangeUnits) {
|
||||
if (scanner.scan(unit)) {
|
||||
tokens.add(Token(TokenType.RANGE_UNIT, scanner.lastSpan));
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
var ch = scanner.readChar();
|
||||
throw RangeHeaderParseException(
|
||||
'Unexpected character: "${String.fromCharCode(ch)}"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
class Parser {
|
||||
Token? _current;
|
||||
int _index = -1;
|
||||
final List<Token> tokens;
|
||||
|
||||
Parser(this.tokens);
|
||||
|
||||
Token? get current => _current;
|
||||
|
||||
bool get done => _index >= tokens.length - 1;
|
||||
|
||||
RangeHeaderParseException _expected(String type) {
|
||||
var offset = current?.span?.start.offset;
|
||||
|
||||
if (offset == null) return RangeHeaderParseException('Expected $type.');
|
||||
|
||||
Token? peek;
|
||||
|
||||
if (_index < tokens.length - 1) peek = tokens[_index + 1];
|
||||
|
||||
if (peek != null && peek.span != null) {
|
||||
return RangeHeaderParseException(
|
||||
'Expected $type at offset $offset, found "${peek.span!.text}" instead. \nSource:\n${peek.span?.highlight() ?? peek.type}');
|
||||
} else {
|
||||
return RangeHeaderParseException(
|
||||
'Expected $type at offset $offset, but the header string ended without one.\nSource:\n${current!.span?.highlight() ?? current!.type}');
|
||||
}
|
||||
}
|
||||
|
||||
bool next(TokenType type) {
|
||||
if (done) return false;
|
||||
var tok = tokens[_index + 1];
|
||||
if (tok.type == type) {
|
||||
_index++;
|
||||
_current = tok;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RangeHeader? parseRangeHeader() {
|
||||
if (next(TokenType.RANGE_UNIT)) {
|
||||
var unit = current!.span!.text;
|
||||
next(TokenType.EQUALS); // Consume =, if any.
|
||||
|
||||
var items = <RangeHeaderItem>[];
|
||||
var item = parseHeaderItem();
|
||||
|
||||
while (item != null) {
|
||||
items.add(item);
|
||||
// Parse comma
|
||||
if (next(TokenType.COMMA)) {
|
||||
item = parseHeaderItem();
|
||||
} else {
|
||||
item = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (items.isEmpty) {
|
||||
throw _expected('range');
|
||||
} else {
|
||||
return RangeHeaderImpl(unit, items);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
RangeHeaderItem? parseHeaderItem() {
|
||||
if (next(TokenType.INT)) {
|
||||
// i.e 500-544, or 600-
|
||||
var start = int.parse(current!.span!.text);
|
||||
if (next(TokenType.DASH)) {
|
||||
if (next(TokenType.INT)) {
|
||||
return RangeHeaderItem(start, int.parse(current!.span!.text));
|
||||
} else {
|
||||
return RangeHeaderItem(start);
|
||||
}
|
||||
} else {
|
||||
throw _expected('"-"');
|
||||
}
|
||||
} else if (next(TokenType.DASH)) {
|
||||
// i.e. -599
|
||||
if (next(TokenType.INT)) {
|
||||
return RangeHeaderItem(-1, int.parse(current!.span!.text));
|
||||
} else {
|
||||
throw _expected('integer');
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
68
packages/range_header/lib/src/range_header.dart
Normal file
68
packages/range_header/lib/src/range_header.dart
Normal file
|
@ -0,0 +1,68 @@
|
|||
import 'dart:collection';
|
||||
import 'exception.dart';
|
||||
import 'parser.dart';
|
||||
import 'range_header_item.dart';
|
||||
import 'range_header_impl.dart';
|
||||
|
||||
/// Represents the contents of a parsed `Range` header.
|
||||
abstract class RangeHeader {
|
||||
/// Returns an immutable list of the ranges that were parsed.
|
||||
UnmodifiableListView<RangeHeaderItem> get items;
|
||||
|
||||
const factory RangeHeader(Iterable<RangeHeaderItem> items,
|
||||
{String? rangeUnit}) = _ConstantRangeHeader;
|
||||
|
||||
/// Eliminates any overlapping [items], sorts them, and folds them all into the most efficient representation possible.
|
||||
static UnmodifiableListView<RangeHeaderItem> foldItems(
|
||||
Iterable<RangeHeaderItem> items) {
|
||||
var out = <RangeHeaderItem>{};
|
||||
|
||||
for (var item in items) {
|
||||
// Remove any overlapping items, consolidate them.
|
||||
while (out.any((x) => x.overlaps(item))) {
|
||||
var f = out.firstWhere((x) => x.overlaps(item));
|
||||
out.remove(f);
|
||||
item = item.consolidate(f);
|
||||
}
|
||||
|
||||
out.add(item);
|
||||
}
|
||||
|
||||
return UnmodifiableListView(out.toList()..sort());
|
||||
}
|
||||
|
||||
/// Attempts to parse a [RangeHeader] from its [text] representation.
|
||||
///
|
||||
/// You can optionally pass a custom list of [allowedRangeUnits].
|
||||
/// The default is `['bytes']`.
|
||||
///
|
||||
/// If [fold] is `true`, the items will be folded into the most compact
|
||||
/// possible representation.
|
||||
///
|
||||
factory RangeHeader.parse(String text,
|
||||
{Iterable<String>? allowedRangeUnits, bool fold = true}) {
|
||||
var tokens = scan(text, allowedRangeUnits?.toList() ?? ['bytes']);
|
||||
var parser = Parser(tokens);
|
||||
var header = parser.parseRangeHeader();
|
||||
if (header == null) {
|
||||
throw InvalidRangeHeaderException('Header is null');
|
||||
}
|
||||
var items = foldItems(header.items);
|
||||
return RangeHeaderImpl(header.rangeUnit, items);
|
||||
}
|
||||
|
||||
/// Returns this header's range unit. Most commonly, this is `bytes`.
|
||||
String? get rangeUnit;
|
||||
}
|
||||
|
||||
class _ConstantRangeHeader implements RangeHeader {
|
||||
final Iterable<RangeHeaderItem> items_;
|
||||
@override
|
||||
final String? rangeUnit;
|
||||
|
||||
const _ConstantRangeHeader(this.items_, {this.rangeUnit = 'bytes'});
|
||||
|
||||
@override
|
||||
UnmodifiableListView<RangeHeaderItem> get items =>
|
||||
UnmodifiableListView(items_);
|
||||
}
|
20
packages/range_header/lib/src/range_header_impl.dart
Normal file
20
packages/range_header/lib/src/range_header_impl.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
import 'dart:collection';
|
||||
import 'range_header.dart';
|
||||
import 'range_header_item.dart';
|
||||
|
||||
/// Represents the contents of a parsed `Range` header.
|
||||
class RangeHeaderImpl implements RangeHeader {
|
||||
UnmodifiableListView<RangeHeaderItem>? _cached;
|
||||
final List<RangeHeaderItem> _items = [];
|
||||
|
||||
RangeHeaderImpl(this.rangeUnit, [List<RangeHeaderItem> items = const []]) {
|
||||
_items.addAll(items);
|
||||
}
|
||||
|
||||
@override
|
||||
UnmodifiableListView<RangeHeaderItem> get items =>
|
||||
_cached ??= UnmodifiableListView<RangeHeaderItem>(_items);
|
||||
|
||||
@override
|
||||
final String? rangeUnit;
|
||||
}
|
93
packages/range_header/lib/src/range_header_item.dart
Normal file
93
packages/range_header/lib/src/range_header_item.dart
Normal file
|
@ -0,0 +1,93 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
/// Represents an individual range, with an optional start index and optional end index.
|
||||
class RangeHeaderItem implements Comparable<RangeHeaderItem> {
|
||||
/// The index at which this chunk begins. May be `-1`.
|
||||
final int start;
|
||||
|
||||
/// The index at which this chunk ends. May be `-1`.
|
||||
final int end;
|
||||
|
||||
const RangeHeaderItem([this.start = -1, this.end = -1]);
|
||||
|
||||
/// Joins two items together into the largest possible range.
|
||||
RangeHeaderItem consolidate(RangeHeaderItem other) {
|
||||
if (!(other.overlaps(this))) {
|
||||
throw ArgumentError('The two ranges do not overlap.');
|
||||
}
|
||||
return RangeHeaderItem(min(start, other.start), max(end, other.end));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hash2(start, end);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is RangeHeaderItem && other.start == start && other.end == end;
|
||||
|
||||
bool overlaps(RangeHeaderItem other) {
|
||||
if (other.start <= start) {
|
||||
return other.end < start;
|
||||
} else if (other.start > start) {
|
||||
return other.start <= end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int compareTo(RangeHeaderItem other) {
|
||||
if (other.start > start) {
|
||||
return -1;
|
||||
} else if (other.start == start) {
|
||||
if (other.end == end) {
|
||||
return 0;
|
||||
} else if (other.end < end) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (other.start < start) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (start > -1 && end > -1) {
|
||||
return '$start-$end';
|
||||
} else if (start > -1) {
|
||||
return '$start-';
|
||||
} else {
|
||||
return '-$end';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a representation of this instance suitable for a `Content-Range` header.
|
||||
///
|
||||
/// This can only be used if the user request only one range. If not, send a
|
||||
/// `multipart/byteranges` response.
|
||||
///
|
||||
/// Please adhere to the standard!!!
|
||||
/// http://httpwg.org/specs/rfc7233.html
|
||||
|
||||
String toContentRange([int? totalSize]) {
|
||||
// var maxIndex = totalSize != null ? (totalSize - 1).toString() : '*';
|
||||
var s = start > -1 ? start : 0;
|
||||
|
||||
if (end == -1) {
|
||||
if (totalSize == null) {
|
||||
throw UnsupportedError(
|
||||
'If the end of this range is unknown, `totalSize` must not be null.');
|
||||
} else {
|
||||
// if (end == totalSize - 1) {
|
||||
return '$s-${totalSize - 1}/$totalSize';
|
||||
}
|
||||
}
|
||||
|
||||
return '$s-$end/$totalSize';
|
||||
}
|
||||
}
|
18
packages/range_header/pubspec.yaml
Normal file
18
packages/range_header/pubspec.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
name: belatuk_range_header
|
||||
version: 4.0.0
|
||||
description: Range header parser for Dart. Beyond parsing, a stream transformer is included.
|
||||
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/range_header
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
async: ^2.6.0
|
||||
charcode: ^1.2.0
|
||||
quiver: ^3.0.1
|
||||
source_span: ^1.8.1
|
||||
string_scanner: ^1.1.0
|
||||
dev_dependencies:
|
||||
file: ^6.1.0
|
||||
http_parser: ^4.0.0
|
||||
logging: ^1.0.1
|
||||
test: ^1.17.8
|
||||
lints: ^1.0.0
|
95
packages/range_header/test/all_test.dart
Normal file
95
packages/range_header/test/all_test.dart
Normal file
|
@ -0,0 +1,95 @@
|
|||
import 'package:angel3_range_header/angel3_range_header.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final Matcher throwsRangeParseException =
|
||||
throwsA(const TypeMatcher<RangeHeaderParseException>());
|
||||
|
||||
final Matcher throwsInvalidRangeHeaderException =
|
||||
throwsA(const TypeMatcher<InvalidRangeHeaderException>());
|
||||
|
||||
void main() {
|
||||
group('one item', () {
|
||||
test('start and end', () {
|
||||
var r = RangeHeader.parse('bytes 1-200');
|
||||
expect(r.items, hasLength(1));
|
||||
expect(r.items.first.start, 1);
|
||||
expect(r.items.first.end, 200);
|
||||
});
|
||||
|
||||
test('start only', () {
|
||||
var r = RangeHeader.parse('bytes 1-');
|
||||
expect(r.items, hasLength(1));
|
||||
expect(r.items.first.start, 1);
|
||||
expect(r.items.first.end, -1);
|
||||
});
|
||||
|
||||
test('end only', () {
|
||||
var r = RangeHeader.parse('bytes -200');
|
||||
print(r.items);
|
||||
expect(r.items, hasLength(1));
|
||||
expect(r.items.first.start, -1);
|
||||
expect(r.items.first.end, 200);
|
||||
});
|
||||
});
|
||||
|
||||
group('multiple items', () {
|
||||
test('three items', () {
|
||||
var r = RangeHeader.parse('bytes 1-20, 21-40, 41-60');
|
||||
print(r.items);
|
||||
expect(r.items, hasLength(3));
|
||||
expect(r.items[0].start, 1);
|
||||
expect(r.items[0].end, 20);
|
||||
expect(r.items[1].start, 21);
|
||||
expect(r.items[1].end, 40);
|
||||
expect(r.items[2].start, 41);
|
||||
expect(r.items[2].end, 60);
|
||||
});
|
||||
|
||||
test('one item without end', () {
|
||||
var r = RangeHeader.parse('bytes 1-20, 21-');
|
||||
print(r.items);
|
||||
expect(r.items, hasLength(2));
|
||||
expect(r.items[0].start, 1);
|
||||
expect(r.items[0].end, 20);
|
||||
expect(r.items[1].start, 21);
|
||||
expect(r.items[1].end, -1);
|
||||
});
|
||||
});
|
||||
|
||||
group('failures', () {
|
||||
test('no start with no end', () {
|
||||
expect(() => RangeHeader.parse('-'), throwsInvalidRangeHeaderException);
|
||||
});
|
||||
});
|
||||
|
||||
group('exceptions', () {
|
||||
test('invalid character', () {
|
||||
expect(() => RangeHeader.parse('!!!'), throwsRangeParseException);
|
||||
});
|
||||
|
||||
test('no ranges', () {
|
||||
expect(() => RangeHeader.parse('bytes'), throwsRangeParseException);
|
||||
});
|
||||
|
||||
test('no dash after int', () {
|
||||
expect(() => RangeHeader.parse('bytes 3'), throwsRangeParseException);
|
||||
expect(() => RangeHeader.parse('bytes 3,'), throwsRangeParseException);
|
||||
expect(() => RangeHeader.parse('bytes 3 24'), throwsRangeParseException);
|
||||
});
|
||||
|
||||
test('no int after dash', () {
|
||||
expect(() => RangeHeader.parse('bytes -,'), throwsRangeParseException);
|
||||
});
|
||||
});
|
||||
|
||||
group('complete coverage', () {
|
||||
test('exception toString()', () {
|
||||
var m = RangeHeaderParseException('hey');
|
||||
expect(m.toString(), contains('hey'));
|
||||
});
|
||||
});
|
||||
|
||||
test('content-range', () {
|
||||
expect(RangeHeader.parse('bytes 1-2').items[0].toContentRange(3), '1-2/3');
|
||||
});
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
# Belatuk Symbol Table
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/belatuk_symbol_table)
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dev/packages/belatuk_symbol_table)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/symbol_table/LICENSE)
|
||||
|
||||
|
|
1
packages/user_agent/.travis.yml
Normal file
1
packages/user_agent/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
12
packages/user_agent/AUTHORS.md
Normal file
12
packages/user_agent/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
22
packages/user_agent/CHANGELOG.md
Normal file
22
packages/user_agent/CHANGELOG.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Change Log
|
||||
|
||||
## 3.0.2
|
||||
|
||||
* Upgraded from `pendantic` to `lints` linter
|
||||
* Fixed linter warnings
|
||||
|
||||
## 3.0.1
|
||||
|
||||
* Updated to use non nullable results
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Dart 2 updates.
|
||||
|
||||
## 0.0.1
|
||||
|
||||
* Initial version, created by Stagehand
|
29
packages/user_agent/LICENSE
Normal file
29
packages/user_agent/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
packages/user_agent/README.md
Normal file
26
packages/user_agent/README.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# User Agent Analyzer
|
||||
|
||||
[![version](https://img.shields.io/badge/pub-v3.0.2-brightgreen)](https://pub.dev/packages/user_agent_analyzer)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/packages/user_agent/LICENSE)
|
||||
|
||||
**Replacement of `package:user_agent` with breaking changes to support NNBD.**
|
||||
|
||||
A library to identify the type of devices and web browsers based on `User-Agent` string.
|
||||
|
||||
Runs anywhere.
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
app.get('/', (req, res) async {
|
||||
var ua = UserAgent(req.headers.value('user-agent'));
|
||||
|
||||
if (ua.isChrome) {
|
||||
res.redirect('/upgrade-your-browser');
|
||||
return;
|
||||
} else {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
1
packages/user_agent/analysis_options.yaml
Normal file
1
packages/user_agent/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
7
packages/user_agent/example/example.dart
Normal file
7
packages/user_agent/example/example.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'package:user_agent_analyzer/user_agent_analyzer.dart';
|
||||
|
||||
void main() {
|
||||
var ua = UserAgent(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36');
|
||||
print(ua.isChrome);
|
||||
}
|
235
packages/user_agent/lib/user_agent_analyzer.dart
Normal file
235
packages/user_agent/lib/user_agent_analyzer.dart
Normal file
|
@ -0,0 +1,235 @@
|
|||
library user_agent_analyzer;
|
||||
|
||||
/// Utils for device detection.
|
||||
class UserAgent {
|
||||
bool _isChrome = false;
|
||||
bool _isOpera = false;
|
||||
bool _isIE = false;
|
||||
bool _isFirefox = false;
|
||||
bool _isWebKit = false;
|
||||
String? _cachedCssPrefix;
|
||||
String? _cachedPropertyPrefix;
|
||||
|
||||
final String value, _lowerValue;
|
||||
|
||||
static const List<String> knownMobileUserAgentPrefixes = [
|
||||
'w3c ',
|
||||
'w3c-',
|
||||
'acs-',
|
||||
'alav',
|
||||
'alca',
|
||||
'amoi',
|
||||
'audi',
|
||||
'avan',
|
||||
'benq',
|
||||
'bird',
|
||||
'blac',
|
||||
'blaz',
|
||||
'brew',
|
||||
'cell',
|
||||
'cldc',
|
||||
'cmd-',
|
||||
'dang',
|
||||
'doco',
|
||||
'eric',
|
||||
'hipt',
|
||||
'htc_',
|
||||
'inno',
|
||||
'ipaq',
|
||||
'ipod',
|
||||
'jigs',
|
||||
'kddi',
|
||||
'keji',
|
||||
'leno',
|
||||
'lg-c',
|
||||
'lg-d',
|
||||
'lg-g',
|
||||
'lge-',
|
||||
'lg/u',
|
||||
'maui',
|
||||
'maxo',
|
||||
'midp',
|
||||
'mits',
|
||||
'mmef',
|
||||
'mobi',
|
||||
'mot-',
|
||||
'moto',
|
||||
'mwbp',
|
||||
'nec-',
|
||||
'newt',
|
||||
'noki',
|
||||
'palm',
|
||||
'pana',
|
||||
'pant',
|
||||
'phil',
|
||||
'play',
|
||||
'port',
|
||||
'prox',
|
||||
'qwap',
|
||||
'sage',
|
||||
'sams',
|
||||
'sany',
|
||||
'sch-',
|
||||
'sec-',
|
||||
'send',
|
||||
'seri',
|
||||
'sgh-',
|
||||
'shar',
|
||||
'sie-',
|
||||
'siem',
|
||||
'smal',
|
||||
'smar',
|
||||
'sony',
|
||||
'sph-',
|
||||
'symb',
|
||||
't-mo',
|
||||
'teli',
|
||||
'tim-',
|
||||
'tosh',
|
||||
'tsm-',
|
||||
'upg1',
|
||||
'upsi',
|
||||
'vk-v',
|
||||
'voda',
|
||||
'wap-',
|
||||
'wapa',
|
||||
'wapi',
|
||||
'wapp',
|
||||
'wapr',
|
||||
'webc',
|
||||
'winw',
|
||||
'winw',
|
||||
'xda ',
|
||||
'xda-'
|
||||
];
|
||||
|
||||
static const List<String> knownMobileUserAgentKeywords = [
|
||||
'blackberry',
|
||||
'webos',
|
||||
'ipod',
|
||||
'lge vx',
|
||||
'midp',
|
||||
'maemo',
|
||||
'mmp',
|
||||
'mobile',
|
||||
'netfront',
|
||||
'hiptop',
|
||||
'nintendo DS',
|
||||
'novarra',
|
||||
'openweb',
|
||||
'opera mobi',
|
||||
'opera mini',
|
||||
'palm',
|
||||
'psp',
|
||||
'phone',
|
||||
'smartphone',
|
||||
'symbian',
|
||||
'up.browser',
|
||||
'up.link',
|
||||
'wap',
|
||||
'windows ce'
|
||||
];
|
||||
|
||||
static const List<String> knownTabletUserAgentKeywords = [
|
||||
'ipad',
|
||||
'playbook',
|
||||
'hp-tablet',
|
||||
'kindle'
|
||||
];
|
||||
|
||||
UserAgent(this.value) : _lowerValue = value.toLowerCase();
|
||||
|
||||
/// Determines if the user agent string contains the desired string. Case-insensitive.
|
||||
bool contains(String needle) => _lowerValue.contains(needle.toLowerCase());
|
||||
|
||||
bool get isDesktop => isMacOS || (!isMobile && !isTablet);
|
||||
|
||||
bool get isTablet => knownTabletUserAgentKeywords.any(contains);
|
||||
|
||||
bool get isMobile => knownMobileUserAgentKeywords.any(contains);
|
||||
|
||||
bool get isMacOS => contains('Macintosh') || contains('Mac OS X');
|
||||
|
||||
bool get isSafari => contains('Safari');
|
||||
|
||||
bool get isAndroid => contains('android');
|
||||
|
||||
bool get isAndroidPhone => contains('android') && contains('mobile');
|
||||
|
||||
bool get isAndroidTablet => contains('android') && !contains('mobile');
|
||||
|
||||
bool get isWindows => contains('windows');
|
||||
|
||||
bool get isWindowsPhone => isWindows && contains('phone');
|
||||
|
||||
bool get isWindowsTablet => isWindows && contains('touch');
|
||||
|
||||
bool get isBlackberry =>
|
||||
contains('blackberry') || contains('bb10') || contains('rim');
|
||||
|
||||
bool get isBlackberryPhone => isBlackberry && !contains('tablet');
|
||||
|
||||
bool get isBlackberryTablet => isBlackberry && contains('tablet');
|
||||
|
||||
/// Determines if the current device is running Chrome.
|
||||
bool get isChrome {
|
||||
_isChrome = value.contains('Chrome', 0);
|
||||
return _isChrome;
|
||||
}
|
||||
|
||||
/// Determines if the current device is running Opera.
|
||||
bool get isOpera {
|
||||
_isOpera = value.contains('Opera', 0);
|
||||
return _isOpera;
|
||||
}
|
||||
|
||||
/// Determines if the current device is running Internet Explorer.
|
||||
bool get isIE {
|
||||
_isIE = !isOpera && value.contains('Trident/', 0);
|
||||
return _isIE;
|
||||
}
|
||||
|
||||
/// Determines if the current device is running Firefox.
|
||||
bool get isFirefox {
|
||||
_isFirefox = value.contains('Firefox', 0);
|
||||
return _isFirefox;
|
||||
}
|
||||
|
||||
/// Determines if the current device is running WebKit.
|
||||
bool get isWebKit {
|
||||
_isWebKit = !isOpera && value.contains('WebKit', 0);
|
||||
return _isWebKit;
|
||||
}
|
||||
|
||||
/// Gets the CSS property prefix for the current platform.
|
||||
String get cssPrefix {
|
||||
var prefix = _cachedCssPrefix;
|
||||
if (prefix != null) return prefix;
|
||||
if (isFirefox) {
|
||||
prefix = '-moz-';
|
||||
} else if (isIE) {
|
||||
prefix = '-ms-';
|
||||
} else if (isOpera) {
|
||||
prefix = '-o-';
|
||||
} else {
|
||||
prefix = '-webkit-';
|
||||
}
|
||||
return _cachedCssPrefix = prefix;
|
||||
}
|
||||
|
||||
/// Prefix as used for JS property names.
|
||||
String get propertyPrefix {
|
||||
var prefix = _cachedPropertyPrefix;
|
||||
if (prefix != null) return prefix;
|
||||
if (isFirefox) {
|
||||
prefix = 'moz';
|
||||
} else if (isIE) {
|
||||
prefix = 'ms';
|
||||
} else if (isOpera) {
|
||||
prefix = 'o';
|
||||
} else {
|
||||
prefix = 'webkit';
|
||||
}
|
||||
return _cachedPropertyPrefix = prefix;
|
||||
}
|
||||
}
|
9
packages/user_agent/pubspec.yaml
Normal file
9
packages/user_agent/pubspec.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
name: user_agent_analyzer
|
||||
version: 3.0.2
|
||||
description: A library to identify the type of devices and web browsers based on User-Agent string.
|
||||
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/user_agent
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dev_dependencies:
|
||||
test: ^1.17.8
|
||||
lints: ^1.0.0
|
30
packages/user_agent/test/user_agent_test.dart
Normal file
30
packages/user_agent/test/user_agent_test.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2017, thosakwe. All rights reserved. Use of this source code
|
||||
// is governed by a BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:user_agent_analyzer/user_agent_analyzer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('chrome', () {
|
||||
var ua = UserAgent(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36');
|
||||
expect([ua.isChrome, ua.isWebKit, ua.isSafari, ua.isDesktop, ua.isMacOS],
|
||||
everyElement(isTrue));
|
||||
expect([ua.isFirefox, ua.isIE, ua.isOpera, ua.isMobile, ua.isTablet],
|
||||
everyElement(isFalse));
|
||||
expect([
|
||||
ua.isAndroid,
|
||||
ua.isAndroidPhone,
|
||||
ua.isAndroidTablet,
|
||||
ua.isBlackberry,
|
||||
ua.isBlackberryPhone,
|
||||
ua.isBlackberryTablet,
|
||||
ua.isWindows,
|
||||
ua.isWindowsPhone,
|
||||
ua.isWindowsTablet
|
||||
], everyElement(isFalse));
|
||||
|
||||
expect(ua.cssPrefix, equals('-webkit-'));
|
||||
expect(ua.propertyPrefix, equals('webkit'));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue