diff --git a/.github/workflows/all.yaml b/.github/workflows/all.yaml new file mode 100644 index 0000000..6b986ea --- /dev/null +++ b/.github/workflows/all.yaml @@ -0,0 +1,29 @@ +name: Test Cloudinary Library + +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Install dependencies + run: flutter pub get + - name: Copy env file + run: cp example/lib/src/.init.example.dart example/lib/src/init.dart + - name: Analyze + run: flutter analyze + - name: Tests + run: flutter test diff --git a/.gitignore b/.gitignore index f98def8..3146226 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related **/doc/api/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4e659..f467944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.23.1 +- Update dio dependency and sdk constraints + +## 0.20.0 +- Upload large files in chunks support +- Switch from http package to Dio to support chunk upload + +## 0.13.0 +- add public_id option + ## 0.12.0 - Update dependencies diff --git a/README.md b/README.md index 5f978ae..df0d676 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ secretKey. ## Getting started -Add the dependency `cloudinary_public: ^0.11.0` to your project: +Add the dependency `cloudinary_public: ^0.23.1` to your project: ```dart import 'package:cloudinary_public/cloudinary_public.dart'; @@ -109,4 +109,30 @@ final res = await cloudinary.uploadFile( }); }, ); +``` + +## Upload In Chunks +Use chunked upload when file size is more then 100 Megabytes. + +By default, the chunk size is set to 20 Megabytes but can be set to as low as 5 Megabytes by using the chunk_size parameter. + +Source: https://cloudinary.com/documentation/upload_images#chunked_asset_upload + +```dart +final res = await cloudinary.uploadFileInChunks( + CloudinaryFile.fromFile( + _pickedFile.path, + folder: 'hello-folder', + context: { + 'alt': 'Hello', + 'caption': 'An example upload in chunks', + }, + ), + chunkSize: 10000000 + onProgress: (count, total) { + setState(() { + _uploadingPercentage = (count / total) * 100; + }); + }, +); ``` \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a014562 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,32 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the modules of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + - avoid_print + - prefer_single_quotes + - always_declare_return_types + - require_trailing_commas + - avoid_dynamic_calls + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/lib/example.dart b/example/lib/example.dart index a16559b..7d5d4dd 100644 --- a/example/lib/example.dart +++ b/example/lib/example.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:cloudinary_public/cloudinary_public.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; -main() async { +void main() async { // set cache as true if you don't want to make an upload call with files of the same filename // in such case if the filepath/identifier has already been uploaded before, you simply get the previously cached response. var cloudinary = @@ -13,14 +13,16 @@ main() async { File file = File(''); try { CloudinaryResponse response = await cloudinary.uploadFile( - CloudinaryFile.fromFile(file.path, - resourceType: CloudinaryResourceType.Image), + CloudinaryFile.fromFile( + file.path, + resourceType: CloudinaryResourceType.Image, + ), ); - print(response.secureUrl); + debugPrint(response.secureUrl); } on CloudinaryException catch (e) { - print(e.message); - print(e.request); + debugPrint(e.message); + debugPrint(e.request.toString()); } // Using Byte Data. For example gotten from: https://pub.dev/packages/multi_image_picker @@ -38,7 +40,7 @@ main() async { .toList(), ); - print(uploadedImages[0].secureUrl); + debugPrint(uploadedImages[0].secureUrl); } class Asset { diff --git a/example/lib/src/image_picker_example.dart b/example/lib/src/image_picker_example.dart index bc96183..bd44ab5 100644 --- a/example/lib/src/image_picker_example.dart +++ b/example/lib/src/image_picker_example.dart @@ -15,18 +15,18 @@ class ImagePickerExample extends StatefulWidget { class _ImagePickerExampleState extends State { final picker = ImagePicker(); - PickedFile _pickedFile; + XFile? _pickedFile; bool _uploading = false; double _uploadingPercentage = 0.0; Future getImage() async { - final image = await picker.getImage(source: ImageSource.gallery); + final image = await picker.pickImage(source: ImageSource.gallery); setState(() { if (image != null) { _pickedFile = image; } else { - print('No image selected.'); + debugPrint('No image selected.'); } }); } @@ -64,6 +64,8 @@ class _ImagePickerExampleState extends State { } Future _upload() async { + if (_pickedFile == null) return; + setState(() { _uploading = true; }); @@ -71,7 +73,7 @@ class _ImagePickerExampleState extends State { try { final res = await cloudinary.uploadFile( CloudinaryFile.fromFile( - _pickedFile.path, + _pickedFile!.path, folder: 'hello-folder', context: { 'alt': 'Hello', @@ -84,10 +86,10 @@ class _ImagePickerExampleState extends State { }); }, ); - print(res); + debugPrint(res.toString()); } on CloudinaryException catch (e) { - print(e.message); - print(e.request); + debugPrint(e.message); + debugPrint(e.request.toString()); } setState(() { @@ -98,8 +100,8 @@ class _ImagePickerExampleState extends State { Widget _buildImage() { if (kIsWeb) { - return Image.network(_pickedFile.path); + return Image.network(_pickedFile!.path); } - return Image.file(File(_pickedFile.path)); + return Image.file(File(_pickedFile!.path)); } } diff --git a/example/lib/src/multi_image_picker_example.dart b/example/lib/src/multi_image_picker_example.dart index 31062ac..212ec5f 100644 --- a/example/lib/src/multi_image_picker_example.dart +++ b/example/lib/src/multi_image_picker_example.dart @@ -39,13 +39,13 @@ class _MultiImagePickerExampleState extends State { maxImages: 300, enableCamera: true, selectedAssets: images, - cupertinoOptions: CupertinoOptions(takePhotoIcon: "chat"), + cupertinoOptions: CupertinoOptions(takePhotoIcon: 'chat'), materialOptions: MaterialOptions( - actionBarColor: "#abcdef", - actionBarTitle: "Example App", - allViewTitle: "All Photos", + actionBarColor: '#abcdef', + actionBarTitle: 'Example App', + allViewTitle: 'All Photos', useDetailsView: false, - selectCircleStrokeColor: "#000000", + selectCircleStrokeColor: '#000000', ), ); } on Exception catch (e) { @@ -74,7 +74,7 @@ class _MultiImagePickerExampleState extends State { children: [ Center(child: Text('Error: $_error')), ElevatedButton( - child: Text("Pick images"), + child: Text('Pick images'), onPressed: loadAssets, ), Expanded( @@ -104,7 +104,7 @@ class _MultiImagePickerExampleState extends State { .map( (image) => CloudinaryFile.fromFutureByteData( image.getByteData(), - identifier: image.identifier, + identifier: image.identifier ?? 'test', ), ) .toList(), @@ -114,6 +114,6 @@ class _MultiImagePickerExampleState extends State { _uploading = false; }); - print(uploadedImages[0].secureUrl); + debugPrint(uploadedImages[0].secureUrl); } } diff --git a/example/pubspec.lock b/example/pubspec.lock index de73835..daa0180 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,72 +5,89 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" cloudinary_public: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.11.1" + version: "0.23.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.18.0" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: "7632a2bcddc8cef4afde3c6f80e69b29a7060e176f01119c229fe4eb3a2a3d4f" + url: "https://pub.dev" source: hosted version: "0.3.3+1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" + dio: + dependency: transitive + description: + name: dio + sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" + url: "https://pub.dev" + source: hosted + version: "5.3.4" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" flutter: dependency: "direct main" description: flutter @@ -80,7 +97,8 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "6ffe524cd6a7d49d99b2bf979a4f6ad82304c639cea4c8d3d0f8cf1aff24e74a" + url: "https://pub.dev" source: hosted version: "2.0.6" flutter_test: @@ -97,98 +115,112 @@ packages: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + url: "https://pub.dev" source: hosted version: "4.0.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + sha256: f3712cd190227fb92e0960cb0ce22928ba042c7183b16864ade83b259adf8ea6 + url: "https://pub.dev" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + sha256: "524c04b800320c92c079294a8a2024912b5cff6854d8e5da4d966338b243a276" + url: "https://pub.dev" source: hosted version: "0.8.4+13" image_picker_for_web: dependency: "direct main" description: name: image_picker_for_web - url: "https://pub.dartlang.org" + sha256: "60f306ffbdcada4bc8b2691acc420258a1b758e102c87c4f94fb568d640f0e0e" + url: "https://pub.dev" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + sha256: c1af1bd2223362771e161a72c5361c762a7719f822583762b3ddd1971bcea081 + url: "https://pub.dev" source: hosted version: "0.8.5+5" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + sha256: b5cfa6b0364979411dfbd3a68bd874452eff22344f184f92af79bddc4acf4742 + url: "https://pub.dev" source: hosted version: "2.5.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.5.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.10.0" multi_image_picker: dependency: "direct main" description: name: multi_image_picker - url: "https://pub.dartlang.org" + sha256: "3071bef51d4165f152bde7e22b6f6769745acd5939d70b9fc3d0c9137dbd0a15" + url: "https://pub.dev" source: hosted version: "4.8.01" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.3" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "075f927ebbab4262ace8d0b283929ac5410c0ac4e7fc123c76429564facfb757" + url: "https://pub.dev" source: hosted version: "2.1.2" sky_engine: @@ -200,58 +232,74 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.6.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=2.8.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 16297b3..563d9ba 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: diff --git a/lib/src/cloudinary_file.dart b/lib/src/cloudinary_file.dart index e24dc2a..387ac08 100644 --- a/lib/src/cloudinary_file.dart +++ b/lib/src/cloudinary_file.dart @@ -1,7 +1,12 @@ +import 'dart:io'; +import 'dart:math'; + +// ignore: unnecessary_import +import 'dart:typed_data'; + import 'package:cloudinary_public/cloudinary_public.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart' as http; /// The recognised file class to be used for this package class CloudinaryFile { @@ -14,8 +19,11 @@ class CloudinaryFile { /// The path of the [File] to be uploaded final String? filePath; + /// The file public id which will be used to name the file + final String? publicId; + /// The file name/path - final String? identifier; + final String identifier; /// An optional folder name where the uploaded asset will be stored. /// The public ID will contain the full path of the uploaded asset, @@ -44,13 +52,26 @@ class CloudinaryFile { /// Determine if initialized from [CloudinaryFile.fromUrl] bool get fromExternalUrl => url != null; + int get fileSize { + if (byteData != null) { + return byteData!.lengthInBytes; + } else if (bytesData != null) { + return bytesData!.length; + } else if (filePath != null) { + return File(filePath!).lengthSync(); + } else { + return 0; + } + } + /// [CloudinaryFile] instance const CloudinaryFile._({ - this.resourceType: CloudinaryResourceType.Auto, + this.resourceType = CloudinaryResourceType.Auto, this.byteData, this.bytesData, this.filePath, - this.identifier, + this.publicId, + required this.identifier, this.url, this.tags, this.folder, @@ -58,12 +79,16 @@ class CloudinaryFile { }); /// Instantiate [CloudinaryFile] from future [ByteData] - static Future fromFutureByteData(Future byteData, - {String? identifier, - CloudinaryResourceType resourceType: CloudinaryResourceType.Auto, - List? tags}) async => + static Future fromFutureByteData( + Future byteData, { + required String identifier, + String? publicId, + CloudinaryResourceType resourceType = CloudinaryResourceType.Auto, + List? tags, + }) async => CloudinaryFile.fromByteData( await byteData, + publicId: publicId, identifier: identifier, resourceType: resourceType, tags: tags, @@ -72,14 +97,16 @@ class CloudinaryFile { /// Instantiate [CloudinaryFile] from [ByteData] factory CloudinaryFile.fromByteData( ByteData byteData, { - String? identifier, - CloudinaryResourceType resourceType: CloudinaryResourceType.Auto, + required String identifier, + String? publicId, + CloudinaryResourceType resourceType = CloudinaryResourceType.Auto, List? tags, String? folder, Map? context, }) { return CloudinaryFile._( byteData: byteData, + publicId: publicId, identifier: identifier, resourceType: resourceType, tags: tags, @@ -91,14 +118,16 @@ class CloudinaryFile { /// Instantiate [CloudinaryFile] from [ByteData] factory CloudinaryFile.fromBytesData( List bytesData, { - String? identifier, - CloudinaryResourceType resourceType: CloudinaryResourceType.Auto, + required String identifier, + String? publicId, + CloudinaryResourceType resourceType = CloudinaryResourceType.Auto, List? tags, String? folder, Map? context, }) { return CloudinaryFile._( bytesData: bytesData, + publicId: publicId, identifier: identifier, resourceType: resourceType, tags: tags, @@ -110,14 +139,16 @@ class CloudinaryFile { /// Instantiate [CloudinaryFile] from [File] path factory CloudinaryFile.fromFile( String path, { + String? publicId, String? identifier, - CloudinaryResourceType resourceType: CloudinaryResourceType.Auto, + CloudinaryResourceType resourceType = CloudinaryResourceType.Auto, List? tags, String? folder, Map? context, }) { return CloudinaryFile._( filePath: path, + publicId: publicId, identifier: identifier ??= path.split('/').last, resourceType: resourceType, tags: tags, @@ -129,7 +160,7 @@ class CloudinaryFile { /// Instantiate [CloudinaryFile] from an external url factory CloudinaryFile.fromUrl( String url, { - CloudinaryResourceType resourceType: CloudinaryResourceType.Auto, + CloudinaryResourceType resourceType = CloudinaryResourceType.Auto, List? tags, String? folder, Map? context, @@ -144,42 +175,115 @@ class CloudinaryFile { } /// Convert [CloudinaryFile] to [MultipartFile] - Future toMultipartFile( - [String fieldName = 'file']) async { + Future toMultipartFile() async { assert( !fromExternalUrl, 'toMultipartFile() not available when uploading from external urls', ); if (byteData != null) { - return http.MultipartFile.fromBytes( - fieldName, - byteData!.buffer.asUint8List(), + return MultipartFile.fromBytes( + byteData?.buffer.asUint8List() ?? [], filename: identifier, ); } if (bytesData != null) { - return http.MultipartFile.fromBytes( - fieldName, + return MultipartFile.fromBytes( bytesData!, filename: identifier, ); } if (kIsWeb) { - final bytes = await http.readBytes(Uri.parse(filePath!)); - return http.MultipartFile.fromBytes( - fieldName, - bytes, + final res = await Dio().get>( + filePath!, + options: Options(responseType: ResponseType.bytes), + ); + return MultipartFile.fromBytes( + res.data!, filename: identifier, ); } - return http.MultipartFile.fromPath( - fieldName, + return MultipartFile.fromFile( filePath!, filename: identifier, ); } + + /// Convert to multipart with chunked upload + MultipartFile toMultipartFileChunked( + int start, + int end, + ) { + assert( + !fromExternalUrl, + 'toMultipartFileChunked() not available when uploading from external urls', + ); + Stream> chunkStream; + + if (byteData != null) { + chunkStream = Stream.fromIterable( + [byteData!.buffer.asUint8List(start, end - start)], + ); + } else if (bytesData != null) { + chunkStream = Stream.fromIterable( + [bytesData!.sublist(start, end)], + ); + } else { + chunkStream = File(filePath!).openRead(start, end); + } + + return MultipartFile.fromStream( + () => chunkStream, + end - start, + filename: identifier, + ); + } + + /// common function to generate form data + /// Override the default upload preset (when [CloudinaryPublic] is instantiated) with this one (if specified). + Map toFormData({ + required String uploadPreset, + }) { + final Map data = { + 'upload_preset': uploadPreset, + if (publicId != null) 'public_id': publicId, + if (folder != null) 'folder': folder, + if (tags != null && tags!.isNotEmpty) 'tags': tags!.join(','), + }; + + if (context != null && context!.isNotEmpty) { + String context = ''; + + this.context!.forEach((key, value) { + context += '|$key=$value'; + }); + + // remove the extra `|` at the beginning + data['context'] = context.replaceFirst('|', ''); + } + + return data; + } + + List createChunks( + int chunksCount, + int maxChunkSize, + ) { + List chunks = []; + + for (int i = 0; i < chunksCount; i++) { + int start = i * maxChunkSize; + int end = min(fileSize, start + maxChunkSize); + chunks.add( + toMultipartFileChunked( + start, + end, + ), + ); + } + return chunks; + } } diff --git a/lib/src/cloudinary_public.dart b/lib/src/cloudinary_public.dart index 044d43c..c9b47e5 100644 --- a/lib/src/cloudinary_public.dart +++ b/lib/src/cloudinary_public.dart @@ -1,13 +1,10 @@ import 'dart:async'; -import 'dart:convert'; +import 'dart:math'; +// ignore: unused_import +import 'dart:typed_data'; import 'package:cloudinary_public/cloudinary_public.dart'; -import 'package:cloudinary_public/src/progress_callback.dart'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; - -import '../cloudinary_public.dart'; -import 'multipart_request.dart'; +import 'package:dio/dio.dart'; /// The base class for this package class CloudinaryPublic { @@ -18,7 +15,13 @@ class CloudinaryPublic { static const _fieldName = 'file'; /// To cache all the uploaded files in the current class instance - Map _uploadedFiles = {}; + final Map _uploadedFiles = {}; + + /// The provided Dio client to be used to upload files + final Dio? dioClient; + + /// The Dio client to be used to upload files + final Dio _dioClient; /// Cloud name from Cloudinary final String _cloudName; @@ -29,17 +32,18 @@ class CloudinaryPublic { /// Defaults to false final bool cache; - /// The http client to be used to upload files - http.Client? client; - CloudinaryPublic( this._cloudName, this._uploadPreset, { this.cache = false, - this.client, - }) { - /// set default http client - client ??= http.Client(); + this.dioClient, + }) : _dioClient = dioClient ?? Dio(); + + String _createUrl(CloudinaryResourceType type) { + var url = '$_baseUrl/$_cloudName/' + '${type.name.toLowerCase()}' + '/upload'; + return url; } CloudinaryImage getImage(String publicId) { @@ -66,79 +70,41 @@ class CloudinaryPublic { ProgressCallback? onProgress, }) async { if (cache) { - assert(file.identifier != null, 'identifier is required for caching'); - - if (_uploadedFiles.containsKey(file.identifier)) + if (_uploadedFiles.containsKey(file.identifier)) { return _uploadedFiles[file.identifier]!.enableCache(); + } } - final url = '$_baseUrl/$_cloudName/' - '${describeEnum(file.resourceType).toLowerCase()}' - '/upload'; - - final request = MultipartRequest( - 'POST', - Uri.parse(url), - onProgress: (count, total) { - onProgress?.call(count, total); - }, - ); - - request.headers.addAll({ - 'Accept': 'application/json', - }); - - final data = { - 'upload_preset': uploadPreset ?? _uploadPreset, - }; + Map data = + file.toFormData(uploadPreset: uploadPreset ?? _uploadPreset); if (file.fromExternalUrl) { data[_fieldName] = file.url!; } else { - request.files.add( - await file.toMultipartFile(_fieldName), - ); - } - - if (file.folder != null) { - data['folder'] = file.folder!; + data[_fieldName] = await file.toMultipartFile(); } - if (file.tags != null && file.tags!.isNotEmpty) { - data['tags'] = file.tags!.join(','); - } - - if (file.context != null && file.context!.isNotEmpty) { - String context = ''; - - file.context!.forEach((key, value) { - context += '|$key=$value'; - }); - - // remove the extra `|` at the beginning - data['context'] = context.replaceFirst('|', ''); - } - - request.fields.addAll(data); - - final sendRequest = await client!.send(request); - - final res = await http.Response.fromStream(sendRequest); + var response = await _dioClient.post( + _createUrl(file.resourceType), + data: FormData.fromMap(data), + onSendProgress: onProgress, + ); - if (res.statusCode != 200) { + if (response.statusCode != 200) { throw CloudinaryException( - res.body, - res.statusCode, + response.data, + response.statusCode ?? 0, request: { 'url': file.url, 'path': file.filePath, + 'public_id': file.identifier, 'identifier': file.identifier, }, ); } final cloudinaryResponse = CloudinaryResponse.fromMap( - json.decode(res.body), + response.data, ); if (cache) { @@ -165,11 +131,108 @@ class CloudinaryPublic { Future> multiUpload( List> files, { String? uploadPreset, + ProgressCallback? onProgress, + void Function(int index)? currentUploadIndex, }) async { return Future.wait( files.map( - (file) => uploadFutureFile(file, uploadPreset: uploadPreset), + (file) { + if (currentUploadIndex != null) { + currentUploadIndex(files.indexOf(file)); + } + return uploadFutureFile( + file, + uploadPreset: uploadPreset, + onProgress: onProgress, + ); + }, ), ); } + + /// Upload file in chunks + /// default chunk size is 20 MB + /// chunk size must be less than 20 MB and greater than 5 MB + Future uploadFileInChunks( + CloudinaryFile file, { + String? uploadPreset, + ProgressCallback? onProgress, + int chunkSize = 20000000, // 20MB + }) async { + if (chunkSize > 20000000 || chunkSize < 5000000) { + throw CloudinaryException( + 'Chunk size must be less than 20 MB and greater than 5 MB', + 0, + request: { + 'url': file.url, + 'path': file.filePath, + 'public_id': file.identifier, + 'identifier': file.identifier, + }, + ); + } + CloudinaryResponse? cloudinaryResponse; + + Response? finalResponse; + + int maxChunkSize = min(file.fileSize, chunkSize); + + int chunksCount = (file.fileSize / maxChunkSize).ceil(); + + List? chunks = file.createChunks(chunksCount, maxChunkSize); + + Map data = file.toFormData( + uploadPreset: uploadPreset ?? _uploadPreset, + ); + + try { + for (int i = 0; i < chunksCount; i++) { + final start = i * maxChunkSize; + final end = min((i + 1) * maxChunkSize, file.fileSize); + + final formData = FormData.fromMap({ + 'file': chunks[i], + ...data, + }); + + finalResponse = await _dioClient.post( + _createUrl(file.resourceType), + data: formData, + options: Options( + headers: { + 'Accept': 'application/json', + 'Content-Type': 'multipart/form-data', + 'X-Unique-Upload-Id': file.identifier, + 'Content-Range': 'bytes $start-${end - 1}/${file.fileSize}', + }, + ), + onSendProgress: (sent, total) { + // total progress + final s = sent + i * maxChunkSize; + onProgress?.call(s, file.fileSize); + }, + ); + } + + if (finalResponse?.statusCode != 200 || finalResponse == null) { + throw CloudinaryException( + finalResponse?.data, + finalResponse?.statusCode ?? 0, + request: { + 'url': file.url, + 'path': file.filePath, + 'public_id': file.identifier, + 'identifier': file.identifier, + }, + ); + } + + cloudinaryResponse = CloudinaryResponse.fromMap( + finalResponse.data, + ); + } catch (e) { + rethrow; + } + return cloudinaryResponse; + } } diff --git a/lib/src/cloudinary_resource_type.dart b/lib/src/cloudinary_resource_type.dart index 4c140e4..2609f29 100644 --- a/lib/src/cloudinary_resource_type.dart +++ b/lib/src/cloudinary_resource_type.dart @@ -1,4 +1,6 @@ /// The various types of cloudinary resource types +// ignore_for_file: constant_identifier_names + enum CloudinaryResourceType { Image, Raw, diff --git a/lib/src/cloudinary_response.dart b/lib/src/cloudinary_response.dart index 7031b27..e26a6a5 100644 --- a/lib/src/cloudinary_response.dart +++ b/lib/src/cloudinary_response.dart @@ -9,6 +9,7 @@ class CloudinaryResponse { final List tags; final Map context; final bool fromCache; + final Map data; /// Extract and return the image context Map get customContext { @@ -24,9 +25,10 @@ class CloudinaryResponse { required this.url, required this.secureUrl, required this.originalFilename, - this.tags: const [], - this.context: const {}, - this.fromCache: false, + required this.data, + this.tags = const [], + this.context = const {}, + this.fromCache = false, }); /// Instantiate this class from a map data @@ -42,6 +44,7 @@ class CloudinaryResponse { ? (data['tags'] as List).map((tag) => tag as String).toList() : [], context: data['context'] is Map ? data['context'] : {}, + data: data, ); } @@ -57,6 +60,7 @@ class CloudinaryResponse { tags: tags, context: context, fromCache: true, + data: data, ); } @@ -71,6 +75,7 @@ class CloudinaryResponse { 'original_filename': originalFilename, 'tags': tags, 'context': context, + 'data': data, }; } diff --git a/lib/src/exceptions/cloudinary_exception.dart b/lib/src/exceptions/cloudinary_exception.dart index 63ff94d..22e0e32 100644 --- a/lib/src/exceptions/cloudinary_exception.dart +++ b/lib/src/exceptions/cloudinary_exception.dart @@ -16,7 +16,9 @@ class CloudinaryException implements Exception { /// Extract the error message from cloudinary String? get message { try { - return jsonDecode(responseString)['error']['message']; + final Map json = jsonDecode(responseString); + final Map error = json['error']; + return error['message']; } catch (e) { // unable to extract error message return null; @@ -27,6 +29,7 @@ class CloudinaryException implements Exception { CloudinaryException(this.responseString, this.statusCode, {this.request}); /// `CloudinaryException` summary + @override String toString() { return '($statusCode) ${message ?? responseString}'; } diff --git a/lib/src/multipart_request.dart b/lib/src/multipart_request.dart deleted file mode 100644 index 25ae105..0000000 --- a/lib/src/multipart_request.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -import 'package:http/http.dart' as http; - -class MultipartRequest extends http.MultipartRequest { - /// Creates a new [MultipartRequest]. - MultipartRequest( - String method, - Uri url, { - this.onProgress, - }) : super(method, url); - - final void Function(int bytes, int totalBytes)? onProgress; - - /// Freezes all mutable fields and returns a - /// single-subscription [http.ByteStream] - /// that will emit the request body. - @override - http.ByteStream finalize() { - final byteStream = super.finalize(); - if (onProgress == null) return byteStream; - - final total = contentLength; - var bytes = 0; - - final t = StreamTransformer.fromHandlers( - handleData: (List data, EventSink> sink) { - bytes += data.length; - onProgress?.call(bytes, total); - sink.add(data); - }, - ); - final stream = byteStream.transform(t); - return http.ByteStream(stream); - } -} diff --git a/lib/src/transformation/cloudinary_image.dart b/lib/src/transformation/cloudinary_image.dart index bcb3285..462b10d 100644 --- a/lib/src/transformation/cloudinary_image.dart +++ b/lib/src/transformation/cloudinary_image.dart @@ -14,11 +14,11 @@ class CloudinaryImage { CloudinaryImage(String url) { // remove version - _originalUrl = url.replaceFirst(RegExp(r"v\d+/"), ''); + _originalUrl = url.replaceFirst(RegExp(r'v\d+/'), ''); final resource = url.split('/upload/'); assert(resource.length == 2, 'Invalid cloudinary url'); - _path = resource[0] + '/upload/'; + _path = '${resource[0]}/upload/'; _publicId = resource[1]; } @@ -32,7 +32,7 @@ class CloudinaryImage { return Transformation(_path, _publicId); } - Transformation thumbnail({int width: 200, int height: 200}) { + Transformation thumbnail({int width = 200, int height = 200}) { return transform() .width(width) .height(height) diff --git a/lib/src/transformation/transformation.dart b/lib/src/transformation/transformation.dart index 99bd1c1..adc33d9 100644 --- a/lib/src/transformation/transformation.dart +++ b/lib/src/transformation/transformation.dart @@ -3,8 +3,8 @@ import '../../cloudinary_public.dart'; class Transformation { final String _path; final String _publicId; - Map _params = {}; - List> _chains = []; + final Map _params = {}; + final List> _chains = []; Transformation(this._path, this._publicId); @@ -67,10 +67,10 @@ class Transformation { String url = _path; - _chains.forEach((element) { + for (var element in _chains) { url += _values(element); url += '/'; - }); + } url += _publicId; @@ -90,9 +90,9 @@ class Transformation { List values = []; - keys.forEach((key) { + for (var key in keys) { values.add('${key}_${items[key]}'); - }); + } return values.join(','); } diff --git a/pubspec.lock b/pubspec.lock index dca851f..3319176 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,167 +1,468 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: transitive + description: + name: args + sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + url: "https://pub.dev" + source: hosted + version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: d2494157c32b303f47dedee955b1479f2979c4ff66934eb7c0def44fd9e0267a + url: "https://pub.dev" + source: hosted + version: "1.6.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + dio: + dependency: "direct main" + description: + name: dio + sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" + url: "https://pub.dev" + source: hosted + version: "5.3.4" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - http: - dependency: "direct main" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + http_multi_server: + dependency: transitive description: - name: http - url: "https://pub.dartlang.org" + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + url: "https://pub.dev" source: hosted version: "4.0.0" + io: + dependency: transitive + description: + name: io + sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946 + url: "https://pub.dev" + source: hosted + version: "1.1.0" matcher: dependency: "direct dev" description: name: matcher - url: "https://pub.dartlang.org" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.5.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: dab22e92b41aa1255ea90ddc4bc2feaf35544fd0728e209638cad041a6e3928a + url: "https://pub.dev" + source: hosted + version: "1.0.2" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" + source: hosted + version: "1.4.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" + source: hosted + version: "1.1.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" + source: hosted + version: "1.0.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427" + url: "https://pub.dev" + source: hosted + version: "0.10.11" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + url: "https://pub.dev" + source: hosted + version: "1.24.9" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + url: "https://pub.dev" + source: hosted + version: "9.4.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" + source: hosted + version: "3.1.1" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4ba6243..678ea4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,28 +1,28 @@ name: cloudinary_public description: This package allows you to upload media files directly to cloudinary, without exposing your apiKey or secretKey. -version: 0.12.0 +version: 0.23.1 homepage: https://github.com/djade007/cloudinary_public environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.15.0 <4.0.0" dependencies: flutter: sdk: flutter - http: ^0.13.5 + dio: ^5.3.4 dev_dependencies: flutter_test: sdk: flutter matcher: any - + flutter_lints: any + mocktail: any # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: - # To add assets to your package, add an assets section, like this: # assets: # - icon.png diff --git a/test/cloudinary_file_test.dart b/test/cloudinary_file_test.dart new file mode 100644 index 0000000..603a304 --- /dev/null +++ b/test/cloudinary_file_test.dart @@ -0,0 +1,95 @@ +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:cloudinary_public/cloudinary_public.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'file_manager.dart'; + +const chunkSize10 = 1024 * 1024 * 10; // 10MB + +void main() { + late File tempFile; + late File tempVideoFile; + + setUpAll(() { + tempFile = getFile(); + tempVideoFile = getVideoFile(); + }); + + tearDownAll(() { + // delete generated video file + deleteGeneratedVideoFile(); + }); + + group('Cloudinary file size test', () { + test('uploads an image file', () async { + final file = CloudinaryFile.fromFile(tempFile.path); + expect(file.fileSize, tempFile.lengthSync()); + }); + + test('uploads an image from byte data', () async { + final bytes = await getFutureByteData(); + final file = CloudinaryFile.fromByteData(bytes, identifier: 'test'); + expect(file.fileSize, tempFile.lengthSync()); + }); + + test('uploads an image from bytes data', () async { + final bytes = await getFutureByteData(); + final file = CloudinaryFile.fromBytesData( + bytes.buffer.asUint8List(), + identifier: 'test', + ); + expect(file.fileSize, tempFile.lengthSync()); + }); + }); + + group('cloudinary chunks test', () { + test('chunks count and size', () async { + // Getting file + final file = tempVideoFile; + CloudinaryFile videoFile = CloudinaryFile.fromFile(file.path); + ByteData byteData = await getFutureVideoByteData(); + CloudinaryFile videoFileFromByteData = CloudinaryFile.fromByteData( + byteData, + identifier: 'video.mp4', + ); + + // Values from file + int maxChunkSize = min(videoFile.fileSize, chunkSize10); + int chunksCount = (videoFile.fileSize / maxChunkSize).ceil(); + + var chunks = videoFile.createChunks(chunksCount, maxChunkSize); + + // count chunk size + int chunkSize = 0; + for (var element in chunks) { + chunkSize += element.length; + } + + // values from byte data + int maxChunkSizeFromByteData = + min(videoFileFromByteData.fileSize, chunkSize10); + int chunksCountFromByteData = + (videoFileFromByteData.fileSize / maxChunkSizeFromByteData).ceil(); + + var chunksFromByteData = videoFileFromByteData.createChunks( + chunksCountFromByteData, + maxChunkSize, + ); + + int chunkSizeFromByteData = 0; + for (var element in chunksFromByteData) { + chunkSizeFromByteData += element.length; + } + + // Tests + expect(chunkSize, videoFile.fileSize); + expect(chunks.length, chunksCount); + expect(chunks.length, chunksFromByteData.length); + expect(chunkSize, chunkSizeFromByteData); + expect(chunkSizeFromByteData, videoFile.fileSize); + }); + }); +} diff --git a/test/cloudinary_public_test.dart b/test/cloudinary_public_test.dart index ede742f..68c5c50 100644 --- a/test/cloudinary_public_test.dart +++ b/test/cloudinary_public_test.dart @@ -1,40 +1,58 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:cloudinary_public/cloudinary_public.dart'; -import 'package:flutter/services.dart'; +import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; - -File getFile() { - File file = File('../test/icon.png'); - try { - file.lengthSync(); - } catch (exception) { - file = File('test/icon.png'); - } - return file; -} +import 'package:mocktail/mocktail.dart'; + +import 'file_manager.dart'; -const cloudName = 'demo'; -const uploadPreset = 'preset'; +const cloudName = 'test'; +const uploadPreset = 'test'; + +class MockClient extends Mock implements Dio {} void main() { - // mock http client - final client = MockClient( - (request) async => http.Response( - jsonEncode(_sampleResponse), - 200, - ), - ); + late MockClient client; + + setUp(() { + client = MockClient(); + when( + () => client.post( + 'https://api.cloudinary.com/v1_1/$cloudName/image/upload', + data: any(named: 'data'), + onSendProgress: any(named: 'onSendProgress'), + ), + ).thenAnswer( + (_) async => Response( + data: _sampleResponse, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + ), + ); + + when( + () => client.post( + 'https://api.cloudinary.com/v1_1/$cloudName/video/upload', + data: any(named: 'data'), + onSendProgress: any(named: 'onSendProgress'), + options: any(named: 'options'), + ), + ).thenAnswer( + (_) async => Response( + data: _sampleResponse, + statusCode: 200, + requestOptions: RequestOptions(path: ''), + ), + ); + }); + + tearDownAll(() => deleteGeneratedVideoFile()); test('uploads an image from external url', () async { final cloudinary = CloudinaryPublic( cloudName, uploadPreset, - client: client, cache: true, + dioClient: client, ); final file = CloudinaryFile.fromUrl( @@ -43,14 +61,14 @@ void main() { ); final res = await cloudinary.uploadFile(file); - expect(res, TypeMatcher()); + expect(res, const TypeMatcher()); // test toString expect(res.toString(), res.toMap().toString()); // test cache final secondUpload = await cloudinary.uploadFile(file); - expect(secondUpload, TypeMatcher()); + expect(secondUpload, const TypeMatcher()); expect(secondUpload.fromCache, true); }); @@ -60,27 +78,27 @@ void main() { final cloudinary = CloudinaryPublic( cloudName, uploadPreset, - client: client, cache: true, + dioClient: client, ); - final file = CloudinaryFile.fromFile(tempFile.path, - resourceType: CloudinaryResourceType.Image, - tags: [ - 'trip' - ], - context: { - 'alt': 'Image', - }); + final file = CloudinaryFile.fromFile( + tempFile.path, + resourceType: CloudinaryResourceType.Image, + tags: ['trip'], + context: { + 'alt': 'Image', + }, + ); final res = await cloudinary.uploadFile(file); - expect(res, TypeMatcher()); + expect(res, const TypeMatcher()); // test toString expect(res.toString(), res.toMap().toString()); // test cache final secondUpload = await cloudinary.uploadFile(file); - expect(secondUpload, TypeMatcher()); + expect(secondUpload, const TypeMatcher()); expect(secondUpload.fromCache, true); }); @@ -88,14 +106,17 @@ void main() { final cloudinary = CloudinaryPublic( cloudName, uploadPreset, - client: client, cache: true, + dioClient: client, ); - final file = CloudinaryFile.fromFile(tempFile.path, - resourceType: CloudinaryResourceType.Image, tags: ['trip']); + final file = CloudinaryFile.fromFile( + tempFile.path, + resourceType: CloudinaryResourceType.Image, + tags: ['trip'], + ); final res = await cloudinary.uploadFile(file); - expect(res, TypeMatcher()); + expect(res, const TypeMatcher()); // test toString expect(res.toString(), res.toMap().toString()); @@ -105,7 +126,7 @@ void main() { file, uploadPreset: 'another_preset', ); - expect(secondUpload, TypeMatcher()); + expect(secondUpload, const TypeMatcher()); expect(secondUpload.fromCache, true); }); @@ -113,8 +134,8 @@ void main() { final cloudinary = CloudinaryPublic( cloudName, uploadPreset, - client: client, cache: true, + dioClient: client, ); final files = []; @@ -128,22 +149,25 @@ void main() { expect(uploadedFiles.length, 2); - expect(uploadedFiles[0], TypeMatcher()); + expect(uploadedFiles[0], const TypeMatcher()); - expect(uploadedFiles[1], TypeMatcher()); + expect(uploadedFiles[1], const TypeMatcher()); }); test('upload multiple image byteData', () async { final cloudinary = CloudinaryPublic( cloudName, uploadPreset, - client: client, cache: true, + dioClient: client, ); final files = >[]; - final file = CloudinaryFile.fromFutureByteData(Future.value(ByteData(8)), - resourceType: CloudinaryResourceType.Image, identifier: 'image.jpg'); + final file = CloudinaryFile.fromFutureByteData( + getFutureByteData(), + resourceType: CloudinaryResourceType.Image, + identifier: 'image.jpg', + ); files.add(file); files.add(file); @@ -152,17 +176,17 @@ void main() { expect(uploadedFiles.length, 2); - expect(uploadedFiles[0], TypeMatcher()); + expect(uploadedFiles[0], const TypeMatcher()); - expect(uploadedFiles[1], TypeMatcher()); + expect(uploadedFiles[1], const TypeMatcher()); }); test('Test transformation', () { final cloudinary = CloudinaryPublic( cloudName, uploadPreset, - client: client, cache: true, + dioClient: client, ); final image = CloudinaryImage( @@ -192,17 +216,17 @@ void main() { .generate(); expect( - 'https://res.cloudinary.com/demo/image/upload/c_thumb,g_face,h_150,' - 'w_150/r_20/e_sepia/e_brightness:200,g_south_east,l_cloudinary_icon,' - 'o_60,w_50,x_5,y_5/a_10/front_face.png', - url); + 'https://res.cloudinary.com/demo/image/upload/c_thumb,g_face,h_150,' + 'w_150/r_20/e_sepia/e_brightness:200,g_south_east,l_cloudinary_icon,' + 'o_60,w_50,x_5,y_5/a_10/front_face.png', + url, + ); }); test('thumbnail shortcut', () { final cloudinary = CloudinaryPublic( - cloudName, - uploadPreset, - client: client, + 'demo', + 'present', cache: true, ); @@ -212,6 +236,42 @@ void main() { 'https://res.cloudinary.com/demo/image/upload/c_thumb,g_face,' 'h_200,w_200/cloudinary_icon'); }); + + test('Upload file in Chunks', () async { + final cloudinary = CloudinaryPublic( + cloudName, + uploadPreset, + cache: true, + dioClient: client, + ); + final videoFile = getVideoFile(); + + final file = CloudinaryFile.fromFile( + videoFile.path, + resourceType: CloudinaryResourceType.Video, + tags: ['trip'], + ); + final res = await cloudinary.uploadFileInChunks(file); + expect(res, const TypeMatcher()); + }); + + test('Upload file bytes in chunks', () async { + final cloudinary = CloudinaryPublic( + cloudName, + uploadPreset, + cache: true, + dioClient: client, + ); + final videoBytes = getFutureVideoByteData(); + + final file = await CloudinaryFile.fromFutureByteData( + videoBytes, + resourceType: CloudinaryResourceType.Video, + identifier: 'video.mp4', + ); + final res = await cloudinary.uploadFileInChunks(file); + expect(res, const TypeMatcher()); + }); } const _sampleResponse = { @@ -235,10 +295,4 @@ const _sampleResponse = { 'secure_url': 'https://res.cloudinary.com/$cloudName/image/upload/v1590212116/psryios0nkgpf1h4um3h.jpg', 'original_filename': '001', - 'context': { - 'custom': { - 'alt': 'image', - 'caption': 'Example image', - } - } }; diff --git a/test/file_manager.dart b/test/file_manager.dart new file mode 100644 index 0000000..a89536a --- /dev/null +++ b/test/file_manager.dart @@ -0,0 +1,46 @@ +import 'dart:io'; +import 'dart:typed_data'; + +const videoSize = 1024 * 1024 * 50; // 50MB + +File getFile() { + File file = File('../test/icon.png'); + try { + file.lengthSync(); + } catch (exception) { + file = File('test/icon.png'); + } + return file; +} + +Future getFutureByteData() async { + final tempFile = getFile(); + Uint8List uIntBytes = tempFile.readAsBytesSync(); + ByteData bytes = (ByteData.view(uIntBytes.buffer)); + return Future.value(bytes); +} + +// To test video add sample video to test folder +File getVideoFile() { + final file = File('test/video.mp4'); + if (file.existsSync()) { + return file; + } + + file.writeAsBytesSync(List.filled(videoSize, 0)); + return file; +} + +Future getFutureVideoByteData() { + final tempFile = getVideoFile(); + Uint8List uIntBytes = tempFile.readAsBytesSync(); + ByteData bytes = (ByteData.view(uIntBytes.buffer)); + return Future.value(bytes); +} + +Future deleteGeneratedVideoFile() async { + final file = getVideoFile(); + if (file.existsSync()) { + await file.delete(); + } +}