Line data Source code
1 : import 'dart:async'; 2 : import 'dart:convert'; 3 : import 'dart:math'; 4 : import 'multipart_file.dart'; 5 : import 'utils.dart'; 6 : 7 : /// A class to create readable "multipart/form-data" streams. 8 : /// It can be used to submit forms and file uploads to http server. 9 : class FormData { 10 : static const String _BOUNDARY_PRE_TAG = '--dio-boundary-'; 11 : static const _BOUNDARY_LENGTH = _BOUNDARY_PRE_TAG.length + 10; 12 : 13 : String _boundary; 14 : 15 : /// The boundary of FormData, it consists of a constant prefix and a random 16 : /// postfix to assure the the boundary unpredictable and unique, each FormData 17 : /// instance will be different. 18 2 : String get boundary => _boundary; 19 : 20 : final _newlineRegExp = RegExp(r'\r\n|\r|\n'); 21 : 22 : /// The form fields to send for this request. 23 : final fields = <MapEntry<String, String>>[]; 24 : 25 : /// The [files]. 26 : final files = <MapEntry<String, MultipartFile>>[]; 27 : 28 : /// Whether [finalize] has been called. 29 2 : bool get isFinalized => _isFinalized; 30 : bool _isFinalized = false; 31 : 32 1 : FormData() { 33 1 : _init(); 34 : } 35 : 36 : /// Create FormData instance with a Map. 37 1 : FormData.fromMap(Map<String, dynamic> map) { 38 1 : _init(); 39 1 : encodeMap( 40 : map, 41 1 : (key, value) { 42 : if (value == null) return null; 43 1 : if (value is MultipartFile) { 44 3 : files.add(MapEntry(key, value)); 45 : } else { 46 4 : fields.add(MapEntry(key, value.toString())); 47 : } 48 : return null; 49 : }, 50 : encode: false, 51 : ); 52 : } 53 : 54 1 : void _init() { 55 : // Assure the boundary unpredictable and unique 56 1 : var random = Random(); 57 2 : _boundary = _BOUNDARY_PRE_TAG + 58 3 : random.nextInt(4294967296).toString().padLeft(10, '0'); 59 : } 60 : 61 : /// Returns the header string for a field. The return value is guaranteed to 62 : /// contain only ASCII characters. 63 1 : String _headerForField(String name, String value) { 64 : var header = 65 2 : 'content-disposition: form-data; name="${_browserEncode(name)}"'; 66 1 : if (!isPlainAscii(value)) { 67 : header = '$header\r\n' 68 : 'content-type: text/plain; charset=utf-8\r\n' 69 : 'content-transfer-encoding: binary'; 70 : } 71 1 : return '$header\r\n\r\n'; 72 : } 73 : 74 : /// Returns the header string for a file. The return value is guaranteed to 75 : /// contain only ASCII characters. 76 1 : String _headerForFile(MapEntry<String, MultipartFile> entry) { 77 1 : var file = entry.value; 78 : var header = 79 3 : 'content-disposition: form-data; name="${_browserEncode(entry.key)}"'; 80 1 : if (file.filename != null) { 81 3 : header = '$header; filename="${_browserEncode(file.filename)}"'; 82 : } 83 : header = '$header\r\n' 84 1 : 'content-type: ${file.contentType}'; 85 1 : return '$header\r\n\r\n'; 86 : } 87 : 88 : /// Encode [value] in the same way browsers do. 89 1 : String _browserEncode(String value) { 90 : // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for 91 : // field names and file names, but in practice user agents seem not to 92 : // follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as 93 : // `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII 94 : // characters). We follow their behavior. 95 3 : return value.replaceAll(_newlineRegExp, '%0D%0A').replaceAll('"', '%22'); 96 : } 97 : 98 : /// The total length of the request body, in bytes. This is calculated from 99 : /// [fields] and [files] and cannot be set manually. 100 1 : int get length { 101 : var length = 0; 102 3 : fields.forEach((entry) { 103 3 : length += '--'.length + 104 1 : _BOUNDARY_LENGTH + 105 2 : '\r\n'.length + 106 6 : utf8.encode(_headerForField(entry.key, entry.value)).length + 107 4 : utf8.encode(entry.value).length + 108 1 : '\r\n'.length; 109 : }); 110 : 111 2 : for (var file in files) { 112 3 : length += '--'.length + 113 1 : _BOUNDARY_LENGTH + 114 2 : '\r\n'.length + 115 4 : utf8.encode(_headerForFile(file)).length + 116 3 : file.value.length + 117 1 : '\r\n'.length; 118 : } 119 : 120 5 : return length + '--'.length + _BOUNDARY_LENGTH + '--\r\n'.length; 121 : } 122 : 123 1 : Stream<List<int>> finalize() { 124 1 : if (isFinalized) { 125 1 : throw StateError("Can't finalize a finalized MultipartFile."); 126 : } 127 1 : _isFinalized = true; 128 1 : var controller = StreamController<List<int>>(sync: false); 129 1 : void writeAscii(String string) { 130 2 : controller.add(utf8.encode(string)); 131 : } 132 : 133 3 : void writeUtf8(String string) => controller.add(utf8.encode(string)); 134 3 : void writeLine() => controller.add([13, 10]); // \r\n 135 : 136 3 : fields.forEach((entry) { 137 3 : writeAscii('--$boundary\r\n'); 138 4 : writeAscii(_headerForField(entry.key, entry.value)); 139 2 : writeUtf8(entry.value); 140 1 : writeLine(); 141 : }); 142 : 143 3 : Future.forEach(files, (file) { 144 3 : writeAscii('--$boundary\r\n'); 145 2 : writeAscii(_headerForFile(file)); 146 3 : return writeStreamToSink(file.value.finalize(), controller) 147 3 : .then((_) => writeLine()); 148 2 : }).then((_) { 149 3 : writeAscii('--$boundary--\r\n'); 150 1 : controller.close(); 151 : }); 152 1 : return controller.stream; 153 : } 154 : 155 : ///Transform the entire FormData contents as a list of bytes asynchronously. 156 1 : Future<List<int>> readAsBytes() { 157 10 : return Future(()=>finalize().reduce((a, b) => [...a, ...b])); 158 : } 159 : }