1
+ import 'dart:collection' ;
2
+ import 'dart:convert' ;
1
3
import 'dart:io' ;
2
4
5
+ import 'package:http_parser/http_parser.dart' ;
6
+ import 'package:mime/mime.dart' ;
7
+
3
8
/// Content-Type: application/x-www-form-urlencoded
4
9
final formUrlEncodedContentType = ContentType (
5
10
'application' ,
6
11
'x-www-form-urlencoded' ,
7
12
);
8
13
9
- /// Parses the body as form data and returns a `Future<Map<String, String>>` .
10
- /// Throws a [StateError] if the MIME type is not "application/x-www-form-urlencoded"
14
+ /// Content-Type: multipart/form-data
15
+ final multipartFormDataContentType = ContentType (
16
+ 'multipart' ,
17
+ 'form-data' ,
18
+ );
19
+
20
+ /// Parses the body as form data and returns a `Future<Map<String, dynamic>>` .
21
+ /// Throws a [StateError] if the MIME type is not "application/x-www-form-urlencoded" or "multipart/form-data".
11
22
/// https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata%E2%91%A0
12
- Future <Map < String , String > > parseFormData ({
23
+ Future <FormData > parseFormData ({
13
24
required Map <String , String > headers,
14
25
required Future <String > Function () body,
26
+ required Stream <List <int >> Function () bytes,
15
27
}) async {
16
28
final contentType = _extractContentType (headers);
17
- if (! _isFormUrlEncoded (contentType)) {
29
+ final isFormUrlEncoded = _isFormUrlEncoded (contentType);
30
+ final isMultipartFormData = _isMultipartFormData (contentType);
31
+
32
+ if (! isFormUrlEncoded && ! isMultipartFormData) {
18
33
throw StateError (
19
34
'''
20
35
Body could not be parsed as form data due to an invalid MIME type.
21
- Expected MIME type: "${formUrlEncodedContentType .mimeType }"
36
+ Expected MIME type: "${formUrlEncodedContentType .mimeType }" OR "${ multipartFormDataContentType . mimeType }"
22
37
Actual MIME type: "${contentType ?.mimeType ?? '' }"
23
38
''' ,
24
39
);
25
40
}
26
41
27
- return Uri .splitQueryString (await body ());
42
+ return isFormUrlEncoded
43
+ ? _extractFormUrlEncodedFormData (body: await body ())
44
+ : await _extractMultipartFormData (headers: headers, bytes: bytes ());
28
45
}
29
46
30
47
ContentType ? _extractContentType (Map <String , String > headers) {
@@ -37,3 +54,145 @@ bool _isFormUrlEncoded(ContentType? contentType) {
37
54
if (contentType == null ) return false ;
38
55
return contentType.mimeType == formUrlEncodedContentType.mimeType;
39
56
}
57
+
58
+ bool _isMultipartFormData (ContentType ? contentType) {
59
+ if (contentType == null ) return false ;
60
+ return contentType.mimeType == multipartFormDataContentType.mimeType;
61
+ }
62
+
63
+ FormData _extractFormUrlEncodedFormData ({required String body}) {
64
+ return FormData (fields: Uri .splitQueryString (body), files: {});
65
+ }
66
+
67
+ final _keyValueRegexp = RegExp ('(?:(?<key>[a-zA-Z0-9-_]+)="(?<value>.*?)";*)+' );
68
+
69
+ Future <FormData > _extractMultipartFormData ({
70
+ required Map <String , String > headers,
71
+ required Stream <List <int >> bytes,
72
+ }) async {
73
+ final contentType = headers[HttpHeaders .contentTypeHeader]! ;
74
+ final mediaType = MediaType .parse (contentType);
75
+ final boundary = mediaType.parameters['boundary' ];
76
+ final transformer = MimeMultipartTransformer (boundary! );
77
+
78
+ final fields = < String , String > {};
79
+ final files = < String , UploadedFile > {};
80
+
81
+ await for (final part in transformer.bind (bytes)) {
82
+ final contentDisposition = part.headers['content-disposition' ];
83
+ if (contentDisposition == null ) continue ;
84
+ if (! contentDisposition.startsWith ('form-data;' )) continue ;
85
+
86
+ final values = _keyValueRegexp
87
+ .allMatches (contentDisposition)
88
+ .fold (< String , String > {}, (map, match) {
89
+ return map..[match.namedGroup ('key' )! ] = match.namedGroup ('value' )! ;
90
+ });
91
+
92
+ final name = values['name' ]! ;
93
+ final fileName = values['filename' ];
94
+
95
+ if (fileName != null ) {
96
+ files[name] = UploadedFile (
97
+ fileName,
98
+ ContentType .parse (part.headers['content-type' ] ?? 'text/plain' ),
99
+ part,
100
+ );
101
+ } else {
102
+ final bytes = (await part.toList ()).fold (< int > [], (p, e) => p..addAll (e));
103
+ fields[name] = utf8.decode (bytes);
104
+ }
105
+ }
106
+
107
+ return FormData (fields: fields, files: files);
108
+ }
109
+
110
+ /// {@template form_data}
111
+ /// The fields and files of received form data request.
112
+ /// {@endtemplate}
113
+ class FormData with MapMixin <String , String > {
114
+ /// {@macro form_data}
115
+ const FormData ({
116
+ required Map <String , String > fields,
117
+ required Map <String , UploadedFile > files,
118
+ }) : _fields = fields,
119
+ _files = files;
120
+
121
+ final Map <String , String > _fields;
122
+
123
+ final Map <String , UploadedFile > _files;
124
+
125
+ /// The fields that were submitted in the form.
126
+ Map <String , String > get fields => Map .unmodifiable (_fields);
127
+
128
+ /// The files that were uploaded in the form.
129
+ Map <String , UploadedFile > get files => Map .unmodifiable (_files);
130
+
131
+ @override
132
+ @Deprecated ('Use `fields[key]` to retrieve values' )
133
+ String ? operator [](Object ? key) => _fields[key] ?? _files[key]? .toString ();
134
+
135
+ @override
136
+ @Deprecated ('Use `fields.keys` to retrieve field keys' )
137
+ Iterable <String > get keys => _fields.keys;
138
+
139
+ @override
140
+ @Deprecated ('Use `fields.values` to retrieve field values' )
141
+ Iterable <String > get values => _fields.values;
142
+
143
+ @override
144
+ @Deprecated (
145
+ 'FormData should be immutable, in the future this will thrown an error' ,
146
+ )
147
+ void operator []= (String key, String value) => _fields[key] = value;
148
+
149
+ @override
150
+ @Deprecated (
151
+ 'FormData should be immutable, in the future this will thrown an error' ,
152
+ )
153
+ void clear () => _fields.clear ();
154
+
155
+ @override
156
+ @Deprecated (
157
+ 'FormData should be immutable, in the future this will thrown an error' ,
158
+ )
159
+ String ? remove (Object ? key) => _fields.remove (key);
160
+ }
161
+
162
+ /// {@template uploaded_file}
163
+ /// The uploaded file of a form data request.
164
+ /// {@endtemplate}
165
+ class UploadedFile {
166
+ /// {@macro uploaded_file}
167
+ const UploadedFile (
168
+ this .name,
169
+ this .contentType,
170
+ this ._byteStream,
171
+ );
172
+
173
+ /// The name of the uploaded file.
174
+ final String name;
175
+
176
+ /// The type of the uploaded file.
177
+ final ContentType contentType;
178
+
179
+ final Stream <List <int >> _byteStream;
180
+
181
+ /// Read the content of the file as a list of bytes.
182
+ ///
183
+ /// Can only be called once.
184
+ Future <List <int >> readAsBytes () async {
185
+ return (await _byteStream.toList ())
186
+ .fold <List <int >>([], (p, e) => p..addAll (e));
187
+ }
188
+
189
+ /// Open the content of the file as a stream of bytes.
190
+ ///
191
+ /// Can only be called once.
192
+ Stream <List <int >> openRead () => _byteStream;
193
+
194
+ @override
195
+ String toString () {
196
+ return '{ name: $name , contentType: $contentType }' ;
197
+ }
198
+ }
0 commit comments