├── analysis_options.yaml ├── test ├── input.jpeg ├── result.bin └── form_data_test.dart ├── lib ├── form_data.dart └── src │ ├── utils.dart │ ├── entry.dart │ ├── result.dart │ └── form_data.dart ├── .gitignore ├── CHANGELOG.md ├── pubspec.yaml ├── example └── form_data.dart ├── LICENSE └── README.md /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /test/input.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARE/form_data/main/test/input.jpeg -------------------------------------------------------------------------------- /test/result.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARE/form_data/main/test/result.bin -------------------------------------------------------------------------------- /lib/form_data.dart: -------------------------------------------------------------------------------- 1 | library form_data; 2 | 3 | export 'src/form_data.dart' show FormData; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## `1.0.0-nullsafety.1` 2 | 3 | * :memo: update README 4 | 5 | ## `1.0.0-nullsafety.0` 6 | 7 | * :wrench: updated the code and added caching 8 | 9 | ## `0.0.1-nullsafety.1` 10 | 11 | * :memo: update README 12 | 13 | ## `0.0.1-nullsafety.0` 14 | 15 | * :tada: initial commit -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | String generateBoundary() { 4 | var random = Random(); 5 | 6 | var boundary = '--------------------------'; 7 | 8 | for (var i = 0; i < 24; i++) { 9 | boundary += random.nextInt(10).toString(); 10 | } 11 | 12 | return boundary; 13 | } 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: form_data 2 | version: 1.0.0-nullsafety.1 3 | description: multipart/form-data builder for Dart aiming to be compatible with RFC 7578. 4 | homepage: https://github.com/are/form_data 5 | 6 | environment: 7 | sdk: '>=2.12.0-0 <3.0.0' 8 | 9 | dev_dependencies: 10 | pedantic: ^1.10.0-nullsafety.3 11 | test: ^1.16.0-nullsafety.12 12 | -------------------------------------------------------------------------------- /example/form_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:form_data/form_data.dart'; 4 | 5 | void main() { 6 | var formData = FormData() 7 | ..add('field name', 'value') 8 | ..add('primitive types', 42) 9 | ..addBytes('my file', File(Platform.script.path).readAsBytesSync(), 10 | filename: 'form_data.dart'); 11 | 12 | print(formData.contentType); 13 | print(formData.contentLength); 14 | print(formData.body); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/entry.dart: -------------------------------------------------------------------------------- 1 | class Entry { 2 | final List name; 3 | final Value value; 4 | 5 | const Entry(this.name, this.value); 6 | 7 | factory Entry.value(List name, List bytes) => 8 | Entry(name, Value(bytes)); 9 | 10 | factory Entry.file( 11 | List name, 12 | List bytes, { 13 | List? filename, 14 | List? contentType, 15 | }) => 16 | Entry(name, File(bytes, filename: filename, contentType: contentType)); 17 | } 18 | 19 | class Value { 20 | final List bytes; 21 | 22 | const Value(this.bytes); 23 | } 24 | 25 | class File extends Value { 26 | final List? filename; 27 | final List? contentType; 28 | 29 | const File(List bytes, {this.filename, this.contentType}) : super(bytes); 30 | } 31 | -------------------------------------------------------------------------------- /test/form_data_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:form_data/form_data.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('FormData encoder', () { 8 | test('works correctly with primitive data types', () async { 9 | var formData = FormData(); 10 | 11 | formData.boundary = '---123'; 12 | 13 | formData.add('name', 'Fisrtname McSurname'); 14 | formData.add('age', 10); 15 | formData.add('address', '''some longer 16 | value with new lines and emojis 😃'''); 17 | 18 | formData.addBytes('image', await File('test/input.jpeg').readAsBytes(), 19 | filename: 'myImage.jpeg', contentType: 'image/jpeg'); 20 | 21 | var expected = await File('test/result.bin').readAsBytes(); 22 | 23 | expect(formData.contentLength, equals(expected.length)); 24 | expect(formData.body, equals(expected)); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Artur Wojciechowski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #
🧾 `package:form_data`
2 | 3 | [![Pub Version](https://img.shields.io/pub/v/form_data)](https://pub.dev/packages/form_data) 4 | [![RFC](https://img.shields.io/badge/RFC-7578-blue)](https://tools.ietf.org/html/rfc7578) 5 | 6 | `Content-Type: multipart/form-data` builder for Dart aiming to be compatible with [RFC 7578](https://tools.ietf.org/html/rfc7578). 7 | 8 | API documentation is available [here](https://pub.dev/documentation/form_data/latest/). 9 | 10 | 11 | ## Installation 12 | 13 | Add `form_data` to your `pubspec.yaml` and run `pub get` or `flutter pub get`. 14 | 15 | ```yaml 16 | dependencies: 17 | form_data: ^1.0.0-nullsafety.1 18 | ``` 19 | 20 | ## Usage 21 | 22 | Instantinate `FormData` class and add fields using `add` and `addBytes` methods. 23 | 24 | ```dart 25 | var formData = FormData(); 26 | 27 | formData.add('name', 'Name Surname'); 28 | formData.add('answer', 42); 29 | formData.addBytes('file', await File('picture.png').readAsBytes(), 30 | filename: 'myPicture.png', contentType: 'image/png'); 31 | ``` 32 | 33 | Extract data using `body`, `contentType` and `contentLength` headers. 34 | 35 | ```dart 36 | var request = client.postUrl(myUri); 37 | 38 | request.headers.set('Content-Type', formData.contentType); 39 | request.headers.set('Content-Length', formData.contentLength); 40 | 41 | request.add(formData.body); 42 | 43 | await request.close(); 44 | ``` -------------------------------------------------------------------------------- /lib/src/result.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'entry.dart'; 4 | 5 | class FormDataResult { 6 | final List _entries; 7 | 8 | /// Boundary of the form-data. 9 | final String boundary; 10 | 11 | /// Content type header including the boundary. 12 | String get contentType => 'multipart/form-data; boundary=${boundary}'; 13 | 14 | /// Actual body of the form-data. 15 | late final List body; 16 | 17 | /// Length of the content. 18 | late final int contentLength; 19 | 20 | final List _midBoundary; 21 | final List _endBoundary; 22 | 23 | static final List _lineBreak = utf8.encode('\r\n'); 24 | static final List _contentDispositionPrefix = 25 | utf8.encode('Content-Disposition: form-data; name="'); 26 | static final List _contentDispositionPostfix = utf8.encode('"'); 27 | static final List _contentDispositionInfix = 28 | utf8.encode('"; filename="'); 29 | static final List _contentTypePrefix = utf8.encode('Content-Type: '); 30 | 31 | FormDataResult(this._entries, this.boundary) 32 | : _midBoundary = utf8.encode('--${boundary}\r\n'), 33 | _endBoundary = utf8.encode('--${boundary}--\r\n') { 34 | var _body = []; 35 | 36 | for (var entry in _entries) { 37 | _body.addAll(_midBoundary); 38 | _body.addAll(_contentDispositionPrefix); 39 | _body.addAll(entry.name); 40 | 41 | var value = entry.value; 42 | if (value is File) { 43 | var filename = value.filename; 44 | 45 | if (filename != null) { 46 | _body.addAll(_contentDispositionInfix); 47 | _body.addAll(filename); 48 | } 49 | } 50 | 51 | _body.addAll(_contentDispositionPostfix); 52 | _body.addAll(_lineBreak); 53 | 54 | if (value is File) { 55 | var contentType = value.contentType; 56 | 57 | if (contentType != null) { 58 | _body.addAll(_contentTypePrefix); 59 | _body.addAll(contentType); 60 | _body.addAll(_lineBreak); 61 | } 62 | } 63 | 64 | _body.addAll(_lineBreak); 65 | _body.addAll(entry.value.bytes); 66 | _body.addAll(_lineBreak); 67 | } 68 | 69 | _body.addAll(_endBoundary); 70 | 71 | body = List.unmodifiable(_body); 72 | contentLength = body.length; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/form_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'entry.dart'; 4 | import 'result.dart'; 5 | import 'utils.dart'; 6 | 7 | /// Used to generate form data. 8 | class FormData { 9 | /// Encoding used in converting [String] to [List]. Defaults to `utf8`. 10 | final Encoding encoding; 11 | 12 | /// Boundary used in the form data. 13 | String boundary = generateBoundary(); 14 | 15 | final List _entries = []; 16 | 17 | FormData({this.encoding = utf8}); 18 | 19 | /// Add a field [name] to the form data. 20 | /// 21 | /// [value] will be converted to a string using [Object.toString] and encoded using [FormData.encoding]. 22 | void add(String name, dynamic value) { 23 | _isDirty = true; 24 | _entries.add( 25 | Entry.value(encoding.encode(name), encoding.encode(value.toString())), 26 | ); 27 | } 28 | 29 | /// Add a field [name] to the form data. 30 | /// 31 | /// [contents] will be added directly to the body, skipping encoding. 32 | void addBytes(String name, List bytes, 33 | {String? contentType, String? filename}) { 34 | _isDirty = true; 35 | _entries.add( 36 | Entry.file(encoding.encode(name), bytes, 37 | contentType: 38 | contentType == null ? null : encoding.encode(contentType), 39 | filename: filename == null ? null : encoding.encode(filename)), 40 | ); 41 | } 42 | 43 | /// Returns value of a first field named [name] or null if no fields were found. 44 | String? get(String name) { 45 | var bytes = getBytes(name); 46 | return bytes != null ? encoding.decode(bytes) : null; 47 | } 48 | 49 | /// Returns bytes of a first field named [name] or null if no fields were found. 50 | List? getBytes(String name) { 51 | var matchedEntries = 52 | _entries.where((entry) => encoding.decode(entry.name) == name); 53 | 54 | if (matchedEntries.isEmpty) { 55 | return null; 56 | } 57 | 58 | return matchedEntries.first.value.bytes; 59 | } 60 | 61 | /// Returns list of bytes of fields named [name]. 62 | List> getAllBytes(String name) { 63 | return _entries 64 | .where((entry) => encoding.decode(entry.name) == name) 65 | .map((entry) => entry.value.bytes) 66 | .toList(); 67 | } 68 | 69 | /// Returns list of values of fields named [name]. 70 | List getAll(String name) { 71 | return getAllBytes(name).map((bytes) => encoding.decode(bytes)).toList(); 72 | } 73 | 74 | bool _isDirty = true; 75 | FormDataResult? _lastResult; 76 | 77 | FormDataResult get _result { 78 | if (_isDirty || _lastResult == null) { 79 | _lastResult = _build(); 80 | } 81 | 82 | return _lastResult!; 83 | } 84 | 85 | FormDataResult _build() => FormDataResult(List.from(_entries), boundary); 86 | 87 | /// Content-Type header value including the boundary. 88 | String get contentType => _result.contentType; 89 | 90 | /// Content-Length header value. 91 | int get contentLength => _result.contentLength; 92 | 93 | /// Actual body of the form-data. 94 | List get body => _result.body; 95 | } 96 | --------------------------------------------------------------------------------