Update flutter/http.dart to use dart:io. (#5940)

This commit is contained in:
Chinmay Garde 2016-09-20 14:17:33 -07:00 committed by GitHub
parent 4f0eff31bc
commit 400585cb96
25 changed files with 1697 additions and 479 deletions

View file

@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/http.dart' as http;
import 'package:mojo/core.dart' as core;
// All of these sounds are marked as public domain at soundbible.
const String chimes = "http://soundbible.com/grab.php?id=2030&type=wav";
@ -42,7 +43,10 @@ class PianoKey {
Future<Null> load(mojom.MediaServiceProxy mediaService) async {
try {
mediaService.createPlayer(player);
player.prepare(await http.readDataPipe(soundUrl), (bool ignored) { });
http.Response response = await http.get(soundUrl);
core.MojoDataPipe pipe = new core.MojoDataPipe();
core.DataPipeFiller.fillHandle(pipe.producer, response.bodyBytes.buffer.asByteData());
player.prepare(pipe.consumer, (bool ignored) { });
} catch (e) {
print("Error: failed to load sound file $soundUrl");
player.close();

View file

@ -3,16 +3,18 @@
// found in the LICENSE file.
/// A [Future]-based library for making HTTP requests.
///
///
/// To use, import `package:flutter/http.dart`.
///
/// This library is based on Dart's `http` package, but differs in that it is a
/// `mojo`-based HTTP client and does not have a dependency on mirrors.
/// This library is based on Dart's `http` package, but differs in that it does
/// not have a dependency on mirrors.
///
/// This library depends only on core Dart libraries as well as the `mojo`,
/// `mojo_services`, and `sky_services` packages.
// TODO(chinmaygarde): The contents of `lib/src/http` will become redundant
// once https://github.com/dart-lang/http/issues/1 is fixed (removes the use
// of mirrors). Once that issue is addressed, we should get rid this directory
// and use `dart-lang/http` directly.
library http;
export 'src/http/http.dart';
export 'src/http/mojo_client.dart';
export 'src/http/response.dart';
export 'src/http/mock_client.dart';

View file

@ -0,0 +1,7 @@
`dart-lang/http` without Mirrors
================================
The contents of this will become redundant once
https://github.com/dart-lang/http/issues/1 is fixed (removes the use
of mirrors). Once that issue is addressed, we should get rid this directory
and use `dart-lang/http` directly.

View file

@ -0,0 +1,199 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'base_request.dart';
import 'client.dart';
import 'exception.dart';
import 'request.dart';
import 'response.dart';
import 'streamed_response.dart';
/// The abstract base class for an HTTP client. This is a mixin-style class;
/// subclasses only need to implement [send] and maybe [close], and then they
/// get various convenience methods for free.
abstract class BaseClient implements Client {
/// Sends an HTTP HEAD request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// For more fine-grained control over the request, use [send] instead.
@override
Future<Response> head(dynamic url, {Map<String, String> headers}) =>
_sendUnstreamed("HEAD", url, headers);
/// Sends an HTTP GET request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// For more fine-grained control over the request, use [send] instead.
@override
Future<Response> get(dynamic url, {Map<String, String> headers}) =>
_sendUnstreamed("GET", url, headers);
/// Sends an HTTP POST request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>]
/// or a [Map<String, String>]. If it's a String, it's encoded using
/// [encoding] and used as the body of the request. The content-type of the
/// request will default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to UTF-8.
///
/// For more fine-grained control over the request, use [send] instead.
@override
Future<Response> post(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding}) =>
_sendUnstreamed("POST", url, headers, body, encoding);
/// Sends an HTTP PUT request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>]
/// or a [Map<String, String>]. If it's a String, it's encoded using
/// [encoding] and used as the body of the request. The content-type of the
/// request will default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to UTF-8.
///
/// For more fine-grained control over the request, use [send] instead.
@override
Future<Response> put(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding}) =>
_sendUnstreamed("PUT", url, headers, body, encoding);
/// Sends an HTTP PATCH request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>]
/// or a [Map<String, String>]. If it's a String, it's encoded using
/// [encoding] and used as the body of the request. The content-type of the
/// request will default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to UTF-8.
///
/// For more fine-grained control over the request, use [send] instead.
@override
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding}) =>
_sendUnstreamed("PATCH", url, headers, body, encoding);
/// Sends an HTTP DELETE request with the given headers to the given URL,
/// which can be a [Uri] or a [String].
///
/// For more fine-grained control over the request, use [send] instead.
@override
Future<Response> delete(dynamic url, {Map<String, String> headers}) =>
_sendUnstreamed("DELETE", url, headers);
/// Sends an HTTP GET request with the given headers to the given URL, which
/// can be a [Uri] or a [String], and returns a Future that completes to the
/// body of the response as a String.
///
/// The Future will emit a [ClientException] if the response doesn't have a
/// success status code.
///
/// For more fine-grained control over the request and response, use [send] or
/// [get] instead.
@override
Future<String> read(dynamic url, {Map<String, String> headers}) {
return get(url, headers: headers).then((Response response) {
_checkResponseSuccess(url, response);
return response.body;
});
}
/// Sends an HTTP GET request with the given headers to the given URL, which
/// can be a [Uri] or a [String], and returns a Future that completes to the
/// body of the response as a list of bytes.
///
/// The Future will emit an [ClientException] if the response doesn't have a
/// success status code.
///
/// For more fine-grained control over the request and response, use [send] or
/// [get] instead.
@override
Future<Uint8List> readBytes(dynamic url, {Map<String, String> headers}) {
return get(url, headers: headers).then((Response response) {
_checkResponseSuccess(url, response);
return response.bodyBytes;
});
}
/// Sends an HTTP request and asynchronously returns the response.
///
/// Implementers should call [BaseRequest.finalize] to get the body of the
/// request as a [ByteStream]. They shouldn't make any assumptions about the
/// state of the stream; it could have data written to it asynchronously at a
/// later point, or it could already be closed when it's returned. Any
/// internal HTTP errors should be wrapped as [ClientException]s.
@override
Future<StreamedResponse> send(BaseRequest request);
/// Sends a non-streaming [Request] and returns a non-streaming [Response].
Future<Response> _sendUnstreamed(String method, dynamic url,
Map<String, String> headers, [dynamic body, Encoding encoding]) async {
if (url is String) url = Uri.parse(url);
Request request = new Request(method, url);
if (headers != null) request.headers.addAll(headers);
if (encoding != null) request.encoding = encoding;
if (body != null) {
if (body is String) {
request.body = body;
} else if (body is List) {
request.bodyBytes = DelegatingList.typed(body);
} else if (body is Map) {
request.bodyFields = DelegatingMap.typed(body);
} else {
throw new ArgumentError('Invalid request body "$body".');
}
}
return Response.fromStream(await send(request));
}
/// Throws an error if [response] is not successful.
void _checkResponseSuccess(dynamic url, Response response) {
if (response.statusCode < 400) return;
String message = "Request to $url failed with status ${response.statusCode}";
if (response.reasonPhrase != null) {
message = "$message: ${response.reasonPhrase}";
}
if (url is String) url = Uri.parse(url);
throw new ClientException("$message.", url);
}
/// Closes the client and cleans up any resources associated with it. It's
/// important to close each client when it's done being used; failing to do so
/// can cause the Dart process to hang.
@override
void close() {}
}

View file

@ -0,0 +1,141 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'byte_stream.dart';
import 'client.dart';
import 'streamed_response.dart';
import 'utils.dart';
/// The base class for HTTP requests.
///
/// Subclasses of [BaseRequest] can be constructed manually and passed to
/// [BaseClient.send], which allows the user to provide fine-grained control
/// over the request properties. However, usually it's easier to use convenience
/// methods like [get] or [BaseClient.get].
abstract class BaseRequest {
/// The HTTP method of the request. Most commonly "GET" or "POST", less
/// commonly "HEAD", "PUT", or "DELETE". Non-standard method names are also
/// supported.
final String method;
/// The URL to which the request will be sent.
final Uri url;
/// Creates a new HTTP request.
BaseRequest(this.method, this.url)
: headers = new LinkedHashMap<String, String>(
equals: (String key1, String key2) => key1.toLowerCase() == key2.toLowerCase(),
hashCode: (String key) => key.toLowerCase().hashCode);
/// The size of the request body, in bytes.
///
/// This defaults to `null`, which indicates that the size of the request is
/// not known in advance.
int get contentLength => _contentLength;
int _contentLength;
set contentLength(int value) {
if (value != null && value < 0) {
throw new ArgumentError("Invalid content length $value.");
}
_checkFinalized();
_contentLength = value;
}
/// Whether a persistent connection should be maintained with the server.
/// Defaults to true.
bool get persistentConnection => _persistentConnection;
bool _persistentConnection = true;
set persistentConnection(bool value) {
_checkFinalized();
_persistentConnection = value;
}
/// Whether the client should follow redirects while resolving this request.
/// Defaults to true.
bool get followRedirects => _followRedirects;
bool _followRedirects = true;
set followRedirects(bool value) {
_checkFinalized();
_followRedirects = value;
}
/// The maximum number of redirects to follow when [followRedirects] is true.
/// If this number is exceeded the [BaseResponse] future will signal a
/// [RedirectException]. Defaults to 5.
int get maxRedirects => _maxRedirects;
int _maxRedirects = 5;
set maxRedirects(int value) {
_checkFinalized();
_maxRedirects = value;
}
// TODO(nweiz): automatically parse cookies from headers
// TODO(nweiz): make this a HttpHeaders object
/// The headers for this request.
final Map<String, String> headers;
/// Whether the request has been finalized.
bool get finalized => _finalized;
bool _finalized = false;
/// Finalizes the HTTP request in preparation for it being sent. This freezes
/// all mutable fields and returns a single-subscription [ByteStream] that
/// emits the body of the request.
///
/// The base implementation of this returns null rather than a [ByteStream];
/// subclasses are responsible for creating the return value, which should be
/// single-subscription to ensure that no data is dropped. They should also
/// freeze any additional mutable fields they add that don't make sense to
/// change after the request headers are sent.
ByteStream finalize() {
// TODO(nweiz): freeze headers
if (finalized) throw new StateError("Can't finalize a finalized Request.");
_finalized = true;
return null;
}
/// Sends this request.
///
/// This automatically initializes a new [Client] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [Client] for all of those
/// requests.
Future<StreamedResponse> send() async {
Client client = new Client();
try {
StreamedResponse response = await client.send(this);
Stream<dynamic> stream = onDone(response.stream, client.close);
return new StreamedResponse(
new ByteStream(stream),
response.statusCode,
contentLength: response.contentLength,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase);
} catch (ex) {
client.close();
rethrow;
}
}
// Throws an error if this request has been finalized.
void _checkFinalized() {
if (!finalized) return;
throw new StateError("Can't modify a finalized Request.");
}
@override
String toString() => "$method $url";
}

View file

@ -0,0 +1,53 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'base_request.dart';
/// The base class for HTTP responses.
///
/// Subclasses of [BaseResponse] are usually not constructed manually; instead,
/// they're returned by [BaseClient.send] or other HTTP client methods.
abstract class BaseResponse {
/// The (frozen) request that triggered this response.
final BaseRequest request;
/// The status code of the response.
final int statusCode;
/// The reason phrase associated with the status code.
final String reasonPhrase;
/// The size of the response body, in bytes.
///
/// If the size of the request is not known in advance, this is `null`.
final int contentLength;
// TODO(nweiz): automatically parse cookies from headers
// TODO(nweiz): make this a HttpHeaders object.
/// The headers for this response.
final Map<String, String> headers;
/// Whether this response is a redirect.
final bool isRedirect;
/// Whether the server requested that a persistent connection be maintained.
final bool persistentConnection;
/// Creates a new HTTP response.
BaseResponse(
this.statusCode,
{this.contentLength,
this.request,
this.headers: const <String, String>{},
this.isRedirect: false,
this.persistentConnection: true,
this.reasonPhrase}) {
if (statusCode < 100) {
throw new ArgumentError("Invalid status code $statusCode.");
} else if (contentLength != null && contentLength < 0) {
throw new ArgumentError("Invalid content length $contentLength.");
}
}
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
/// A stream of chunks of bytes representing a single piece of data.
class ByteStream extends StreamView<List<int>> {
ByteStream(Stream<List<int>> stream)
: super(stream);
/// Returns a single-subscription byte stream that will emit the given bytes
/// in a single chunk.
factory ByteStream.fromBytes(List<int> bytes) =>
new ByteStream(new Stream<dynamic>.fromIterable(<List<int>>[bytes]));
/// Collects the data of this stream in a [Uint8List].
Future<Uint8List> toBytes() {
Completer<Uint8List> completer = new Completer<Uint8List>();
dynamic sink = new ByteConversionSink.withCallback((dynamic bytes) =>
completer.complete(new Uint8List.fromList(bytes)));
listen(sink.add, onError: completer.completeError, onDone: sink.close,
cancelOnError: true);
return completer.future;
}
/// Collect the data of this stream in a [String], decoded according to
/// [encoding], which defaults to `UTF8`.
Future<String> bytesToString([Encoding encoding=UTF8]) =>
encoding.decodeStream(this);
Stream<String> toStringStream([Encoding encoding=UTF8]) =>
encoding.decoder.bind(this);
}

View file

@ -0,0 +1,148 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'base_client.dart';
import 'base_request.dart';
import 'io_client.dart';
import 'response.dart';
import 'streamed_response.dart';
typedef Client ClientOverride();
/// The interface for HTTP clients that take care of maintaining persistent
/// connections across multiple requests to the same server. If you only need to
/// send a single request, it's usually easier to use [head], [get], [post],
/// [put], [patch], or [delete] instead.
///
/// When creating an HTTP client class with additional functionality, you must
/// extend [BaseClient] rather than [Client]. In most cases, you can wrap
/// another instance of [Client] and add functionality on top of that. This
/// allows all classes implementing [Client] to be mutually composable.
abstract class Client {
/// Creates a new client.
///
/// Currently this will create an [IOClient] if `dart:io` is available and
/// throw an [UnsupportedError] otherwise. In the future, it will create a
/// [BrowserClient] if `dart:html` is available.
factory Client() {
return clientOverride == null ? new IOClient() : clientOverride();
}
static ClientOverride clientOverride;
/// Sends an HTTP HEAD request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> head(dynamic url, {Map<String, String> headers});
/// Sends an HTTP GET request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> get(dynamic url, {Map<String, String> headers});
/// Sends an HTTP POST request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>]
/// or a [Map<String, String>]. If it's a String, it's encoded using
/// [encoding] and used as the body of the request. The content-type of the
/// request will default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to [UTF8].
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> post(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding});
/// Sends an HTTP PUT request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>]
/// or a [Map<String, String>]. If it's a String, it's encoded using
/// [encoding] and used as the body of the request. The content-type of the
/// request will default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to [UTF8].
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> put(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding});
/// Sends an HTTP PATCH request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>]
/// or a [Map<String, String>]. If it's a String, it's encoded using
/// [encoding] and used as the body of the request. The content-type of the
/// request will default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to [UTF8].
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding});
/// Sends an HTTP DELETE request with the given headers to the given URL,
/// which can be a [Uri] or a [String].
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> delete(dynamic url, {Map<String, String> headers});
/// Sends an HTTP GET request with the given headers to the given URL, which
/// can be a [Uri] or a [String], and returns a Future that completes to the
/// body of the response as a String.
///
/// The Future will emit a [ClientException] if the response doesn't have a
/// success status code.
///
/// For more fine-grained control over the request and response, use [send] or
/// [get] instead.
Future<String> read(dynamic url, {Map<String, String> headers});
/// Sends an HTTP GET request with the given headers to the given URL, which
/// can be a [Uri] or a [String], and returns a Future that completes to the
/// body of the response as a list of bytes.
///
/// The Future will emit a [ClientException] if the response doesn't have a
/// success status code.
///
/// For more fine-grained control over the request and response, use [send] or
/// [get] instead.
Future<Uint8List> readBytes(dynamic url, {Map<String, String> headers});
/// Sends an HTTP request and asynchronously returns the response.
Future<StreamedResponse> send(BaseRequest request);
/// Closes the client and cleans up any resources associated with it. It's
/// important to close each client when it's done being used; failing to do so
/// can cause the Dart process to hang.
void close();
}

View file

@ -0,0 +1,16 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// An exception caused by an error in a pkg/http client.
class ClientException implements Exception {
final String message;
/// The URL of the HTTP request or response that failed.
final Uri uri;
ClientException(this.message, [this.uri]);
@override
String toString() => message;
}

View file

@ -2,42 +2,49 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// A composable, [Future]-based library for making HTTP requests.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:mojo/core.dart' as mojo;
import 'mojo_client.dart';
import 'client.dart';
import 'response.dart';
export 'base_client.dart';
export 'base_request.dart';
export 'base_response.dart';
export 'byte_stream.dart';
export 'client.dart';
export 'exception.dart';
export 'io_client.dart';
export 'multipart_file.dart';
export 'multipart_request.dart';
export 'request.dart';
export 'response.dart';
export 'streamed_request.dart';
export 'streamed_response.dart';
/// Sends an HTTP HEAD request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// This automatically initializes a new [Client] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<Response> head(dynamic url) {
return _withClient/*<Response>*/((MojoClient client) => client.head(url));
}
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> head(dynamic url, {Map<String, String> headers}) =>
_withClient((Client client) => client.head(url, headers: headers));
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// This automatically initializes a new [Client] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<Response> get(dynamic url, { Map<String, String> headers }) {
return _withClient/*<Response>*/((MojoClient client) => client.get(url, headers: headers));
}
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> get(dynamic url, {Map<String, String> headers}) =>
_withClient((Client client) => client.get(url, headers: headers));
/// Sends an HTTP POST request with the given headers and body to the given URL,
/// which can be a [Uri] or a [String].
@ -56,18 +63,12 @@ Future<Response> get(dynamic url, { Map<String, String> headers }) {
///
/// [encoding] defaults to [UTF8].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<Response> post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
return _withClient/*<Response>*/((MojoClient client) {
return client.post(url, headers: headers, body: body, encoding: encoding);
});
}
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> post(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding}) =>
_withClient((Client client) => client.post(url,
headers: headers, body: body, encoding: encoding));
/// Sends an HTTP PUT request with the given headers and body to the given URL,
/// which can be a [Uri] or a [String].
@ -86,18 +87,12 @@ Future<Response> post(dynamic url, { Map<String, String> headers, dynamic body,
///
/// [encoding] defaults to [UTF8].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<Response> put(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
return _withClient/*<Response>*/((MojoClient client) {
return client.put(url, headers: headers, body: body, encoding: encoding);
});
}
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> put(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding}) =>
_withClient((Client client) => client.put(url,
headers: headers, body: body, encoding: encoding));
/// Sends an HTTP PATCH request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
@ -116,78 +111,61 @@ Future<Response> put(dynamic url, { Map<String, String> headers, dynamic body, E
///
/// [encoding] defaults to [UTF8].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<Response> patch(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
return _withClient/*<Response>*/((MojoClient client) {
return client.patch(url, headers: headers, body: body, encoding: encoding);
});
}
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body,
Encoding encoding}) =>
_withClient((Client client) => client.patch(url,
headers: headers, body: body, encoding: encoding));
/// Sends an HTTP DELETE request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// This automatically initializes a new [Client] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<Response> delete(dynamic url, { Map<String, String> headers }) {
return _withClient/*<Response>*/((MojoClient client) => client.delete(url, headers: headers));
}
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> delete(dynamic url, {Map<String, String> headers}) =>
_withClient((Client client) => client.delete(url, headers: headers));
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String], and returns a Future that completes to the body of
/// the response as a [String].
///
/// The Future will resolve with an error in the case of a network error or if
/// the response doesn't have a success status code.
/// The Future will emit a [ClientException] if the response doesn't have a
/// success status code.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// This automatically initializes a new [Client] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<String> read(dynamic url, { Map<String, String> headers }) {
return _withClient/*<String>*/((MojoClient client) => client.read(url, headers: headers));
}
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request and response, use [Request]
/// instead.
Future<String> read(dynamic url, {Map<String, String> headers}) =>
_withClient((Client client) => client.read(url, headers: headers));
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String], and returns a Future that completes to the body of
/// the response as a list of bytes.
///
/// The Future will resolve with an error in the case of a network error or if
/// the response doesn't have a success status code.
/// The Future will emit a [ClientException] if the response doesn't have a
/// success status code.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// This automatically initializes a new [Client] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<Uint8List> readBytes(dynamic url, { Map<String, String> headers }) {
return _withClient/*<Uint8List>*/((MojoClient client) => client.readBytes(url, headers: headers));
}
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String], and returns a Future that completes to a data pipe
/// containing the response bytes.
/// the same server, you should use a single [Client] for all of those requests.
///
/// The Future will resolve with an error in the case of a network error or if
/// the response doesn't have a success status code.
///
/// This automatically initializes a new [MojoClient] and closes that client once
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [MojoClient] for all of those requests,
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
Future<mojo.MojoDataPipeConsumer> readDataPipe(dynamic url, { Map<String, String> headers }) {
return _withClient/*<mojo.MojoDataPipeConsumer>*/((MojoClient client) => client.readDataPipe(url, headers: headers));
}
/// For more fine-grained control over the request and response, use [Request]
/// instead.
Future<Uint8List> readBytes(dynamic url, {Map<String, String> headers}) =>
_withClient((Client client) => client.readBytes(url, headers: headers));
Future<dynamic/*=T*/> _withClient/*<T>*/(Future<dynamic/*=T*/> fn(MojoClient client)) {
return fn(new MojoClient());
Future/*<T>*/ _withClient/*<T>*/(Future/*<T>*/ fn(Client client)) async {
Client client = new Client();
try {
return await fn(client);
} finally {
client.close();
}
}

View file

@ -0,0 +1,26 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io' as io;
/// Whether `dart:io` is supported on this platform.
bool get supported => true;
/// Asserts that the [name]d `dart:io` feature is supported on this platform.
///
/// If `dart:io` doesn't work on this platform, this throws an
/// [UnsupportedError].
void assertSupported(String name) {}
/// Creates a new `dart:io` HttpClient instance.
io.HttpClient newHttpClient() => new io.HttpClient();
/// Creates a new `dart:io` File instance with the given [path].
io.File newFile(String path) => new io.File(path);
/// Returns whether [error] is a `dart:io` HttpException.
bool isHttpException(dynamic error) => error is io.HttpException;
/// Returns whether [client] is a `dart:io` HttpClient.
bool isHttpClient(dynamic client) => client is io.HttpClient;

View file

@ -0,0 +1,90 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:async/async.dart';
import 'base_client.dart';
import 'base_request.dart';
import 'exception.dart';
import 'io.dart' as io;
import 'streamed_response.dart';
/// A `dart:io`-based HTTP client.
///
/// This is the default client when running on the command line.
class IOClient extends BaseClient {
/// The underlying `dart:io` HTTP client.
dynamic _inner;
/// Creates a new HTTP client.
///
/// [innerClient] must be a `dart:io` HTTP client. If it's not passed, a
/// default one will be instantiated.
IOClient([dynamic innerClient]) {
io.assertSupported("IOClient");
if (innerClient != null) {
// TODO(nweiz): remove this assert when we can type [innerClient]
// properly.
assert(io.isHttpClient(innerClient));
_inner = innerClient;
} else {
_inner = io.newHttpClient();
}
}
/// Sends an HTTP request and asynchronously returns the response.
@override
Future<StreamedResponse> send(BaseRequest request) async {
dynamic stream = request.finalize();
try {
dynamic ioRequest = await _inner.openUrl(request.method, request.url);
ioRequest
..followRedirects = request.followRedirects
..maxRedirects = request.maxRedirects
..contentLength = request.contentLength == null
? -1
: request.contentLength
..persistentConnection = request.persistentConnection;
request.headers.forEach((String name, String value) {
ioRequest.headers.set(name, value);
});
dynamic response = await stream.pipe(
DelegatingStreamConsumer.typed(ioRequest));
Map<String, dynamic> headers = <String, dynamic>{};
response.headers.forEach((String key, dynamic values) {
headers[key] = values.join(',');
});
return new StreamedResponse(
DelegatingStream.typed/*<List<int>>*/(response).handleError((dynamic error) =>
throw new ClientException(error.message, error.uri),
test: (dynamic error) => io.isHttpException(error)),
response.statusCode,
contentLength: response.contentLength == -1
? null
: response.contentLength,
request: request,
headers: headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase);
} catch (error) {
if (!io.isHttpException(error)) rethrow;
throw new ClientException(error.message, error.uri);
}
}
/// Closes the client. This terminates all active connections. If a client
/// remains unclosed, the Dart process may not terminate.
@override
void close() {
if (_inner != null) _inner.close(force: true);
_inner = null;
}
}

View file

@ -0,0 +1,88 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import 'base_client.dart';
import 'base_request.dart';
import 'byte_stream.dart';
import 'request.dart';
import 'response.dart';
import 'streamed_response.dart';
// TODO(nweiz): once Dart has some sort of Rack- or WSGI-like standard for
// server APIs, MockClient should conform to it.
/// A mock HTTP client designed for use when testing code that uses
/// [BaseClient]. This client allows you to define a handler callback for all
/// requests that are made through it so that you can mock a server without
/// having to send real HTTP requests.
class MockClient extends BaseClient {
/// The handler for receiving [StreamedRequest]s and sending
/// [StreamedResponse]s.
final MockClientStreamHandler _handler;
/// Creates a [MockClient] with a handler that receives [Request]s and sends
/// [Response]s.
MockClient(MockClientHandler fn)
: this._((Request baseRequest, ByteStream bodyStream) {
return bodyStream.toBytes().then((Uint8List bodyBytes) {
Request request = new Request(baseRequest.method, baseRequest.url)
..persistentConnection = baseRequest.persistentConnection
..followRedirects = baseRequest.followRedirects
..maxRedirects = baseRequest.maxRedirects
..headers.addAll(baseRequest.headers)
..bodyBytes = bodyBytes
..finalize();
return fn(request);
}).then((Response response) {
return new StreamedResponse(
new ByteStream.fromBytes(response.bodyBytes),
response.statusCode,
contentLength: response.contentLength,
request: baseRequest,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase);
});
});
MockClient._(this._handler);
/// Creates a [MockClient] with a handler that receives [StreamedRequest]s and
/// sends [StreamedResponse]s.
MockClient.streaming(MockClientStreamHandler fn)
: this._((Request request, ByteStream bodyStream) {
return fn(request, bodyStream).then((StreamedResponse response) {
return new StreamedResponse(
response.stream,
response.statusCode,
contentLength: response.contentLength,
request: request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase);
});
});
/// Sends a request.
@override
Future<StreamedResponse> send(BaseRequest request) async {
ByteStream bodyStream = request.finalize();
return await _handler(request, bodyStream);
}
}
/// A handler function that receives [StreamedRequest]s and sends
/// [StreamedResponse]s. Note that [request] will be finalized.
typedef Future<StreamedResponse> MockClientStreamHandler(
BaseRequest request, ByteStream bodyStream);
/// A handler function that receives [Request]s and sends [Response]s. Note that
/// [request] will be finalized.
typedef Future<Response> MockClientHandler(Request request);

View file

@ -1,260 +0,0 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:mojo/core.dart' as mojo;
import 'package:mojo/mojo/http_header.mojom.dart' as mojom;
import 'package:mojo/mojo/url_request.mojom.dart' as mojom;
import 'package:mojo/mojo/url_response.mojom.dart' as mojom;
import 'package:mojo_services/mojo/network_service.mojom.dart' as mojom;
import 'package:mojo_services/mojo/url_loader.mojom.dart' as mojom;
import 'response.dart';
/// A `mojo`-based HTTP client.
class MojoClient {
/// Sends an HTTP HEAD request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
Future<Response> head(dynamic url, { Map<String, String> headers }) {
return _createResponse(_send("HEAD", url, headers));
}
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
Future<Response> get(dynamic url, { Map<String, String> headers }) {
return _createResponse(_send("GET", url, headers));
}
/// Sends an HTTP POST request with the given headers and body to the given URL,
/// which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>] or
/// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
/// used as the body of the request. The content-type of the request will
/// default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to [UTF8].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
Future<Response> post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
return _createResponse(_send("POST", url, headers, body, encoding));
}
/// Sends an HTTP PUT request with the given headers and body to the given URL,
/// which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>] or
/// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
/// used as the body of the request. The content-type of the request will
/// default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to [UTF8].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
Future<Response> put(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
return _createResponse(_send("PUT", url, headers, body, encoding));
}
/// Sends an HTTP PATCH request with the given headers and body to the given
/// URL, which can be a [Uri] or a [String].
///
/// [body] sets the body of the request. It can be a [String], a [List<int>] or
/// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
/// used as the body of the request. The content-type of the request will
/// default to "text/plain".
///
/// If [body] is a List, it's used as a list of bytes for the body of the
/// request.
///
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
/// content-type of the request will be set to
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
///
/// [encoding] defaults to [UTF8].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
return _createResponse(_send("PATCH", url, headers, body, encoding));
}
/// Sends an HTTP DELETE request with the given headers to the given URL, which
/// can be a [Uri] or a [String].
///
/// Network errors will be turned into [Response] object with a non-null
/// [Response.error] field.
Future<Response> delete(dynamic url, { Map<String, String> headers }) {
return _createResponse(_send("DELETE", url, headers));
}
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String], and returns a Future that completes to the body of
/// the response as a [String].
///
/// The Future will resolve with an error in the case of a network error or if
/// the response doesn't have a success status code.
Future<String> read(dynamic url, { Map<String, String> headers }) {
return get(url, headers: headers).then((Response response) {
_requireSuccess(url, response.statusCode, response.error);
return response.body;
});
}
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String], and returns a Future that completes to the body of
/// the response as a list of bytes.
///
/// The Future will resolve with an error in the case of a network error or if
/// the response doesn't have a success status code.
Future<Uint8List> readBytes(dynamic url, { Map<String, String> headers }) {
return get(url, headers: headers).then((Response response) {
_requireSuccess(url, response.statusCode, response.error);
return response.bodyBytes;
});
}
/// Sends an HTTP GET request with the given headers to the given URL, which can
/// be a [Uri] or a [String], and returns a Future that completes to the body of
/// the response as a [mojo.MojoDataPipeConsumer].
///
/// The Future will resolve with an error in the case of a network error or if
/// the response doesn't have a success status code.
Future<mojo.MojoDataPipeConsumer> readDataPipe(dynamic url, { Map<String, String> headers }) {
return _send('GET', url, headers).then((mojom.UrlResponse response) {
_requireSuccess(url, response.statusCode, response.statusLine);
return response.body;
});
}
mojom.UrlRequest _prepareRequest(String method, dynamic url, Map<String, String> headers, [dynamic body, Encoding encoding = UTF8]) {
List<mojom.HttpHeader> mojoHeaders = <mojom.HttpHeader>[];
headers?.forEach((String name, String value) {
mojom.HttpHeader header = new mojom.HttpHeader()
..name = name
..value = value;
mojoHeaders.add(header);
});
mojom.UrlRequest request = new mojom.UrlRequest()
..url = url.toString()
..headers = mojoHeaders
..method = method
..autoFollowRedirects = true;
if (body != null) {
mojo.MojoDataPipe pipe = new mojo.MojoDataPipe();
request.body = <mojo.MojoDataPipeConsumer>[pipe.consumer];
Uint8List encodedBody = encoding.encode(body);
ByteData data = new ByteData.view(encodedBody.buffer);
mojo.DataPipeFiller.fillHandle(pipe.producer, data);
}
return request;
}
Future<mojom.UrlResponse> _send(String method, dynamic url, Map<String, String> headers, [dynamic body, Encoding encoding = UTF8]) {
Completer<mojom.UrlResponse> completer = new Completer<mojom.UrlResponse>();
mojom.UrlLoaderProxy loader = new mojom.UrlLoaderProxy.unbound();
networkService.createUrlLoader(loader);
mojom.UrlRequest request = _prepareRequest(method, url, headers, body, encoding);
loader.start(request, (mojom.UrlResponse response) async {
loader.close();
try {
if (response.error != null)
throw new Exception('Request to "$url" failed with error ${response.error.code}.\n${response.error.description}');
if (!response.body.handle.isValid)
throw new Exception('Response body does not have a valid handle, but no error was reported.\n${response.body}');
completer.complete(response);
} catch (e, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: e,
stack: stack,
library: 'networking HTTP library',
context: 'while interacting with the Mojo network library',
silent: true
));
completer.completeError(e);
}
});
return completer.future;
}
Future<Response> _createResponse(Future<mojom.UrlResponse> futureResponse) async {
try {
mojom.UrlResponse response = await futureResponse;
try {
ByteData data = await mojo.DataPipeDrainer.drainHandle(response.body);
Uint8List bodyBytes = new Uint8List.view(data.buffer);
Map<String, String> headers = <String, String>{};
if (response.headers != null) {
for (mojom.HttpHeader header in response.headers) {
String headerName = header.name.toLowerCase();
String existingValue = headers[headerName];
headers[headerName] = existingValue != null ? '$existingValue, ${header.value}' : header.value;
}
}
return new Response.bytes(bodyBytes, response.statusCode, headers: headers, error: response.statusLine);
} catch (e, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: e,
stack: stack,
library: 'networking HTTP library',
context: 'while interacting with the Mojo network library',
silent: true
));
rethrow;
}
} catch (e) {
return new Response.bytes(null, 500, error: e);
}
}
void _requireSuccess(dynamic url, int statusCode, dynamic error) {
if (error is Exception)
throw error;
if (statusCode >= 400) {
String extra;
if (error is String && error != '') {
extra = '\nServer response: "$error"';
} else if (error != null) {
extra = '\n$error';
} else {
extra = '';
}
throw new Exception('Request to "$url" failed with status $statusCode.$extra');
}
}
static mojom.NetworkServiceProxy _initNetworkService() {
return shell.connectToApplicationService('mojo:authenticated_network_service', mojom.NetworkService.connectToService);
}
/// A handle to the [NetworkService] object used by [MojoClient].
static final mojom.NetworkServiceProxy networkService = _initNetworkService();
}

View file

@ -0,0 +1,111 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:async/async.dart';
import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as path;
import 'byte_stream.dart';
import 'io.dart' as io;
import 'utils.dart';
/// A file to be uploaded as part of a [MultipartRequest]. This doesn't need to
/// correspond to a physical file.
class MultipartFile {
/// The name of the form field for the file.
final String field;
/// The size of the file in bytes. This must be known in advance, even if this
/// file is created from a [ByteStream].
final int length;
/// The basename of the file. May be null.
final String filename;
/// The content-type of the file. Defaults to `application/octet-stream`.
final MediaType contentType;
/// The stream that will emit the file's contents.
final ByteStream _stream;
/// Creates a new [MultipartFile] from a chunked [Stream] of bytes. The length
/// of the file in bytes must be known in advance. If it's not, read the data
/// from the stream and use [MultipartFile.fromBytes] instead.
///
/// [contentType] currently defaults to `application/octet-stream`, but in the
/// future may be inferred from [filename].
MultipartFile(this.field, Stream<List<int>> stream, this.length,
{this.filename, MediaType contentType})
: this._stream = toByteStream(stream),
this.contentType = contentType != null ? contentType :
new MediaType("application", "octet-stream");
/// Creates a new [MultipartFile] from a byte array.
///
/// [contentType] currently defaults to `application/octet-stream`, but in the
/// future may be inferred from [filename].
factory MultipartFile.fromBytes(String field, List<int> value,
{String filename, MediaType contentType}) {
ByteStream stream = new ByteStream.fromBytes(value);
return new MultipartFile(field, stream, value.length,
filename: filename,
contentType: contentType);
}
/// Creates a new [MultipartFile] from a string.
///
/// The encoding to use when translating [value] into bytes is taken from
/// [contentType] if it has a charset set. Otherwise, it defaults to UTF-8.
/// [contentType] currently defaults to `text/plain; charset=utf-8`, but in
/// the future may be inferred from [filename].
factory MultipartFile.fromString(String field, String value,
{String filename, MediaType contentType}) {
contentType = contentType == null ? new MediaType("text", "plain")
: contentType;
Encoding encoding = encodingForCharset(contentType.parameters['charset'], UTF8);
contentType = contentType.change(parameters: <String, String>{'charset': encoding.name});
return new MultipartFile.fromBytes(field, encoding.encode(value),
filename: filename,
contentType: contentType);
}
/// Whether [finalize] has been called.
bool get isFinalized => _isFinalized;
bool _isFinalized = false;
// TODO(nweiz): Infer the content-type from the filename.
/// Creates a new [MultipartFile] from a path to a file on disk.
///
/// [filename] defaults to the basename of [filePath]. [contentType] currently
/// defaults to `application/octet-stream`, but in the future may be inferred
/// from [filename].
///
/// This can only be used in an environment that supports "dart:io".
static Future<MultipartFile> fromPath(String field, String filePath,
{String filename, MediaType contentType}) async {
io.assertSupported("MultipartFile.fromPath");
if (filename == null) filename = path.basename(filePath);
dynamic file = io.newFile(filePath);
int length = await file.length();
ByteStream stream = new ByteStream(DelegatingStream.typed(file.openRead()));
return new MultipartFile(field, stream, length,
filename: filename,
contentType: contentType);
}
// Finalizes the file in preparation for it being sent as part of a
// [MultipartRequest]. This returns a [ByteStream] that should emit the body
// of the file. The stream may be closed to indicate an empty file.
ByteStream finalize() {
if (isFinalized) {
throw new StateError("Can't finalize a finalized MultipartFile.");
}
_isFinalized = true;
return _stream;
}
}

View file

@ -0,0 +1,177 @@
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'base_request.dart';
import 'byte_stream.dart';
import 'multipart_file.dart';
import 'utils.dart';
final RegExp _newlineRegExp = new RegExp(r"\r\n|\r|\n");
/// A `multipart/form-data` request. Such a request has both string [fields],
/// which function as normal form fields, and (potentially streamed) binary
/// [files].
///
/// This request automatically sets the Content-Type header to
/// `multipart/form-data`. This value will override any value set by the user.
///
/// var uri = Uri.parse("http://pub.dartlang.org/packages/create");
/// var request = new http.MultipartRequest("POST", url);
/// request.fields['user'] = 'nweiz@google.com';
/// request.files.add(new http.MultipartFile.fromFile(
/// 'package',
/// new File('build/package.tar.gz'),
/// contentType: new MediaType('application', 'x-tar'));
/// request.send().then((response) {
/// if (response.statusCode == 200) print("Uploaded!");
/// });
class MultipartRequest extends BaseRequest {
/// The total length of the multipart boundaries used when building the
/// request body. According to http://tools.ietf.org/html/rfc1341.html, this
/// can't be longer than 70.
static const int _BOUNDARY_LENGTH = 70;
static final Random _random = new Random();
/// The form fields to send for this request.
final Map<String, String> fields;
/// The private version of [files].
final List<MultipartFile> _files;
/// Creates a new [MultipartRequest].
MultipartRequest(String method, Uri url)
: fields = <String, String>{},
_files = <MultipartFile>[],
super(method, url);
/// The list of files to upload for this request.
List<MultipartFile> get files => _files;
/// The total length of the request body, in bytes. This is calculated from
/// [fields] and [files] and cannot be set manually.
@override
int get contentLength {
int length = 0;
fields.forEach((String name, String value) {
length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
UTF8.encode(_headerForField(name, value)).length +
UTF8.encode(value).length + "\r\n".length;
});
for (MultipartFile file in _files) {
length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
UTF8.encode(_headerForFile(file)).length +
file.length + "\r\n".length;
}
return length + "--".length + _BOUNDARY_LENGTH + "--\r\n".length;
}
@override
set contentLength(int value) {
throw new UnsupportedError("Cannot set the contentLength property of "
"multipart requests.");
}
/// Freezes all mutable fields and returns a single-subscription [ByteStream]
/// that will emit the request body.
@override
ByteStream finalize() {
// TODO(nweiz): freeze fields and files
String boundary = _boundaryString();
headers['content-type'] = 'multipart/form-data; boundary="$boundary"';
super.finalize();
StreamController<List<int>> controller = new StreamController<List<int>>(sync: true);
void writeAscii(String string) {
controller.add(UTF8.encode(string));
}
dynamic writeUtf8(String string) => controller.add(UTF8.encode(string));
dynamic writeLine() => controller.add(<int>[13, 10]); // \r\n
fields.forEach((String name, String value) {
writeAscii('--$boundary\r\n');
writeAscii(_headerForField(name, value));
writeUtf8(value);
writeLine();
});
Future.forEach(_files, (MultipartFile file) {
writeAscii('--$boundary\r\n');
writeAscii(_headerForFile(file));
return writeStreamToSink(file.finalize(), controller)
.then((_) => writeLine());
}).then((_) {
// TODO(nweiz): pass any errors propagated through this future on to
// the stream. See issue 3657.
writeAscii('--$boundary--\r\n');
controller.close();
});
return new ByteStream(controller.stream);
}
/// All character codes that are valid in multipart boundaries. From
/// http://tools.ietf.org/html/rfc2046#section-5.1.1.
static const List<int> _BOUNDARY_CHARACTERS = const <int>[
39, 40, 41, 43, 95, 44, 45, 46, 47, 58, 61, 63, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 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, 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
];
/// Returns the header string for a field. The return value is guaranteed to
/// contain only ASCII characters.
String _headerForField(String name, String value) {
String header =
'content-disposition: form-data; name="${_browserEncode(name)}"';
if (!isPlainAscii(value)) {
header = '$header\r\n'
'content-type: text/plain; charset=utf-8\r\n'
'content-transfer-encoding: binary';
}
return '$header\r\n\r\n';
}
/// Returns the header string for a file. The return value is guaranteed to
/// contain only ASCII characters.
String _headerForFile(MultipartFile file) {
String header = 'content-type: ${file.contentType}\r\n'
'content-disposition: form-data; name="${_browserEncode(file.field)}"';
if (file.filename != null) {
header = '$header; filename="${_browserEncode(file.filename)}"';
}
return '$header\r\n\r\n';
}
/// Encode [value] in the same way browsers do.
String _browserEncode(String value) {
// http://tools.ietf.org/html/rfc2388 mandates some complex encodings for
// field names and file names, but in practice user agents seem not to
// follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as
// `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII
// characters). We follow their behavior.
return value.replaceAll(_newlineRegExp, "%0D%0A").replaceAll('"', "%22");
}
/// Returns a randomly-generated multipart boundary string
String _boundaryString() {
String prefix = "dart-http-boundary-";
List<int> list = new List<int>.generate(_BOUNDARY_LENGTH - prefix.length,
(int index) =>
_BOUNDARY_CHARACTERS[_random.nextInt(_BOUNDARY_CHARACTERS.length)],
growable: false);
return "$prefix${new String.fromCharCodes(list)}";
}
}

View file

@ -0,0 +1,164 @@
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:http_parser/http_parser.dart';
import 'base_request.dart';
import 'byte_stream.dart';
import 'utils.dart';
/// An HTTP request where the entire request body is known in advance.
class Request extends BaseRequest {
/// Creates a new HTTP request.
Request(String method, Uri url)
: _defaultEncoding = UTF8,
_bodyBytes = new Uint8List(0),
super(method, url);
/// The size of the request body, in bytes. This is calculated from
/// [bodyBytes].
///
/// The content length cannot be set for [Request], since it's automatically
/// calculated from [bodyBytes].
@override
int get contentLength => bodyBytes.length;
@override
set contentLength(int value) {
throw new UnsupportedError("Cannot set the contentLength property of "
"non-streaming Request objects.");
}
/// The default encoding to use when converting between [bodyBytes] and
/// [body]. This is only used if [encoding] hasn't been manually set and if
/// the content-type header has no encoding information.
Encoding _defaultEncoding;
/// The encoding used for the request. This encoding is used when converting
/// between [bodyBytes] and [body].
///
/// If the request has a `Content-Type` header and that header has a `charset`
/// parameter, that parameter's value is used as the encoding. Otherwise, if
/// [encoding] has been set manually, that encoding is used. If that hasn't
/// been set either, this defaults to [UTF8].
///
/// If the `charset` parameter's value is not a known [Encoding], reading this
/// will throw a [FormatException].
///
/// If the request has a `Content-Type` header, setting this will set the
/// charset parameter on that header.
Encoding get encoding {
if (_contentType == null ||
!_contentType.parameters.containsKey('charset')) {
return _defaultEncoding;
}
return requiredEncodingForCharset(_contentType.parameters['charset']);
}
set encoding(Encoding value) {
_checkFinalized();
_defaultEncoding = value;
MediaType contentType = _contentType;
if (contentType == null) return;
_contentType = contentType.change(parameters: <String, String>{'charset': value.name});
}
// TODO(nweiz): make this return a read-only view
/// The bytes comprising the body of the request. This is converted to and
/// from [body] using [encoding].
///
/// This list should only be set, not be modified in place.
Uint8List get bodyBytes => _bodyBytes;
Uint8List _bodyBytes;
set bodyBytes(List<int> value) {
_checkFinalized();
_bodyBytes = toUint8List(value);
}
/// The body of the request as a string. This is converted to and from
/// [bodyBytes] using [encoding].
///
/// When this is set, if the request does not yet have a `Content-Type`
/// header, one will be added with the type `text/plain`. Then the `charset`
/// parameter of the `Content-Type` header (whether new or pre-existing) will
/// be set to [encoding] if it wasn't already set.
String get body => encoding.decode(bodyBytes);
set body(String value) {
bodyBytes = encoding.encode(value);
MediaType contentType = _contentType;
if (contentType == null) {
_contentType = new MediaType("text", "plain", <String, String>{'charset': encoding.name});
} else if (!contentType.parameters.containsKey('charset')) {
_contentType = contentType.change(parameters: <String, String>{'charset': encoding.name});
}
}
/// The form-encoded fields in the body of the request as a map from field
/// names to values. The form-encoded body is converted to and from
/// [bodyBytes] using [encoding] (in the same way as [body]).
///
/// If the request doesn't have a `Content-Type` header of
/// `application/x-www-form-urlencoded`, reading this will throw a
/// [StateError].
///
/// If the request has a `Content-Type` header with a type other than
/// `application/x-www-form-urlencoded`, setting this will throw a
/// [StateError]. Otherwise, the content type will be set to
/// `application/x-www-form-urlencoded`.
///
/// This map should only be set, not modified in place.
Map<String, String> get bodyFields {
MediaType contentType = _contentType;
if (contentType == null ||
contentType.mimeType != "application/x-www-form-urlencoded") {
throw new StateError('Cannot access the body fields of a Request without '
'content-type "application/x-www-form-urlencoded".');
}
return Uri.splitQueryString(body, encoding: encoding);
}
set bodyFields(Map<String, String> fields) {
MediaType contentType = _contentType;
if (contentType == null) {
_contentType = new MediaType("application", "x-www-form-urlencoded");
} else if (contentType.mimeType != "application/x-www-form-urlencoded") {
throw new StateError('Cannot set the body fields of a Request with '
'content-type "${contentType.mimeType}".');
}
this.body = mapToQuery(fields, encoding: encoding);
}
/// Freezes all mutable fields and returns a single-subscription [ByteStream]
/// containing the request body.
@override
ByteStream finalize() {
super.finalize();
return new ByteStream.fromBytes(bodyBytes);
}
/// The `Content-Type` header of the request (if it exists) as a
/// [MediaType].
MediaType get _contentType {
String contentType = headers['content-type'];
if (contentType == null) return null;
return new MediaType.parse(contentType);
}
set _contentType(MediaType value) {
headers['content-type'] = value.toString();
}
/// Throw an error if this request has been finalized.
void _checkFinalized() {
if (!finalized) return;
throw new StateError("Can't modify a finalized Request.");
}
}

View file

@ -2,103 +2,94 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:http_parser/http_parser.dart';
import 'base_request.dart';
import 'base_response.dart';
import 'streamed_response.dart';
import 'utils.dart';
/// An HTTP response where the entire response body is known in advance.
class Response {
/// Creates a [Response] object with the given fields.
///
/// If [bodyBytes] is non-null, it is used to populate [body].
Response.bytes(this.bodyBytes, this.statusCode, {
this.headers: const <String, String>{},
this.error
});
/// The result of decoding [bodyBytes] using the character encoding declared
/// in the headers.
///
/// Defaults to [LATIN1] (ISO 8859-1).
///
/// If [bodyBytes] is null, this will also be null.
String get body => bodyBytes == null ? null : _encodingForHeaders(headers).decode(bodyBytes);
/// The raw byte stream.
class Response extends BaseResponse {
/// The bytes comprising the body of this response.
final Uint8List bodyBytes;
/// The HTTP result code.
/// Creates a new HTTP response with a string body.
Response(
String body,
int statusCode,
{BaseRequest request,
Map<String, String> headers: const <String, String>{},
bool isRedirect: false,
bool persistentConnection: true,
String reasonPhrase})
: this.bytes(
_encodingForHeaders(headers).encode(body),
statusCode,
request: request,
headers: headers,
isRedirect: isRedirect,
persistentConnection: persistentConnection,
reasonPhrase: reasonPhrase);
/// Create a new HTTP response with a byte array body.
Response.bytes(
List<int> bodyBytes,
int statusCode,
{BaseRequest request,
Map<String, String> headers: const <String, String>{},
bool isRedirect: false,
bool persistentConnection: true,
String reasonPhrase})
: bodyBytes = toUint8List(bodyBytes),
super(
statusCode,
contentLength: bodyBytes.length,
request: request,
headers: headers,
isRedirect: isRedirect,
persistentConnection: persistentConnection,
reasonPhrase: reasonPhrase);
/// The body of the response as a string. This is converted from [bodyBytes]
/// using the `charset` parameter of the `Content-Type` header field, if
/// available. If it's unavailable or if the encoding name is unknown,
/// [LATIN1] is used by default, as per [RFC 2616][].
///
/// The code 500 is used when no status code could be obtained from the host.
final int statusCode;
/// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
String get body => _encodingForHeaders(headers).decode(bodyBytes);
/// Error information, if any. This may be populated if the [statusCode] is
/// 4xx or 5xx. This may be a string (e.g. the status line from the server) or
/// an [Exception], but in either case the object should have a useful
/// [toString] implementation that returns a human-readable value.
final dynamic error;
/// The headers for this response.
final Map<String, String> headers;
}
bool _isSpace(String c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
}
int _skipSpaces(String string, int index) {
while (index < string.length && _isSpace(string[index]))
index += 1;
return index;
}
// https://html.spec.whatwg.org/#algorithm-for-extracting-a-character-encoding-from-a-meta-element
String _getCharset(String contentType) {
int index = 0;
while (index < contentType.length) {
index = contentType.indexOf(new RegExp(r'charset', caseSensitive: false), index);
if (index == -1)
return null;
index += 7;
index = _skipSpaces(contentType, index);
if (index >= contentType.length)
return null;
if (contentType[index] != '=')
continue;
index += 1;
index = _skipSpaces(contentType, index);
if (index >= contentType.length)
return null;
String delimiter = contentType[index];
if (delimiter == '"' || delimiter == '\'') {
index += 1;
if (index >= contentType.length)
return null;
int start = index;
int end = contentType.indexOf(delimiter, start);
if (end == -1)
return null;
return contentType.substring(start, end);
}
int start = index;
while (index < contentType.length) {
String c = contentType[index];
if (c == ' ' || c == ';')
break;
index += 1;
}
return contentType.substring(start, index);
/// Creates a new HTTP response by waiting for the full body to become
/// available from a [StreamedResponse].
static Future<Response> fromStream(StreamedResponse response) {
return response.stream.toBytes().then((List<int> body) {
return new Response.bytes(
body,
response.statusCode,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase);
});
}
return null;
}
Encoding _encodingForHeaders(Map<String, String> headers) {
if (headers == null)
return LATIN1;
/// Returns the encoding to use for a response with the given headers. This
/// defaults to [LATIN1] if the headers don't specify a charset or
/// if that charset is unknown.
Encoding _encodingForHeaders(Map<String, String> headers) =>
encodingForCharset(_contentTypeForHeaders(headers).parameters['charset']);
/// Returns the [MediaType] object for the given headers's content-type.
///
/// Defaults to `application/octet-stream`.
MediaType _contentTypeForHeaders(Map<String, String> headers) {
String contentType = headers['content-type'];
if (contentType == null)
return LATIN1;
String charset = _getCharset(contentType);
if (charset == null)
return LATIN1;
return Encoding.getByName(charset) ?? LATIN1;
if (contentType != null) return new MediaType.parse(contentType);
return new MediaType("application", "octet-stream");
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'byte_stream.dart';
import 'base_request.dart';
/// An HTTP request where the request body is sent asynchronously after the
/// connection has been established and the headers have been sent.
///
/// When the request is sent via [BaseClient.send], only the headers and
/// whatever data has already been written to [StreamedRequest.stream] will be
/// sent immediately. More data will be sent as soon as it's written to
/// [StreamedRequest.sink], and when the sink is closed the request will end.
class StreamedRequest extends BaseRequest {
/// Creates a new streaming request.
StreamedRequest(String method, Uri url)
: _controller = new StreamController<List<int>>(sync: true),
super(method, url);
/// The sink to which to write data that will be sent as the request body.
/// This may be safely written to before the request is sent; the data will be
/// buffered.
///
/// Closing this signals the end of the request.
EventSink<List<int>> get sink => _controller.sink;
/// The controller for [sink], from which [BaseRequest] will read data for
/// [finalize].
final StreamController<List<int>> _controller;
/// Freezes all mutable fields other than [stream] and returns a
/// single-subscription [ByteStream] that emits the data being written to
/// [sink].
@override
ByteStream finalize() {
super.finalize();
return new ByteStream(_controller.stream);
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'byte_stream.dart';
import 'base_response.dart';
import 'base_request.dart';
import 'utils.dart';
/// An HTTP response where the response body is received asynchronously after
/// the headers have been received.
class StreamedResponse extends BaseResponse {
/// The stream from which the response body data can be read. This should
/// always be a single-subscription stream.
final ByteStream stream;
/// Creates a new streaming response. [stream] should be a single-subscription
/// stream.
StreamedResponse(
Stream<List<int>> stream,
int statusCode,
{int contentLength,
BaseRequest request,
Map<String, String> headers: const <String, String> {},
bool isRedirect: false,
bool persistentConnection: true,
String reasonPhrase})
: this.stream = toByteStream(stream),
super(
statusCode,
contentLength: contentLength,
request: request,
headers: headers,
isRedirect: isRedirect,
persistentConnection: persistentConnection,
reasonPhrase: reasonPhrase);
}

View file

@ -0,0 +1,143 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'byte_stream.dart';
/// Converts a [Map] from parameter names to values to a URL query string.
///
/// mapToQuery({"foo": "bar", "baz": "bang"});
/// //=> "foo=bar&baz=bang"
String mapToQuery(Map<String, String> map, {Encoding encoding}) {
List<List<String>> pairs = <List<String>>[];
map.forEach((String key, String value) =>
pairs.add(<String>[Uri.encodeQueryComponent(key, encoding: encoding),
Uri.encodeQueryComponent(value, encoding: encoding)]));
return pairs.map((List<String> pair) => "${pair[0]}=${pair[1]}").join("&");
}
/// Like [String.split], but only splits on the first occurrence of the pattern.
/// This will always return an array of two elements or fewer.
///
/// split1("foo,bar,baz", ","); //=> ["foo", "bar,baz"]
/// split1("foo", ","); //=> ["foo"]
/// split1("", ","); //=> []
List<String> split1(String toSplit, String pattern) {
if (toSplit.isEmpty) return <String>[];
int index = toSplit.indexOf(pattern);
if (index == -1) return <String>[toSplit];
return <String>[
toSplit.substring(0, index),
toSplit.substring(index + pattern.length)
];
}
/// Returns the [Encoding] that corresponds to [charset]. Returns [fallback] if
/// [charset] is null or if no [Encoding] was found that corresponds to
/// [charset].
Encoding encodingForCharset(String charset, [Encoding fallback = LATIN1]) {
if (charset == null) return fallback;
Encoding encoding = Encoding.getByName(charset);
return encoding == null ? fallback : encoding;
}
/// Returns the [Encoding] that corresponds to [charset]. Throws a
/// [FormatException] if no [Encoding] was found that corresponds to [charset].
/// [charset] may not be null.
Encoding requiredEncodingForCharset(String charset) {
Encoding encoding = Encoding.getByName(charset);
if (encoding != null) return encoding;
throw new FormatException('Unsupported encoding "$charset".');
}
/// A regular expression that matches strings that are composed entirely of
/// ASCII-compatible characters.
final RegExp _kAsciiOnly = new RegExp(r"^[\x00-\x7F]+$");
/// Returns whether [string] is composed entirely of ASCII-compatible
/// characters.
bool isPlainAscii(String string) => _kAsciiOnly.hasMatch(string);
/// Converts [input] into a [Uint8List].
///
/// If [input] is a [TypedData], this just returns a view on [input].
Uint8List toUint8List(dynamic input) {
if (input is Uint8List)
return input;
if (input is TypedData)
return new Uint8List.view(input.buffer);
return new Uint8List.fromList(input);
}
/// If [stream] is already a [ByteStream], returns it. Otherwise, wraps it in a
/// [ByteStream].
ByteStream toByteStream(Stream<List<int>> stream) {
if (stream is ByteStream) return stream;
return new ByteStream(stream);
}
/// Calls [onDone] once [stream] (a single-subscription [Stream]) is finished.
/// The return value, also a single-subscription [Stream] should be used in
/// place of [stream] after calling this method.
Stream/*<T>*/ onDone/*<T>*/(Stream/*<T>*/ stream, void onDone()) =>
stream.transform(new StreamTransformer.fromHandlers(handleDone: (EventSink<dynamic> sink) { // ignore: always_specify_types
sink.close();
onDone();
}));
// TODO(nweiz): remove this when issue 7786 is fixed.
/// Pipes all data and errors from [stream] into [sink]. When [stream] is done,
/// [sink] is closed and the returned [Future] is completed.
Future<dynamic> store(Stream<dynamic> stream, EventSink<dynamic> sink) {
Completer<dynamic> completer = new Completer<dynamic>();
stream.listen(sink.add,
onError: sink.addError,
onDone: () {
sink.close();
completer.complete();
});
return completer.future;
}
/// Pipes all data and errors from [stream] into [sink]. Completes [Future] once
/// [stream] is done. Unlike [store], [sink] remains open after [stream] is
/// done.
Future<dynamic> writeStreamToSink(Stream<dynamic> stream, EventSink<dynamic> sink) {
Completer<dynamic> completer = new Completer<dynamic>();
stream.listen(sink.add,
onError: sink.addError,
onDone: () => completer.complete());
return completer.future;
}
/// A pair of values.
class Pair<E, F> {
E first;
F last;
Pair(this.first, this.last);
@override
String toString() => '($first, $last)';
@override
bool operator==(dynamic other) {
if (other is! Pair) return false;
return other.first == first && other.last == last;
}
@override
int get hashCode => first.hashCode ^ last.hashCode;
}
/// Configures [future] so that its result (success or exception) is passed on
/// to [completer].
void chainToCompleter(Future<dynamic> future, Completer<dynamic> completer) {
future.then(completer.complete, onError: completer.completeError);
}

View file

@ -13,8 +13,6 @@ import 'package:flutter/http.dart' as http;
import 'package:mojo/core.dart' as core;
import 'package:mojo_services/mojo/asset_bundle/asset_bundle.mojom.dart' as mojom;
import 'shell.dart';
/// A collection of resources used by the application.
///
/// Asset bundles contain resources, such as images and strings, that can be
@ -86,12 +84,18 @@ class NetworkAssetBundle extends AssetBundle {
@override
Future<core.MojoDataPipeConsumer> load(String key) async {
return await http.readDataPipe(_urlFromKey(key));
http.Response response = await http.get(_urlFromKey(key));
if (response.statusCode == 200)
return null;
core.MojoDataPipe pipe = new core.MojoDataPipe();
core.DataPipeFiller.fillHandle(pipe.producer, response.bodyBytes.buffer.asByteData());
return pipe.consumer;
}
@override
Future<String> loadString(String key, { bool cache: true }) async {
return (await http.get(_urlFromKey(key))).body;
http.Response response = await http.get(_urlFromKey(key));
return response.statusCode == 200 ? response.body : null;
}
/// Retrieve a string from the asset bundle, parse it with the given function,
@ -191,22 +195,6 @@ class MojoAssetBundle extends CachingAssetBundle {
/// Creates an [AssetBundle] interface around the given [mojom.AssetBundleProxy] Mojo service.
MojoAssetBundle(this._bundle);
/// Retrieves the asset bundle located at the given URL, unpacks it, and provides it contents.
factory MojoAssetBundle.fromNetwork(String relativeUrl) {
final mojom.AssetBundleProxy bundle = new mojom.AssetBundleProxy.unbound();
_fetchAndUnpackBundleAsychronously(relativeUrl, bundle);
return new MojoAssetBundle(bundle);
}
static Future<Null> _fetchAndUnpackBundleAsychronously(String relativeUrl, mojom.AssetBundleProxy bundle) async {
final core.MojoDataPipeConsumer bundleData = await http.readDataPipe(Uri.base.resolve(relativeUrl));
final mojom.AssetUnpackerProxy unpacker = shell.connectToApplicationService(
'mojo:asset_bundle', mojom.AssetUnpacker.connectToService
);
unpacker.unpackZipStream(bundleData, bundle);
unpacker.close();
}
mojom.AssetBundleProxy _bundle;
@override

View file

@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:ui' show Size, Locale, hashValues;
import 'dart:ui' as ui show Image;
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/http.dart' as http;
@ -278,7 +279,7 @@ abstract class DataPipeImageProvider<T> extends ImageProvider<T> {
// TODO(ianh): Find some way to honour cache headers to the extent that when the
// last reference to an image is released, we proactively evict the image from
// our cache if the headers describe the image as having expired at that point.
class NetworkImage extends DataPipeImageProvider<NetworkImage> {
class NetworkImage extends ImageProvider<NetworkImage> {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments must not be null.
@ -296,15 +297,36 @@ class NetworkImage extends DataPipeImageProvider<NetworkImage> {
}
@override
Future<mojo.MojoDataPipeConsumer> loadDataPipe(NetworkImage key) async {
assert(key == this);
return http.readDataPipe(Uri.base.resolve(key.url));
ImageStreamCompleter load(NetworkImage key) {
return new OneFrameImageStreamCompleter(
_loadAsync(key),
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
}
);
}
@override
double getScale(NetworkImage key) {
Future<ImageInfo> _loadAsync(NetworkImage key) async {
assert(key == this);
return key.scale;
final Uri resolved = Uri.base.resolve(key.url);
final http.Response response = await http.get(resolved);
if (response == null || response.statusCode != 200)
return null;
Uint8List bytes = response.bodyBytes;
if (bytes.lengthInBytes == 0)
return null;
final ui.Image image = await decodeImageFromList(bytes);
if (image == null)
return null;
return new ImageInfo(
image: image,
scale: key.scale,
);
}
@override

View file

@ -10,6 +10,11 @@ dependencies:
meta: ^1.0.3
vector_math: '>=2.0.3 <3.0.0'
# async and http_parser can be removed when we move to using dart-lang/http
# directly.
async: "^1.10.0"
http_parser: ">=0.0.1 <4.0.0"
sky_engine:
path: ../../bin/cache/pkg/sky_engine
sky_services:

View file

@ -8,6 +8,7 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/http.dart' as http;
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@ -109,6 +110,13 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
@override
void initInstances() {
timeDilation = 1.0; // just in case the developer has artificially changed it for development
http.Client.clientOverride = () {
return new http.MockClient((http.Request request){
return new Future<http.Response>.value(
new http.Response("Mocked: Unavailable.", 404, request: request)
);
});
};
super.initInstances();
}