diff --git a/.github/recipe.yaml b/.github/recipe.yaml index 3d89ef2..f1d1272 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -1,5 +1,5 @@ plugins: firebase_core: ["tv-7.0"] firebase_database: ["tv-7.0"] - firebase_storage: [] + firebase_storage: ["tv-7.0"] cloud_functions: ["tv-7.0"] diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 216a9d2..c5a4ee0 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -10,6 +10,7 @@ on: jobs: integration_test: runs-on: [self-hosted] + timeout-minutes: 30 if: ${{ github.repository_owner == 'flutter-tizen' }} steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 61015a6..a3d56de 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ FlutterFire for Tizen is a set of plugins that enable Flutter apps to use Fireba | Cloud Functions | REPLACEME | [πŸ”—](https://firebase.google.com/products/functions) | [πŸ“–](https://firebase.flutter.dev/docs/functions/overview) | [`cloud_functions`](packages/cloud_functions) | | Core | REPLACEME | [πŸ”—](https://firebase.google.com) | [πŸ“–](https://firebase.flutter.dev/docs/core/usage) | [`firebase_core`](packages/firebase_core) | | Realtime Database | REPLACEME | [πŸ”—](https://firebase.google.com/products/database) | [πŸ“–](https://firebase.flutter.dev/docs/database/overview) | [`firebase_database`](packages/firebase_database) | +| Storage | REPLACEME | [πŸ”—](https://firebase.google.com/products/storage) | [πŸ“–](https://firebase.flutter.dev/docs/storage/overview) | [`firebase_storage`](packages/firebase_storage) | Please note that the plugins are now in an early stage of development, as they're based on the experimental implementation of the Firebase C++ SDK for Linux desktop ([v10.4.0](https://github.com/firebase/firebase-cpp-sdk/tree/v10.4.0)). Our plan is to initially provide development versions first, and then gradually transition to a stable version as a subsequent step following stable SDK releases. @@ -19,6 +20,7 @@ Please note that the plugins are now in an early stage of development, as they'r | Cloud Functions | 7.0 | βœ”οΈ | βœ”οΈ | | Core | 7.0 | βœ”οΈ | βœ”οΈ | | Realtime Database | 7.0 | βœ”οΈ | βœ”οΈ | +| Storage | 7.0 | βœ”οΈ | βœ”οΈ | ## License diff --git a/packages/cloud_functions/tizen/build_def.prop b/packages/cloud_functions/tizen/build_def.prop index 64d7f61..085fff2 100644 --- a/packages/cloud_functions/tizen/build_def.prop +++ b/packages/cloud_functions/tizen/build_def.prop @@ -1,2 +1,2 @@ PREBUILD_COMMAND = ./tar_url.sh \ - https://github.com/daeye0n/gooddaytocode/archive/refs/tags/v10.4.0-draft-4.tar.gz + https://raw.githubusercontent.com/hs0225/download/firebase-sdk/firebaseSDK-tizen-1.0.1.tar.gz diff --git a/packages/firebase_core/tizen/build_def.prop b/packages/firebase_core/tizen/build_def.prop index 144eb94..86c20a8 100644 --- a/packages/firebase_core/tizen/build_def.prop +++ b/packages/firebase_core/tizen/build_def.prop @@ -1,3 +1,3 @@ PREBUILD_COMMAND = ./tar_url.sh \ - https://github.com/daeye0n/gooddaytocode/archive/refs/tags/v10.4.0-draft-4.tar.gz \ + https://raw.githubusercontent.com/hs0225/download/firebase-sdk/firebaseSDK-tizen-1.0.1.tar.gz\ && ./cp_firebase_libs.sh diff --git a/packages/firebase_storage/.clang-format b/packages/firebase_storage/.clang-format new file mode 100644 index 0000000..5ab0945 --- /dev/null +++ b/packages/firebase_storage/.clang-format @@ -0,0 +1,3 @@ +--- +BasedOnStyle: Google +--- diff --git a/packages/firebase_storage/.gitignore b/packages/firebase_storage/.gitignore new file mode 100644 index 0000000..d87f6f0 --- /dev/null +++ b/packages/firebase_storage/.gitignore @@ -0,0 +1,28 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/firebase_storage/CHANGELOG.md b/packages/firebase_storage/CHANGELOG.md new file mode 100644 index 0000000..6073234 --- /dev/null +++ b/packages/firebase_storage/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/firebase_storage/LICENSE b/packages/firebase_storage/LICENSE new file mode 100644 index 0000000..f336bce --- /dev/null +++ b/packages/firebase_storage/LICENSE @@ -0,0 +1,61 @@ +``` +Copyright (c) 2023 Samsung Electronics Co., Ltd. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +``` +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +``` +Copyright 2019 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/packages/firebase_storage/README.md b/packages/firebase_storage/README.md new file mode 100644 index 0000000..395f832 --- /dev/null +++ b/packages/firebase_storage/README.md @@ -0,0 +1,55 @@ +# firebase_storage_tizen + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + +The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. +To add platforms, run `flutter create -t plugin --platforms .` in this directory. +You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. + +# Usage + +To use this package, you need to include `firebase_storage_tizen` as a dependency alongside `firebase_storage` in your `pubspec.yaml`. Please note that `firebase_storage_tizen` implementation is not officially endorsed for `firebase_storage`. + +```yaml +dependencies: + firebase_storage: ^11.0.10 + firebase_storage_tizen: ^0.1.0 +``` + +Then you can import `firebase_storage` in your Dart code: + +```dart +import 'package:firebase_storage/firebase_storage.dart'; +``` + +## Required privileges + +To use this plugin in a Tizen application, you may need to declare the following privileges in your `tizen-manifest.xml` file. + +```xml + + http://tizen.org/privilege/internet + +``` + +- `http://tizen.org/privilege/internet` allows the application to access the Internet. + +For the details on Tizen privileges, please see [Tizen Docs: API Privileges](https://docs.tizen.org/application/dotnet/get-started/api-privileges). + +# Limitations + +The following features are currently unavailable as they're not supported by the version of Firebase C++ SDK for Linux that this plugin is currently based on. + + - useEmulator method of FirebaseStorage class. + - listAll method of Reference class. diff --git a/packages/firebase_storage/analysis_options.yaml b/packages/firebase_storage/analysis_options.yaml new file mode 100644 index 0000000..f04c6cf --- /dev/null +++ b/packages/firebase_storage/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/packages/firebase_storage/example/.gitignore b/packages/firebase_storage/example/.gitignore new file mode 100644 index 0000000..ad3c2ca --- /dev/null +++ b/packages/firebase_storage/example/.gitignore @@ -0,0 +1,42 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/firebase_storage/example/README.md b/packages/firebase_storage/example/README.md new file mode 100644 index 0000000..bec2b4a --- /dev/null +++ b/packages/firebase_storage/example/README.md @@ -0,0 +1,16 @@ +# firebase_storage_tizen_example + +Demonstrates how to use the firebase_storage_tizen plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/firebase_storage/example/analysis_options.yaml b/packages/firebase_storage/example/analysis_options.yaml new file mode 100644 index 0000000..a174f61 --- /dev/null +++ b/packages/firebase_storage/example/analysis_options.yaml @@ -0,0 +1,10 @@ +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# in the LICENSE file. + +include: ../../../analysis_options.yaml +linter: + rules: + avoid_print: false + depend_on_referenced_packages: false + library_private_types_in_public_api: false diff --git a/packages/firebase_storage/example/assets/hello.txt b/packages/firebase_storage/example/assets/hello.txt new file mode 100644 index 0000000..802992c --- /dev/null +++ b/packages/firebase_storage/example/assets/hello.txt @@ -0,0 +1 @@ +Hello world diff --git a/packages/firebase_storage/example/integration_test/extratest_e2e.dart b/packages/firebase_storage/example/integration_test/extratest_e2e.dart new file mode 100644 index 0000000..d6286bb --- /dev/null +++ b/packages/firebase_storage/example/integration_test/extratest_e2e.dart @@ -0,0 +1,105 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. 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:io'; + +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import './test_utils.dart'; + +const String kOkString = 'ok'; +final List okStringList = utf8.encode(kOkString); + +void setupExtraTests() { + group('$Reference', () { + late FirebaseStorage storage; + + setUpAll(() async { + storage = FirebaseStorage.instance; + }); + + test('getData', () async { + final Reference ref = storage.ref('flutter-tests/ok.txt'); + + // fix: Ensure reference file has an ok string. + await ref.putString(kOkString); + final bytes = await ref.getData(); + + List okStringList = utf8.encode(kOkString); + expect(bytes?.length, okStringList.length); + expect(bytes, okStringList); + }); + + test('getMetadata', () async { + final Reference ref = storage.ref('flutter-tests/ok.txt'); + + final result = await ref.getMetadata(); + expect(result, isA()); + expect(result.name, 'ok.txt'); + }); + }); + + group('ControlTask', () { + late FirebaseStorage storage; + + setUpAll(() async { + storage = FirebaseStorage.instance; + }); + + Future _testPauseTask(Task task) async { + List snapshots = []; + bool hasError = false; + expect(task.snapshot.state, TaskState.running); + + task.snapshotEvents.listen( + (TaskSnapshot snapshot) { + snapshots.add(snapshot); + }, + onError: (error) { + hasError = true; + }, + cancelOnError: true, + ); + + await task.snapshotEvents.first; + await Future.delayed(const Duration(milliseconds: 100)); + + bool? paused = await task.pause(); + expect(paused, isTrue); + expect(task.snapshot.state, TaskState.paused); + + await Future.delayed(const Duration(milliseconds: 100)); + bool? resumed = await task.resume(); + expect(resumed, isTrue); + expect(task.snapshot.state, TaskState.running); + + TaskSnapshot snapshot = await task; + expect(task.snapshot.state, TaskState.success); + expect(snapshot.state, TaskState.success); + + expect(snapshot.totalBytes, snapshot.bytesTransferred); + + expect(hasError, false); + } + + test('upload pause and resume', () async { + final File file = await createFile('test-file.txt'); + final Reference ref = storage.ref('flutter-tests').child('test-file.txt'); + + await _testPauseTask(ref.putFile(file)); + }); + + test('download pause and resume', () async { + final Directory systemTempDir = Directory.systemTemp; + final File tempFile = File('${systemTempDir.path}/temp-test-file.txt'); + if (tempFile.existsSync()) await tempFile.delete(); + + final Reference ref = storage.ref('flutter-tests').child('test-file.txt'); + + await _testPauseTask(ref.writeToFile(tempFile)); + }); + }); +} diff --git a/packages/firebase_storage/example/integration_test/firebase_storage_e2e_test.dart b/packages/firebase_storage/example/integration_test/firebase_storage_e2e_test.dart new file mode 100644 index 0000000..7cb9095 --- /dev/null +++ b/packages/firebase_storage/example/integration_test/firebase_storage_e2e_test.dart @@ -0,0 +1,64 @@ +// Copyright 2022, the Chromium 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 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../lib/firebase_options.dart'; +import 'instance_e2e.dart'; +import 'list_result_e2e.dart'; +import 'reference_e2e.dart'; +import 'task_e2e.dart'; +import 'extratest_e2e.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('firebase_storage', () { + setUpAll(() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + // Add a write only file + await FirebaseStorage.instance + .ref('writeOnly.txt') + .putString('Write Only'); + + await FirebaseStorage.instance + .ref('flutter-tests/ok.txt') + .putString('Ok!'); + + // Setup list items - Future.wait not working... + await FirebaseStorage.instance + .ref('flutter-tests/list/file1.txt') + .putString('File 1'); + await FirebaseStorage.instance + .ref('flutter-tests/list/file2.txt') + .putString('File 2'); + await FirebaseStorage.instance + .ref('flutter-tests/list/file3.txt') + .putString('File 3'); + await FirebaseStorage.instance + .ref('flutter-tests/list/file4.txt') + .putString('File 4'); + await FirebaseStorage.instance + .ref('flutter-tests/list/nested/file5.txt') + .putString('File 5'); + + // fix: Add file to pass the list result test. + await FirebaseStorage.instance + .ref('flutter-tests/list/child/file6.txt') + .putString('File 6'); + }); + + setupInstanceTests(); + setupListResultTests(); + setupReferenceTests(); + setupTaskTests(); + setupExtraTests(); + }); +} diff --git a/packages/firebase_storage/example/integration_test/instance_e2e.dart b/packages/firebase_storage/example/integration_test/instance_e2e.dart new file mode 100644 index 0000000..a3071e5 --- /dev/null +++ b/packages/firebase_storage/example/integration_test/instance_e2e.dart @@ -0,0 +1,250 @@ +// Copyright 2022, the Chromium 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 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../lib/firebase_options.dart'; +import 'test_utils.dart'; + +void setupInstanceTests() { + group('$FirebaseStorage', () { + late FirebaseStorage storage; + late FirebaseApp secondaryApp; + late FirebaseApp secondaryAppWithoutBucket; + + setUpAll(() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + storage = FirebaseStorage.instance; + secondaryApp = await testInitializeSecondaryApp(); + }); + + test('instance', () { + expect(storage, isA()); + expect(storage.app, isA()); + expect(storage.app.name, defaultFirebaseAppName); + }); + + test('instanceFor', () { + FirebaseStorage secondaryStorage = + FirebaseStorage.instanceFor(app: secondaryApp, bucket: 'test'); + expect(storage.app, isA()); + expect(secondaryStorage, isA()); + expect(secondaryStorage.app.name, 'testapp'); + }); + + test('default bucket cannot be null', () async { + try { + secondaryAppWithoutBucket = + await testInitializeSecondaryApp(withDefaultBucket: false); + + FirebaseStorage.instanceFor( + app: secondaryAppWithoutBucket, + ); + fail('should have thrown an error'); + } on FirebaseException catch (e) { + expect( + e.message, + "No storage bucket could be found for the app 'testapp-no-bucket'. Ensure you have set the [storageBucket] on [FirebaseOptions] whilst initializing the secondary Firebase app.", + ); + } + }); + + group('ref', () { + test('uses default path if none provided', () { + Reference ref = storage.ref(); + expect(ref.fullPath, '/'); + }); + + test('accepts a custom path', () async { + Reference ref = storage.ref('foo/bar/baz.png'); + expect(ref.fullPath, 'foo/bar/baz.png'); + }); + + test('strips leading / from custom path', () async { + Reference ref = storage.ref('/foo/bar/baz.png'); + expect(ref.fullPath, 'foo/bar/baz.png'); + }); + }); + + group('refFromURL', () { + test('accepts a gs url', () async { + const url = 'gs://foo/bar/baz.png'; + Reference ref = storage.refFromURL(url); + expect(ref.fullPath, 'bar/baz.png'); + expect(ref.bucket, 'foo'); + }); + + test('accepts a https url from google cloud', () async { + const url = + 'https://storage.googleapis.com/flutterfire-e2e-tests.appspot.com/pdf/4lqA70lYwfRgH1krOevw6mLMgPs2_162613790513241'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(ref.name, '4lqA70lYwfRgH1krOevw6mLMgPs2_162613790513241'); + expect( + ref.fullPath, + 'pdf/4lqA70lYwfRgH1krOevw6mLMgPs2_162613790513241', + ); + }); + + test('accepts a https url', () async { + const url = + 'https://firebasestorage.googleapis.com/v0/b/flutterfire-e2e-tests.appspot.com/o/1mbTestFile.gif?alt=media'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(ref.name, '1mbTestFile.gif'); + expect(ref.fullPath, '1mbTestFile.gif'); + }); + + test('accepts a https url with a deep path', () async { + const url = + 'https://firebasestorage.googleapis.com/v0/b/flutterfire-e2e-tests.appspot.com/o/nested/path/segments/1mbTestFile.gif?alt=media'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(ref.name, '1mbTestFile.gif'); + expect(ref.fullPath, 'nested/path/segments/1mbTestFile.gif'); + }); + + test('accepts a https url with special characters', () async { + const url = + 'https://firebasestorage.googleapis.com/v0/b/flutterfire-e2e-tests.appspot.com/o/foo+bar/file with spaces.png?alt=media'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(ref.name, 'file with spaces.png'); + expect(ref.fullPath, 'foo+bar/file with spaces.png'); + }); + + test('accepts a https encoded url', () async { + const url = + 'https%3A%2F%2Ffirebasestorage.googleapis.com%2Fv0%2Fb%2Fflutterfire-e2e-tests.appspot.com%2Fo%2F1mbTestFile.gif%3Falt%3Dmedia'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(ref.name, '1mbTestFile.gif'); + expect(ref.fullPath, '1mbTestFile.gif'); + }); + + test('accepts a Storage emulator url', () { + const url = + 'http://localhost:9199/v0/b/flutterfire-e2e-tests.appspot.com/o/1mbTestFile.gif?alt=media'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(ref.name, '1mbTestFile.gif'); + expect(ref.fullPath, '1mbTestFile.gif'); + }); + + test('accepts a https url including port number', () { + const url = + 'https://firebasestorage.googleapis.com:433/v0/b/flutterfire-e2e-tests.appspot.com/o/nested/path/segments/1mbTestFile.gif?alt=media'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(ref.name, '1mbTestFile.gif'); + expect(ref.fullPath, 'nested/path/segments/1mbTestFile.gif'); + + const googleUrl = + 'https://storage.googleapis.com/flutterfire-e2e-tests.appspot.com/pdf/4lqA70lYwfRgH1krOevw6mLMgPs2_162613790513241'; + Reference refGoogle = storage.refFromURL(googleUrl); + expect(refGoogle.bucket, 'flutterfire-e2e-tests.appspot.com'); + expect(refGoogle.name, '4lqA70lYwfRgH1krOevw6mLMgPs2_162613790513241'); + expect( + refGoogle.fullPath, + 'pdf/4lqA70lYwfRgH1krOevw6mLMgPs2_162613790513241', + ); + }); + + test('throws an error if https url could not be parsed', () async { + expect( + () { + storage.refFromURL('https://invertase.io'); + fail('Did not throw an Error.'); + }, + throwsA( + isA().having( + (p0) => p0.message, + 'assertion message', + contains( + "url could not be parsed, ensure it's a valid storage url", + ), + ), + ), + ); + }); + + test('accepts a gs url without a fullPath', () async { + const url = 'gs://some-bucket'; + Reference ref = storage.refFromURL(url); + expect(ref.bucket, url.replaceFirst('gs://', '')); + expect(ref.fullPath, '/'); + }); + + test('throws an error if url does not start with gs:// or https://', + () async { + expect( + () { + storage.refFromURL('bs://foo/bar/cat.gif'); + fail('Should have thrown an [AssertionError]'); + }, + throwsA( + isA().having( + (p0) => p0.message, + 'assertion message', + contains("a url must start with 'gs://' or 'https://'"), + ), + ), + ); + }); + }); + + group('setMaxOperationRetryTime', () { + test('should set', () async { + expect( + storage.maxOperationRetryTime, + const Duration(milliseconds: 120000), + ); + storage.setMaxOperationRetryTime(const Duration(milliseconds: 100000)); + expect( + storage.maxOperationRetryTime, + const Duration(milliseconds: 100000), + ); + }); + }); + + group('setMaxUploadRetryTime', () { + test('should set', () async { + expect( + storage.maxUploadRetryTime, + const Duration(milliseconds: 600000), + ); + storage.setMaxUploadRetryTime(const Duration(milliseconds: 120000)); + expect( + storage.maxUploadRetryTime, + const Duration(milliseconds: 120000), + ); + }); + }); + + group('setMaxDownloadRetryTime', () { + test('should set', () async { + expect( + storage.maxDownloadRetryTime, + const Duration(milliseconds: 600000), + ); + storage.setMaxDownloadRetryTime(const Duration(milliseconds: 120000)); + expect( + storage.maxDownloadRetryTime, + const Duration(milliseconds: 120000), + ); + }); + }); + + test('toString', () { + expect( + storage.toString(), + 'FirebaseStorage(app: [DEFAULT], bucket: flutterfire-e2e-tests.appspot.com)', + ); + }); + }); +} diff --git a/packages/firebase_storage/example/integration_test/list_result_e2e.dart b/packages/firebase_storage/example/integration_test/list_result_e2e.dart new file mode 100644 index 0000000..d74b788 --- /dev/null +++ b/packages/firebase_storage/example/integration_test/list_result_e2e.dart @@ -0,0 +1,37 @@ +// Copyright 2022, the Chromium 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 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void setupListResultTests() { + group('$ListResult', () { + late FirebaseStorage storage; + late ListResult result; + + setUpAll(() async { + storage = FirebaseStorage.instance; + Reference ref = storage.ref('flutter-tests/list'); + // Needs to be > half of the # of items in the storage, + // so there's a chance of picking up some items and some + // prefixes. + // fix: Change maxResults from 3 to 5 with reference to above. + result = await ref.list(const ListOptions(maxResults: 5)); + }); + + test('items', () async { + expect(result.items, isA>()); + expect(result.items.length, greaterThan(0)); + }); + + test('nextPageToken', () async { + expect(result.nextPageToken, isNotNull); + }); + + test('prefixes', () async { + expect(result.prefixes, isA>()); + expect(result.prefixes.length, greaterThan(0)); + }); + }); +} diff --git a/packages/firebase_storage/example/integration_test/reference_e2e.dart b/packages/firebase_storage/example/integration_test/reference_e2e.dart new file mode 100644 index 0000000..c0cbc67 --- /dev/null +++ b/packages/firebase_storage/example/integration_test/reference_e2e.dart @@ -0,0 +1,467 @@ +// Copyright 2022, the Chromium 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:io'; + +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import './test_utils.dart'; + +void setupReferenceTests() { + group('$Reference', () { + late FirebaseStorage storage; + + setUpAll(() async { + storage = FirebaseStorage.instance; + }); + + group('bucket', () { + test('returns the storage bucket as a string', () async { + expect( + storage.ref('/ok.jpeg').bucket, + storage.app.options.storageBucket, + ); + }); + }); + + group('fullPath', () { + test('returns the full path as a string', () async { + expect( + storage.ref('/foo/uploadNope.jpeg').fullPath, + 'foo/uploadNope.jpeg', + ); + + expect( + storage.ref('foo/uploadNope.jpeg').fullPath, + 'foo/uploadNope.jpeg', + ); + }); + }); + + group('name', () { + test('returns the file name as a string', () async { + Reference ref = storage.ref('/foo/uploadNope.jpeg'); + expect(ref.name, 'uploadNope.jpeg'); + }); + }); + + group('parent', () { + test('returns the parent directory as a reference', () async { + expect(storage.ref('/foo/uploadNope.jpeg').parent?.fullPath, 'foo'); + }); + + test('returns null if already at root', () async { + Reference ref = storage.ref('/'); + expect(ref.parent, isNull); + }); + }); + + group('root', () { + test('returns a reference to the root of the bucket', () async { + expect(storage.ref('/foo/uploadNope.jpeg').root.fullPath, '/'); + }); + }); + + group('child()', () { + test('returns a reference to a child path', () async { + Reference parentRef = storage.ref('/foo'); + Reference childRef = parentRef.child('someFile.json'); + + expect(childRef.fullPath, 'foo/someFile.json'); + }); + }); + + group('delete()', () { + test('should delete a file', () async { + Reference ref = storage.ref('flutter-tests/deleteMe.jpeg'); + await ref.putString('To Be Deleted :)'); + await ref.delete(); + + await expectLater( + () => ref.delete(), + throwsA( + isA() + .having((e) => e.code, 'code', 'object-not-found') + .having( + (e) => e.message, + 'message', + 'No object exists at the desired reference', + ), + ), + ); + }); + + test('throws error if file does not exist', () async { + Reference ref = storage.ref('flutter-tests/iDoNotExist.jpeg'); + + await expectLater( + () => ref.delete(), + throwsA( + isA() + .having((e) => e.code, 'code', 'object-not-found') + .having( + (e) => e.message, + 'message', + 'No object exists at the desired reference', + ), + ), + ); + }); + + test('throws error if no write permission', () async { + Reference ref = storage.ref('/uploadNope.jpeg'); + + await expectLater( + () => ref.delete(), + throwsA( + isA() + .having((e) => e.code, 'code', 'unauthorized') + .having( + (e) => e.message, + 'message', + 'User is not authorized to perform the desired action', + ), + ), + ); + }); + }); + + group('getDownloadURL', () { + test( + 'gets a download url', + () async { + Reference ref = storage.ref('flutter-tests/ok.txt'); + await ref.putString('ok'); + + String downloadUrl = await ref.getDownloadURL(); + expect(downloadUrl, isA()); + expect(downloadUrl, contains('ok.txt')); + expect(downloadUrl, contains(storage.app.options.projectId)); + }, + // Fails on emulator since iOS SDK 10. See PR notes: + // https://github.com/firebase/flutterfire/pull/9708 + skip: defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS, + ); + + test('errors if permission denied', () async { + Reference ref = storage.ref('writeOnly.txt'); + + await expectLater( + () => ref.getDownloadURL(), + throwsA( + isA() + .having((e) => e.code, 'code', 'unauthorized') + .having( + (e) => e.message, + 'message', + 'User is not authorized to perform the desired action', + ), + ), + ); + }); + + test('throws error if file does not exist', () async { + Reference ref = storage.ref('flutter-tests/iDoNotExist.jpeg'); + + await expectLater( + () => ref.getDownloadURL(), + throwsA( + isA() + .having((e) => e.code, 'code', 'object-not-found') + .having( + (e) => e.message, + 'message', + 'No object exists at the desired reference', + ), + ), + ); + }); + }); + + group('list', () { + test('returns list results', () async { + Reference ref = storage.ref('flutter-tests/list'); + ListResult result = await ref.list(const ListOptions(maxResults: 25)); + + expect(result.items.length, greaterThan(0)); + expect(result.prefixes, isA>()); + expect(result.prefixes.length, greaterThan(0)); + }); + + test('errors if maxResults is less than 0 ', () async { + Reference ref = storage.ref('/list'); + expect( + () => ref.list(const ListOptions(maxResults: -1)), + throwsAssertionError, + ); + }); + + test('errors if maxResults is 0 ', () async { + Reference ref = storage.ref('/list'); + expect( + () => ref.list(const ListOptions(maxResults: 0)), + throwsAssertionError, + ); + }); + + test('errors if maxResults is more than 1000 ', () async { + Reference ref = storage.ref('/list'); + expect( + () => ref.list(const ListOptions(maxResults: 1001)), + throwsAssertionError, + ); + }); + }); + + // hosung: not supported on Tizen. + test('listAll', () async { + Reference ref = storage.ref('flutter-tests/list'); + ListResult result = await ref.listAll(); + expect(result.items, isNotNull); + expect(result.items.length, greaterThan(0)); + expect(result.nextPageToken, isNull); + + expect(result.prefixes, isA>()); + expect(result.prefixes.length, greaterThan(0)); + }, skip: Platform.isLinux); + + group( + 'putData', + () { + test('uploads a file with buffer', () async { + List list = utf8.encode(kTestString); + + Uint8List data = Uint8List.fromList(list); + + final Reference ref = + storage.ref('flutter-tests').child('flt-ok.txt'); + + final TaskSnapshot complete = await ref.putData( + data, + SettableMetadata( + contentLanguage: 'en', + ), + ); + + expect(complete.metadata?.size, kTestString.length); + // Metadata isn't saved on objects when using the emulator which fails test + // expect(complete.metadata?.contentLanguage, 'en'); + }); + + //TODO(pr-mais): causes the emulator to crash + // test('errors if permission denied', () async { + // List list = utf8.encode('hello world'); + // Uint8List data = Uint8List.fromList(list); + + // final Reference ref = storage.ref('/uploadNope.jpeg'); + + // await expectLater( + // () => ref.putData(data), + // throwsA(isA() + // .having((e) => e.code, 'code', 'unauthorized') + // .having((e) => e.message, 'message', + // 'User is not authorized to perform the desired action.'))); + // }); + }, + skip: kIsWeb, + ); + + group('putBlob', () { + test( + 'throws [UnimplementedError] for native platforms', + () async { + final File file = await createFile('flt-ok.txt'); + final Reference ref = + storage.ref('flutter-tests').child('flt-ok.txt'); + + await expectLater( + () => ref.putBlob( + file, + SettableMetadata( + contentLanguage: 'en', + customMetadata: {'activity': 'test'}, + ), + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'putBlob() is not supported on native platforms. Use [put], [putFile] or [putString] instead.', + ), + ), + ); + + // This *must* be skipped in web, the test is intended for native platforms. + }, + skip: kIsWeb, + ); + }); + + group( + 'putFile', + () { + test( + 'uploads a file', + () async { + final File file = await createFile('flt-ok.txt'); + + final Reference ref = + storage.ref('flutter-tests').child('flt-ok.txt'); + + final TaskSnapshot complete = await ref.putFile( + file, + SettableMetadata( + contentLanguage: 'en', + customMetadata: {'activity': 'test'}, + ), + ); + + expect(complete.metadata?.size, kTestString.length); + // Metadata isn't saved on objects when using the emulator which fails test + // expect(complete.metadata?.contentLanguage, 'en'); + // expect(complete.metadata?.customMetadata!['activity'], 'test'); + }, + ); + + // TODO(ehesp): Emulator rules issue - comment back in once fixed + // test('errors if permission denied', () async { + // File file = await createFile('flt-ok.txt'); + // final Reference ref = storage.ref('uploadNope.jpeg'); + + // await expectLater( + // () => ref.putFile(file), + // throwsA(isA() + // .having((e) => e.code, 'code', 'unauthorized') + // .having((e) => e.message, 'message', + // 'User is not authorized to perform the desired action.'))); + // }); + }, + // putFile is not supported in web. + // iOS & macOS work locally but times out on CI. We ought to check this periodically + // as it may be OS version specific. + skip: kIsWeb || + defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS, + ); + + group('putString', () { + test('uploads a string', () async { + final Reference ref = storage.ref('flutter-tests').child('flt-ok.txt'); + final TaskSnapshot complete = await ref.putString('data'); + expect(complete.totalBytes, greaterThan(0)); + }); + + // Emulator continues to make request rather than throw unauthorized exception as expected + // test('errors if permission denied', () async { + // final Reference ref = storage.ref('uploadNope.jpeg'); + // + // await expectLater( + // () => ref.putString('data'), + // throwsA( + // isA() + // .having((e) => e.code, 'code', 'unauthorized') + // .having( + // (e) => e.message, + // 'message', + // 'User is not authorized to perform the desired action.', + // ), + // ), + // ); + // }); + }); + + group('updateMetadata', () { + test('updates metadata', () async { + Reference ref = storage.ref('flutter-tests').child('flt-ok.txt'); + FullMetadata fullMetadata = await ref + .updateMetadata(SettableMetadata(customMetadata: {'foo': 'bar'})); + expect(fullMetadata.customMetadata!['foo'], 'bar'); + }); + + test('errors if property does not exist', () async { + Reference ref = storage.ref('flutter-tests/iDoNotExist.jpeg'); + + // - + await expectLater( + () => ref.updateMetadata(SettableMetadata(contentType: 'unknown')), + throwsA( + isA() + .having((e) => e.code, 'code', 'unauthorized') + .having( + (e) => e.message, + 'message', + 'User is not authorized to perform the desired action', + ), + ), + ); + }); + + test( + 'errors if permission denied', + () async { + final ref = storage.ref('uploadNope.jpeg'); + await expectLater( + () => ref.updateMetadata(SettableMetadata(contentType: 'jpeg')), + throwsA( + isA() + .having((e) => e.code, 'code', 'unauthorized') + .having( + (e) => e.message, + 'message', + 'User is not authorized to perform the desired action', + ), + ), + ); + }, + // TODO(salakar): Firebase storage emulator incorrectly returns `object-not-found` instead of `unauthorized`. + skip: true, + ); + }); + + group( + 'writeToFile', + () { + test('downloads a file', () async { + File file = await createFile('ok.jpeg'); + TaskSnapshot complete = + await storage.ref('flutter-tests/ok.txt').writeToFile(file); + expect(complete.bytesTransferred, complete.totalBytes); + expect(complete.state, TaskState.success); + }); + + test('errors if permission denied', () async { + File file = await createFile('not.jpeg'); + final Reference ref = storage.ref('/nope.jpeg'); + + await expectLater( + () => ref.writeToFile(file), + throwsA( + isA() + .having((e) => e.code, 'code', 'unauthorized') + .having( + (e) => e.message, + 'message', + 'User is not authorized to perform the desired action', + ), + ), + ); + // TODO(hosung): Firebase cpp sdk does not throw error. + }, skip: Platform.isLinux); + // writeToFile is not supported in web + }, + skip: kIsWeb, + ); + + test('toString', () async { + expect( + storage.ref('/uploadNope.jpeg').toString(), + equals('Reference(app: [DEFAULT], fullPath: uploadNope.jpeg)'), + ); + }); + }); +} diff --git a/packages/firebase_storage/example/integration_test/task_e2e.dart b/packages/firebase_storage/example/integration_test/task_e2e.dart new file mode 100644 index 0000000..9f366c4 --- /dev/null +++ b/packages/firebase_storage/example/integration_test/task_e2e.dart @@ -0,0 +1,275 @@ +// Copyright 2022, the Chromium 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'; + +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import './test_utils.dart'; + +void setupTaskTests() { + group('Task', () { + late FirebaseStorage storage; + late File file; + late Reference uploadRef; + late Reference downloadRef; + + setUpAll(() async { + storage = FirebaseStorage.instance; + uploadRef = storage.ref('flutter-tests').child('ok.txt'); + downloadRef = storage.ref('flutter-tests/ok.txt'); // 15mb + }); + + group('pause() resume() onComplete()', () { + late Task? task; + + setUp(() { + task = null; + }); + + Future _testPauseTask(String type) async { + List snapshots = []; + late FirebaseException streamError; + expect(task!.snapshot.state, TaskState.running); + + task!.snapshotEvents.listen( + (TaskSnapshot snapshot) { + snapshots.add(snapshot); + }, + onError: (error) { + streamError = error; + }, + cancelOnError: true, + ); + + // TODO(Salakar): Known issue with iOS SDK where pausing immediately will cause an 'unknown' error. + if (defaultTargetPlatform == TargetPlatform.iOS) { + await task!.snapshotEvents.first; + await Future.delayed(const Duration(milliseconds: 750)); + } + + // TODO(Salakar): Known issue with iOS where pausing/resuming doesn't immediately return as paused/resumed 'true'. + if (defaultTargetPlatform != TargetPlatform.iOS) { + bool? paused = await task!.pause(); + expect(paused, isTrue); + expect(task!.snapshot.state, TaskState.paused); + + bool? resumed = await task!.resume(); + expect(resumed, isTrue); + expect(task!.snapshot.state, TaskState.running); + } + + TaskSnapshot? snapshot = await task; + expect(task!.snapshot.state, TaskState.success); + expect(snapshot!.state, TaskState.success); + + expect(snapshot.totalBytes, snapshot.bytesTransferred); + + expect(streamError, isNull); + // TODO(Salakar): Known issue with iOS where pausing/resuming doesn't immediately return as paused/resumed 'true'. + if (defaultTargetPlatform != TargetPlatform.iOS) { + expect( + snapshots, + anyElement( + predicate( + (TaskSnapshot element) => element.state == TaskState.paused, + ), + ), + ); + expect( + snapshots, + anyElement( + predicate( + (TaskSnapshot element) => element.state == TaskState.running, + ), + ), + ); + } + } + + // TODO(Salakar): Test fails on emulator. + test( + 'successfully pauses and resumes a download task', + () async { + file = await createFile('ok.jpeg'); + await downloadRef.writeToFile(file); + await _testPauseTask('Download'); + // Skip on web: There's no DownloadTask on web. + }, + skip: true, + ); + + // TODO(Salakar): Test is flaky on CI - needs investigating ('[firebase_storage/unknown] An unknown error occurred, please check the server response.') + test( + 'successfully pauses and resumes a upload task', + () async { + await uploadRef.putString('This is an upload task!'); + await _testPauseTask('Upload'); + }, + skip: true, + ); + + //TODO(pr-mais): causes the emulator to crash + // test('handles errors, e.g. if permission denied', () async { + // late FirebaseException streamError; + + // List list = utf8.encode('hello world'); + // Uint8List data = Uint8List.fromList(list); + // UploadTask task = storage.ref('/uploadNope.jpeg').putData(data); + + // expect(task.snapshot.state, TaskState.running); + + // task.snapshotEvents.listen((TaskSnapshot snapshot) { + // // noop + // }, onError: (error) { + // streamError = error; + // }, cancelOnError: true); + + // await expectLater( + // task, + // throwsA(isA() + // .having((e) => e.code, 'code', 'unauthorized')), + // ); + + // expect(streamError.plugin, 'firebase_storage'); + // expect(streamError.code, 'unauthorized'); + // expect(streamError.message, + // 'User is not authorized to perform the desired action.'); + + // expect(task.snapshot.state, TaskState.error); + // }); + }); + + group('snapshot', () { + test( + 'returns the latest snapshot for download task', + () async { + file = await createFile('ok.jpeg'); + final downloadTask = downloadRef.writeToFile(file); + + expect(downloadTask.snapshot, isNotNull); + + TaskSnapshot completedSnapshot = await downloadTask; + final snapshot = downloadTask.snapshot; + + expect(snapshot, isA()); + expect(snapshot.state, TaskState.success); + expect(snapshot.bytesTransferred, completedSnapshot.bytesTransferred); + expect(snapshot.totalBytes, completedSnapshot.totalBytes); + expect(snapshot.metadata, isNull); + }, + // TODO(salakar): this test is flakey when using the Firebase Storage Emulator. + skip: true, + // There's no DownloadTask on web. + // skip: kIsWeb + retry: 2, + ); + + test( + 'returns the latest snapshot for upload task', + () async { + final uploadTask = uploadRef.putString('This is an upload task!'); + expect(uploadTask.snapshot, isNotNull); + + TaskSnapshot completedSnapshot = await uploadTask; + final snapshot = uploadTask.snapshot; + expect(snapshot, isA()); + expect(snapshot.bytesTransferred, completedSnapshot.bytesTransferred); + expect(snapshot.totalBytes, completedSnapshot.totalBytes); + expect(snapshot.metadata, isA()); + }, + retry: 2, + // TODO(salakar): this test is flakey when using the Firebase Storage Emulator. + skip: true, + ); + + test( + 'upload task to a custom bucket', + () async { + String secondaryBucket = 'flutterfire-e2e-tests-two'; + Reference storageReference = + FirebaseStorage.instanceFor(bucket: secondaryBucket) + .ref('flutter-tests/ok.txt'); + + expect(storageReference.bucket, secondaryBucket); + + final task = storageReference.putString('test second bucket'); + final snapshot = await task; + + expect(snapshot.ref.bucket, secondaryBucket); + + String url = await storageReference.getDownloadURL(); + + expect(url, contains('/$secondaryBucket/')); + }, + // TODO(salakar): blocked by https://github.com/firebase/firebase-tools/issues/3390 + skip: true, + ); + }); + + // TODO(Salakar): Test fails on Firebase Storage emulator since the task completes before .cancel() has a chance to be called. + group( + 'cancel()', + () { + late Task task; + + Future _testCancelTask() async { + List snapshots = []; + late FirebaseException streamError; + expect(task.snapshot.state, TaskState.running); + + task.snapshotEvents.listen( + (TaskSnapshot snapshot) { + snapshots.add(snapshot); + }, + onError: (error) { + streamError = error; + }, + cancelOnError: true, + ); + + bool canceled = await task.cancel(); + expect(canceled, isTrue); + expect(task.snapshot.state, TaskState.canceled); + + await expectLater( + task, + throwsA( + isA() + .having((e) => e.code, 'code', 'canceled'), + ), + ); + + expect(task.snapshot.state, TaskState.canceled); + + expect(streamError, isNotNull); + expect(streamError.code, 'canceled'); + // Expecting there to only be running states, canceled should not get sent as an event. + expect( + snapshots.every((snapshot) => snapshot.state == TaskState.running), + isTrue, + ); + } + + test( + 'successfully cancels download task', + () async { + file = await createFile('ok.jpeg'); + task = downloadRef.writeToFile(file); + await _testCancelTask(); + // There's no DownloadTask on web. + }, + ); + + test('successfully cancels upload task', () async { + task = uploadRef.putString('This is an upload task!'); + await _testCancelTask(); + }); + }, + skip: true, + ); + }); +} diff --git a/packages/firebase_storage/example/integration_test/test_utils.dart b/packages/firebase_storage/example/integration_test/test_utils.dart new file mode 100644 index 0000000..f1d6a06 --- /dev/null +++ b/packages/firebase_storage/example/integration_test/test_utils.dart @@ -0,0 +1,75 @@ +// Copyright 2022, the Chromium 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'; +import 'dart:math'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; + +import '../lib/firebase_options.dart'; + +final String kTestString = + ([]..length = int.parse('${pow(2, 12)}')).join(_getRandomString(8)) * 100; +const String kTestStorageBucket = 'flutterfire-e2e-tests.appspot.com'; + +const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; +Random _random = Random(); +String _getRandomString(int length) => String.fromCharCodes( + Iterable.generate( + length, + (_) => _chars.codeUnitAt(_random.nextInt(_chars.length)), + ), + ); + +String get testEmulatorHost { + if (defaultTargetPlatform == TargetPlatform.android && !kIsWeb) { + return '10.0.2.2'; + } + return 'localhost'; +} + +const int testEmulatorPort = 9199; + +// Creates a test file with a specified name to +// a locally directory +Future createFile(String name) async { + final Directory systemTempDir = Directory.systemTemp; + final File file = await File('${systemTempDir.path}/$name').create(); + await file.writeAsString(kTestString); + return file; +} + +// Initializes a secondary app with or without a +// default storageBucket value in FirebaseOptions for testing +Future testInitializeSecondaryApp({ + bool withDefaultBucket = true, +}) async { + final String testAppName = + withDefaultBucket ? 'testapp' : 'testapp-no-bucket'; + + FirebaseOptions testAppOptions; + if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) { + testAppOptions = FirebaseOptions( + appId: DefaultFirebaseOptions.currentPlatform.appId, + apiKey: DefaultFirebaseOptions.currentPlatform.apiKey, + projectId: DefaultFirebaseOptions.currentPlatform.projectId, + messagingSenderId: + DefaultFirebaseOptions.currentPlatform.messagingSenderId, + iosBundleId: DefaultFirebaseOptions.currentPlatform.iosBundleId, + storageBucket: withDefaultBucket ? kTestStorageBucket : null, + ); + } else { + testAppOptions = FirebaseOptions( + appId: DefaultFirebaseOptions.currentPlatform.appId, + apiKey: DefaultFirebaseOptions.currentPlatform.apiKey, + projectId: DefaultFirebaseOptions.currentPlatform.projectId, + messagingSenderId: + DefaultFirebaseOptions.currentPlatform.messagingSenderId, + storageBucket: withDefaultBucket ? kTestStorageBucket : null, + ); + } + + return Firebase.initializeApp(name: testAppName, options: testAppOptions); +} diff --git a/packages/firebase_storage/example/lib/firebase_options.dart b/packages/firebase_storage/example/lib/firebase_options.dart new file mode 100644 index 0000000..97dffb5 --- /dev/null +++ b/packages/firebase_storage/example/lib/firebase_options.dart @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + // Note: To find out if you are using the Tizen platform, refer to the link below. + // https://github.com/flutter-tizen/flutter-tizen/issues/482#issuecomment-1441139704 + return tizen; + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions tizen = FirebaseOptions( + apiKey: 'PLACEHOLDER', + appId: 'PLACEHOLDER', + messagingSenderId: 'PLACEHOLDER', + projectId: 'PLACEHOLDER', + databaseURL: 'PLACEHOLDER', + storageBucket: 'PLACEHOLDER', + ); +} diff --git a/packages/firebase_storage/example/lib/main.dart b/packages/firebase_storage/example/lib/main.dart new file mode 100755 index 0000000..9df5d2a --- /dev/null +++ b/packages/firebase_storage/example/lib/main.dart @@ -0,0 +1,386 @@ +// Copyright 2022, the Chromium 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:io' as io; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'firebase_options.dart'; +import 'save_as/save_as.dart'; + +bool USE_FIRESTORAGE_EMULATOR = false; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + if (USE_FIRESTORAGE_EMULATOR) { + final emulatorHost = + (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) + ? '10.0.2.2' + : 'localhost'; + + await FirebaseStorage.instance.useStorageEmulator(emulatorHost, 9199); + } + runApp(StorageExampleApp()); +} + +/// Enum representing the upload task types the example app supports. +enum UploadType { + /// Uploads a randomly generated string (as a file) to Storage. + string, + + /// Uploads a file from the device. + file, + + /// Clears any tasks from the list. + clear, + + // Get uploaded files list. + list, +} + +/// The entry point of the application. +/// +/// Returns a [MaterialApp]. +class StorageExampleApp extends StatelessWidget { + StorageExampleApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Storage Example App', + theme: ThemeData.dark(), + home: Scaffold( + body: TaskManager(), + ), + ); + } +} + +/// A StatefulWidget which keeps track of the current uploaded files. +class TaskManager extends StatefulWidget { + // ignore: public_member_api_docs + TaskManager({Key? key}) : super(key: key); + + @override + State createState() { + return _TaskManager(); + } +} + +class _TaskManager extends State { + List _uploadTasks = []; + + /// The user selects a file, and the task is added to the list. + Future uploadFile(XFile? file) async { + if (file == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No file was selected'), + ), + ); + + return null; + } + + UploadTask uploadTask; + + // Create a Reference to the file + Reference ref = FirebaseStorage.instance + .ref() + .child('flutter-tests') + .child('/some-image.jpg'); + + final metadata = SettableMetadata( + contentType: 'image/jpeg', + customMetadata: {'picked-file-path': file.path}, + ); + + if (kIsWeb) { + uploadTask = ref.putData(await file.readAsBytes(), metadata); + } else { + uploadTask = ref.putFile(io.File(file.path), metadata); + } + + return Future.value(uploadTask); + } + + /// A new string is uploaded to storage. + UploadTask uploadString() { + const String putStringText = + 'This upload has been generated using the putString method! Check the metadata too!'; + + // Create a Reference to the file + Reference ref = FirebaseStorage.instance + .ref() + .child('flutter-tests') + .child('/put-string-example.txt'); + + // Start upload of putString + return ref.putString( + putStringText, + metadata: SettableMetadata( + contentLanguage: 'en', + customMetadata: {'example': 'putString'}, + ), + ); + } + + /// Handles the user pressing the PopupMenuItem item. + Future handleUploadType(UploadType type) async { + switch (type) { + case UploadType.string: + setState(() { + _uploadTasks = [..._uploadTasks, uploadString()]; + }); + break; + case UploadType.file: + final file = await ImagePicker().pickImage(source: ImageSource.gallery); + UploadTask? task = await uploadFile(file); + + if (task != null) { + setState(() { + _uploadTasks = [..._uploadTasks, task]; + }); + } + break; + case UploadType.clear: + setState(() { + _uploadTasks = []; + }); + break; + case UploadType.list: + Reference ref = FirebaseStorage.instance.ref().child('flutter-tests'); + _getList(ref); + break; + } + } + + void _removeTaskAtIndex(int index) { + setState(() { + _uploadTasks = _uploadTasks..removeAt(index); + }); + } + + Future _downloadBytes(Reference ref) async { + final bytes = await ref.getData(); + // Download... + await saveAsBytes(bytes!, 'some-image.jpg'); + } + + Future _downloadLink(Reference ref) async { + final link = await ref.getDownloadURL(); + + await Clipboard.setData( + ClipboardData( + text: link, + ), + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Success!\n Copied download URL to Clipboard!', + ), + ), + ); + } + + Future _downloadFile(Reference ref) async { + final io.Directory systemTempDir = io.Directory.systemTemp; + final io.File tempFile = io.File('${systemTempDir.path}/temp-${ref.name}'); + if (tempFile.existsSync()) await tempFile.delete(); + + await ref.writeToFile(tempFile); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Success!\n Downloaded ${ref.name} \n from bucket: ${ref.bucket}\n ' + 'at path: ${ref.fullPath} \n' + 'Wrote "${ref.fullPath}" to tmp-${ref.name}', + ), + ), + ); + } + + Future _getList(Reference ref) async { + ListResult result = await ref.list(ListOptions(maxResults: 3)); + final itemsStringBuffer = StringBuffer(); + result.items.forEach((item) { + itemsStringBuffer.writeln(item.fullPath); + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + itemsStringBuffer.toString(), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Storage Example App'), + actions: [ + PopupMenuButton( + onSelected: handleUploadType, + icon: const Icon(Icons.add), + itemBuilder: (context) => [ + const PopupMenuItem( + // ignore: sort_child_properties_last + child: Text('Upload string'), + value: UploadType.string, + ), + const PopupMenuItem( + // ignore: sort_child_properties_last + child: Text('Upload local file'), + value: UploadType.file, + ), + if (_uploadTasks.isNotEmpty) + const PopupMenuItem( + // ignore: sort_child_properties_last + child: Text('Clear list'), + value: UploadType.clear, + ), + PopupMenuDivider(), + const PopupMenuItem( + child: Text('List'), + value: UploadType.list, + ), + ], + ) + ], + ), + body: _uploadTasks.isEmpty + ? const Center(child: Text("Press the '+' button to add a new file.")) + : ListView.builder( + itemCount: _uploadTasks.length, + itemBuilder: (context, index) => UploadTaskListTile( + task: _uploadTasks[index], + onDismissed: () => _removeTaskAtIndex(index), + onDownloadLink: () async { + return _downloadLink(_uploadTasks[index].snapshot.ref); + }, + onDownload: () async { + if (kIsWeb) { + return _downloadBytes(_uploadTasks[index].snapshot.ref); + } else { + return _downloadFile(_uploadTasks[index].snapshot.ref); + } + }, + ), + ), + ); + } +} + +/// Displays the current state of a single UploadTask. +class UploadTaskListTile extends StatelessWidget { + // ignore: public_member_api_docs + const UploadTaskListTile({ + Key? key, + required this.task, + required this.onDismissed, + required this.onDownload, + required this.onDownloadLink, + }) : super(key: key); + + /// The [UploadTask]. + final UploadTask /*!*/ task; + + /// Triggered when the user dismisses the task from the list. + final VoidCallback /*!*/ onDismissed; + + /// Triggered when the user presses the download button on a completed upload task. + final VoidCallback /*!*/ onDownload; + + /// Triggered when the user presses the "link" button on a completed upload task. + final VoidCallback /*!*/ onDownloadLink; + + /// Displays the current transferred bytes of the task. + String _bytesTransferred(TaskSnapshot snapshot) { + return '${snapshot.bytesTransferred}/${snapshot.totalBytes}'; + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: task.snapshotEvents, + builder: ( + BuildContext context, + AsyncSnapshot asyncSnapshot, + ) { + Widget subtitle = const Text('---'); + TaskSnapshot? snapshot = asyncSnapshot.data; + TaskState? state = snapshot?.state; + + if (asyncSnapshot.hasError) { + if (asyncSnapshot.error is FirebaseException && + // ignore: cast_nullable_to_non_nullable + (asyncSnapshot.error as FirebaseException).code == 'canceled') { + subtitle = const Text('Upload canceled.'); + } else { + // ignore: avoid_print + print(asyncSnapshot.error); + subtitle = const Text('Something went wrong.'); + } + } else if (snapshot != null) { + subtitle = Text('$state: ${_bytesTransferred(snapshot)} bytes sent'); + } + + return Dismissible( + key: Key(task.hashCode.toString()), + onDismissed: ($) => onDismissed(), + child: ListTile( + title: Text('Upload Task #${task.hashCode}'), + subtitle: subtitle, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (state == TaskState.running) + IconButton( + icon: const Icon(Icons.pause), + onPressed: task.pause, + ), + if (state == TaskState.running) + IconButton( + icon: const Icon(Icons.cancel), + onPressed: task.cancel, + ), + if (state == TaskState.paused) + IconButton( + icon: const Icon(Icons.file_upload), + onPressed: task.resume, + ), + if (state == TaskState.success) + IconButton( + icon: const Icon(Icons.file_download), + onPressed: onDownload, + ), + if (state == TaskState.success) + IconButton( + icon: const Icon(Icons.link), + onPressed: onDownloadLink, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/packages/firebase_storage/example/lib/save_as/save_as.dart b/packages/firebase_storage/example/lib/save_as/save_as.dart new file mode 100644 index 0000000..c126e02 --- /dev/null +++ b/packages/firebase_storage/example/lib/save_as/save_as.dart @@ -0,0 +1,5 @@ +// Copyright 2022, the Chromium 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. + +export 'save_as_interface.dart' if (dart.library.html) 'save_as_html.dart'; diff --git a/packages/firebase_storage/example/lib/save_as/save_as_html.dart b/packages/firebase_storage/example/lib/save_as/save_as_html.dart new file mode 100644 index 0000000..8ef65bc --- /dev/null +++ b/packages/firebase_storage/example/lib/save_as/save_as_html.dart @@ -0,0 +1,49 @@ +// Copyright 2022, the Chromium 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. + +// ignore: avoid_web_libraries_in_flutter +import 'dart:html'; +import 'dart:typed_data'; + +/// Initializes a DOM container where we can host elements. +Element _ensureInitialized(String id) { + var target = querySelector('#$id'); + if (target == null) { + final Element targetElement = Element.tag('flt-x-file')..id = id; + + querySelector('body')?.children.add(targetElement); + target = targetElement; + } + return target; +} + +AnchorElement _createAnchorElement(String href, String suggestedName) { + return AnchorElement(href: href)..download = suggestedName; +} + +/// Add an element to a container and click it +void _addElementToContainerAndClick(Element container, Element element) { + // Add the element and click it + // All previous elements will be removed before adding the new one + container.children.add(element); + element.click(); +} + +/// Present a dialog so the user can save as... a bunch of bytes. +Future saveAsBytes(Uint8List bytes, String suggestedName) async { + // Convert bytes to an ObjectUrl through Blob + final blob = Blob([bytes]); + final path = Url.createObjectUrl(blob); + + // Create a DOM container where we can host the anchor. + final target = _ensureInitialized('__x_file_dom_element'); + + // Create an tag with the appropriate download attributes and click it + // May be overridden with XFileTestOverrides + final AnchorElement element = _createAnchorElement(path, suggestedName); + + // Clear the children in our container so we can add an element to click + target.children.clear(); + _addElementToContainerAndClick(target, element); +} diff --git a/packages/firebase_storage/example/lib/save_as/save_as_interface.dart b/packages/firebase_storage/example/lib/save_as/save_as_interface.dart new file mode 100644 index 0000000..e4d8a5a --- /dev/null +++ b/packages/firebase_storage/example/lib/save_as/save_as_interface.dart @@ -0,0 +1,10 @@ +// Copyright 2022, the Chromium 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:typed_data'; + +/// Present a dialog so the user can save as... a bunch of bytes. +Future saveAsBytes(Uint8List bytes, String suggestedName) async { + return; +} diff --git a/packages/firebase_storage/example/pubspec.yaml b/packages/firebase_storage/example/pubspec.yaml new file mode 100644 index 0000000..154cc1d --- /dev/null +++ b/packages/firebase_storage/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: firebase_storage_tizen_example +description: Demonstrates how to use the firebase_storage plugin. + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + firebase_core: 2.4.1 + firebase_core_tizen: ^1.0.0 + firebase_storage: ^11.0.10 + firebase_storage_tizen: ^0.1.0 + flutter: + sdk: flutter + image_picker: ^0.8.6 + image_picker_tizen: ^2.2.0 + +dependency_overrides: + firebase_core_tizen: + path: ../../firebase_core + firebase_storage_tizen: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: ^2.0.1 + +flutter: + uses-material-design: true + assets: + - assets/hello.txt diff --git a/packages/firebase_storage/example/tizen/.gitignore b/packages/firebase_storage/example/tizen/.gitignore new file mode 100644 index 0000000..750f3af --- /dev/null +++ b/packages/firebase_storage/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/firebase_storage/example/tizen/App.cs b/packages/firebase_storage/example/tizen/App.cs new file mode 100644 index 0000000..6dd4a63 --- /dev/null +++ b/packages/firebase_storage/example/tizen/App.cs @@ -0,0 +1,20 @@ +ο»Ώusing Tizen.Flutter.Embedding; + +namespace Runner +{ + public class App : FlutterApplication + { + protected override void OnCreate() + { + base.OnCreate(); + + GeneratedPluginRegistrant.RegisterPlugins(this); + } + + static void Main(string[] args) + { + var app = new App(); + app.Run(args); + } + } +} diff --git a/packages/firebase_storage/example/tizen/Runner.csproj b/packages/firebase_storage/example/tizen/Runner.csproj new file mode 100644 index 0000000..f4e369d --- /dev/null +++ b/packages/firebase_storage/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ +ο»Ώ + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/firebase_storage/example/tizen/shared/res/ic_launcher.png b/packages/firebase_storage/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/packages/firebase_storage/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/firebase_storage/example/tizen/tizen-manifest.xml b/packages/firebase_storage/example/tizen/tizen-manifest.xml new file mode 100644 index 0000000..d1ac0d8 --- /dev/null +++ b/packages/firebase_storage/example/tizen/tizen-manifest.xml @@ -0,0 +1,18 @@ + + + + + + ic_launcher.png + + + + T-INFOLINK2021-1000 + + + + http://tizen.org/privilege/internet + http://tizen.org/privilege/mediastorage + http://tizen.org/privilege/appmanager.launch + + diff --git a/packages/firebase_storage/pubspec.yaml b/packages/firebase_storage/pubspec.yaml new file mode 100644 index 0000000..56568a9 --- /dev/null +++ b/packages/firebase_storage/pubspec.yaml @@ -0,0 +1,30 @@ +name: firebase_storage_tizen +description: Flutter plugin for Firebase Cloud Storage, a powerful, simple, and + cost-effective object storage service for Tizen. +version: 0.1.0 +homepage: + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + firebase_core_tizen: ^1.0.0 + firebase_storage: ^11.0.10 + flutter: + sdk: flutter + +dependency_overrides: + firebase_core_tizen: + path: ../firebase_core + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + tizen: + pluginClass: FirebaseStorageTizenPlugin + fileName: firebase_storage_tizen_plugin.h diff --git a/packages/firebase_storage/tizen/.gitignore b/packages/firebase_storage/tizen/.gitignore new file mode 100644 index 0000000..a2a7d62 --- /dev/null +++ b/packages/firebase_storage/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/firebase_storage/tizen/build_def.prop b/packages/firebase_storage/tizen/build_def.prop new file mode 100644 index 0000000..085fff2 --- /dev/null +++ b/packages/firebase_storage/tizen/build_def.prop @@ -0,0 +1,2 @@ +PREBUILD_COMMAND = ./tar_url.sh \ + https://raw.githubusercontent.com/hs0225/download/firebase-sdk/firebaseSDK-tizen-1.0.1.tar.gz diff --git a/packages/firebase_storage/tizen/inc/firebase_storage_tizen_plugin.h b/packages/firebase_storage/tizen/inc/firebase_storage_tizen_plugin.h new file mode 100644 index 0000000..abd7ec4 --- /dev/null +++ b/packages/firebase_storage/tizen/inc/firebase_storage_tizen_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FIREBASE_STORAGE_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_FIREBASE_STORAGE_TIZEN_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FirebaseStorageTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FIREBASE_STORAGE_TIZEN_PLUGIN_H_ diff --git a/packages/firebase_storage/tizen/project_def.prop b/packages/firebase_storage/tizen/project_def.prop new file mode 100644 index 0000000..56ef1b9 --- /dev/null +++ b/packages/firebase_storage/tizen/project_def.prop @@ -0,0 +1,30 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = firebase_storage_tizen_plugin +type = sharedLib +profile = common-7.0 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# Custom defines +FIREBASE_SDK_DIR = $(subst $() ,\ ,$(FLUTTER_BUILD_DIR))/.firebaseSDK +FIREBASE_INC_DIR = $(FIREBASE_SDK_DIR)/inc +FIREBASE_LIB_DIR = $(FIREBASE_SDK_DIR)/lib/$(BUILD_ARCH) + +# User includes +USER_INC_DIRS = inc src $(FIREBASE_INC_DIR) +USER_INC_FILES = +USER_CPP_INC_FILES = + +# User libs +USER_LIBS = firebase_app firebase_storage +USER_LIB_DIRS = lib/$(BUILD_ARCH) $(FIREBASE_LIB_DIR) +USER_LFLAGS = -Wl,-rpath='$$ORIGIN' diff --git a/packages/firebase_storage/tizen/src/firebase_storage_error.cc b/packages/firebase_storage/tizen/src/firebase_storage_error.cc new file mode 100644 index 0000000..3dcf1b5 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_error.cc @@ -0,0 +1,77 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "firebase_storage_error.h" + +std::string FirebaseStorageError::GetCodeString() const { + switch (code_) { + case Code::kObjectNotFound: + return "object-not-found"; + case Code::kBucketNotFound: + return "bucket-not-found"; + case Code::kProjectNotFound: + return "project-not-found"; + case Code::kQuotaExceeded: + return "quota-exceeded"; + case Code::kUnauthenticated: + return "unauthenticated"; + case Code::kUnauthorized: + return "unauthorized"; + case Code::kRetryLimitExceeded: + return "retry-limit-exceeded"; + case Code::kNonMatchingChecksum: + return "invalid-checksum"; + case Code::kDownloadSizeExceeded: + return "download-size-exceeded"; + case Code::kCancelled: + return "canceled"; + case Code::kInvalidArgument: + return "invalid-argument"; + case Code::kAppNotFound: + return "app-not-found"; + case Code::kNotSupported: + return "not-supported"; + case Code::kInvalidString: + return "invalid-string"; + case Code::kSystemError: + return "system-error"; + case Code::KTaskNotFound: + return "task-not-found"; + case Code::kUnknown: + default: + return "unknown"; + } +} + +std::string FirebaseStorageError::GetErrorString() const { + switch (code_) { + case Code::kUnknown: + case Code::kObjectNotFound: + case Code::kBucketNotFound: + case Code::kProjectNotFound: + case Code::kQuotaExceeded: + case Code::kUnauthenticated: + case Code::kUnauthorized: + case Code::kRetryLimitExceeded: + case Code::kNonMatchingChecksum: + case Code::kDownloadSizeExceeded: + case Code::kCancelled: + return firebase::storage::GetErrorMessage( + firebase::storage::Error(code_)); + case Code::kInvalidArgument: + return "Invalid argument."; + case Code::kAppNotFound: + return "No app is configured with that name."; + case Code::kNotSupported: + return "The feature is not supported."; + case Code::kInvalidString: + return "The string contains invalid characters."; + case Code::kSystemError: + return "A system error occurred."; + case Code::KTaskNotFound: + return "A task does not exist."; + default: + return "An unknown error occurred."; + } +} diff --git a/packages/firebase_storage/tizen/src/firebase_storage_error.h b/packages/firebase_storage/tizen/src/firebase_storage_error.h new file mode 100644 index 0000000..fb38128 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_error.h @@ -0,0 +1,68 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_FIREBASE_STORAGE_ERROR_H_ +#define FLUTTER_PLUGIN_FIREBASE_STORAGE_ERROR_H_ + +#include + +#include + +using namespace firebase::storage; + +class FirebaseStorageError { + public: + enum class Code { + kUnknown = Error::kErrorUnknown, + kObjectNotFound = Error::kErrorObjectNotFound, + kBucketNotFound = Error::kErrorBucketNotFound, + kProjectNotFound = Error::kErrorProjectNotFound, + kQuotaExceeded = Error::kErrorQuotaExceeded, + kUnauthenticated = Error::kErrorUnauthenticated, + kUnauthorized = Error::kErrorUnauthorized, + kRetryLimitExceeded = Error::kErrorRetryLimitExceeded, + kNonMatchingChecksum = Error::kErrorNonMatchingChecksum, + kDownloadSizeExceeded = Error::kErrorDownloadSizeExceeded, + kCancelled = Error::kErrorCancelled, + kInvalidArgument = 20, + kAppNotFound, + kNotSupported, + kInvalidString, + kSystemError, + KTaskNotFound, + }; + + FirebaseStorageError(Code code, const std::string& message) + : code_(code), message_(message) {} + + FirebaseStorageError(Code code) : code_(code), message_(GetErrorString()) {} + + FirebaseStorageError(int code) + : code_(FirebaseStorageError::Code(code)), message_(GetErrorString()) {} + + FirebaseStorageError(const FirebaseStorageError& other) { + this->code_ = other.code_; + this->message_ = other.message_; + } + + ~FirebaseStorageError() = default; + + FirebaseStorageError& operator=(const FirebaseStorageError& other) { + this->code_ = other.code_; + this->message_ = other.message_; + return *this; + } + + Code GetCode() const { return code_; } + std::string GetMessage() const { return message_; } + std::string GetCodeString() const; + + private: + Code code_; + std::string message_; + + std::string GetErrorString() const; +}; + +#endif // FLUTTER_PLUGIN_FIREBASE_STORAGE_ERROR_H_ diff --git a/packages/firebase_storage/tizen/src/firebase_storage_listener.cc b/packages/firebase_storage/tizen/src/firebase_storage_listener.cc new file mode 100644 index 0000000..2ca5cc7 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_listener.cc @@ -0,0 +1,29 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "firebase_storage_listener.h" + +#include + +#include + +StorageListener::StorageListener(const utils::StorageTaskData& task_data, + const std::shared_ptr channel) + : task_data_(task_data), channel_(std::move(channel)) {} + +void StorageListener::OnPaused(firebase::storage::Controller* controller) { + channel_->InvokeMethod( + "Task#onPaused", + std::make_unique( + utils::GetTaskEventValue(task_data_, controller->bytes_transferred(), + controller->total_byte_count()))); +} + +void StorageListener::OnProgress(firebase::storage::Controller* controller) { + channel_->InvokeMethod( + "Task#onProgress", + std::make_unique( + utils::GetTaskEventValue(task_data_, controller->bytes_transferred(), + controller->total_byte_count()))); +} diff --git a/packages/firebase_storage/tizen/src/firebase_storage_listener.h b/packages/firebase_storage/tizen/src/firebase_storage_listener.h new file mode 100644 index 0000000..bdd040c --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_listener.h @@ -0,0 +1,27 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_FIREBASE_STORAGE_LISTENER_H_ +#define FLUTTER_PLUGIN_FIREBASE_STORAGE_LISTENER_H_ + +#include "firebase/storage.h" +#include "firebase_storage_utils.h" +#include "flutter_types.hpp" + +class StorageListener : public firebase::storage::Listener { + public: + StorageListener(const utils::StorageTaskData& task_data, + const std::shared_ptr channel); + + void OnPaused(firebase::storage::Controller* controller) override; + void OnProgress(firebase::storage::Controller* controller) override; + + const utils::StorageTaskData& GetStorageTaskData() { return task_data_; } + + private: + utils::StorageTaskData task_data_; + std::shared_ptr channel_; +}; + +#endif diff --git a/packages/firebase_storage/tizen/src/firebase_storage_task.cc b/packages/firebase_storage/tizen/src/firebase_storage_task.cc new file mode 100644 index 0000000..65b8775 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_task.cc @@ -0,0 +1,186 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "firebase_storage_task.h" + +#include + +#include +#include +#include + +#include "firebase/app.h" +#include "firebase/storage.h" +#include "firebase_storage_error.h" +#include "log.h" + +StorageTaskHandler& StorageTaskHandler::GetInstance() { + static StorageTaskHandler instance; + return instance; +} + +void StorageTaskHandler::AddTask(int handle, + std::unique_ptr task) { + std::lock_guard lock(mutex_); + tasks_[handle] = std::move(task); +} + +std::optional StorageTaskHandler::GetTask(int handle) { + std::lock_guard lock(mutex_); + auto it = tasks_.find(handle); + if (it == tasks_.end()) { + return std::nullopt; + } + return (it->second).get(); +} + +void StorageTaskHandler::RemoveTask(int handle) { + std::lock_guard lock(mutex_); + tasks_.erase(handle); +} + +StorageTask::StorageTask(Type type, + const std::shared_ptr channel, + std::unique_ptr&& args) + : type_(type), channel_(std::move(channel)), method_args_(std::move(args)) { + auto path = method_args_->GetRequiredArg("path"); + + storage_reference_ = + utils::GetStorage(method_args_.get())->GetReference(path); + utils::StorageTaskData task_data{ + method_args_->GetRequiredArg("handle"), + method_args_->GetRequiredArg("appName"), path, + storage_reference_.bucket()}; + listener_ = std::make_unique(task_data, channel); +} + +const char* StorageTask::GetTaskName() { + switch (type_) { + case kPutData: + return "Put-Data"; + case kPutString: + return "Put-String"; + case kPutFile: + return "Put-File"; + case kWriteToFile: + return "Write-To-File"; + case kNone: + default: + return "None"; + } +} + +void StorageTask::Complete() { + StorageTaskHandler::GetInstance().RemoveTask(GetHandle()); +} + +void StorageTask::Success(const flutter::EncodableValue& result) { + channel_->InvokeMethod("Task#onSuccess", + std::make_unique(result)); +} + +void StorageTask::Fail(const flutter::EncodableValue& result, + const char* error_message) { + LOG_ERROR("Fail %s: %s", GetTaskName(), error_message); + + channel_->InvokeMethod("Task#onFailure", + std::make_unique(result)); +} + +void StoragePutTask::Run() { + RunTaskImpl().OnCompletion( + [](const firebase::Future& metadata, + void* userdata) { + auto task = static_cast(userdata); + if (metadata.error() == firebase::storage::Error::kErrorNone) { + task->Success(utils::GetPutTaskSuccessEventValue( + task->GetStorageTaskData(), metadata.result())); + } else { + task->Fail(utils::GetTaskErrorEventValue(task->GetStorageTaskData(), + metadata.error(), + metadata.error_message()), + metadata.error_message()); + } + + task->Complete(); + }, + this); +} + +firebase::Future +StoragePutDataTask::RunTaskImpl() { + buffer_ = method_args_->GetRequiredArg>("data"); + + auto metadata_value = method_args_->GetArg("metadata"); + if (metadata_value.has_value()) { + return storage_reference_.PutBytes( + buffer_.data(), buffer_.size(), + utils::ParseMetadata(metadata_value.value()), GetListener(), + GetController()); + } else { + return storage_reference_.PutBytes(buffer_.data(), buffer_.size(), + GetListener(), GetController()); + } +} + +firebase::Future +StoragePutStringTask::RunTaskImpl() { + auto format = method_args_->GetRequiredArg("format"); + auto data = method_args_->GetRequiredArg("data"); + + if (!utils::StringToByteData(data, &buffer_, format)) { + throw FirebaseStorageError(FirebaseStorageError::Code::kInvalidString, + "Fail to decode the input string."); + } + + auto metadata_value = method_args_->GetArg("metadata"); + if (metadata_value.has_value()) { + return storage_reference_.PutBytes( + buffer_.data(), buffer_.size(), + utils::ParseMetadata(metadata_value.value()), GetListener(), + GetController()); + } else { + return storage_reference_.PutBytes(buffer_.data(), buffer_.size(), + GetListener(), GetController()); + } +} + +firebase::Future +StoragePutFileTask::RunTaskImpl() { + auto file_path = method_args_->GetRequiredArg("filePath"); + + auto metadata_value = method_args_->GetArg("metadata"); + if (metadata_value.has_value()) { + return storage_reference_.PutFile( + file_path.data(), utils::ParseMetadata(metadata_value.value()), + GetListener(), GetController()); + } else { + return storage_reference_.PutFile(file_path.data(), GetListener(), + GetController()); + } +} + +void StorageWriteToFileTask::Run() { + auto file_path = method_args_->GetRequiredArg("filePath"); + + storage_reference_.GetFile(file_path.data(), GetListener(), GetController()) + .OnCompletion( + [](const firebase::Future& metadata, void* userdata) { + auto task = static_cast(userdata); + + if (metadata.error() == firebase::storage::Error::kErrorNone) { + task->Success(utils::GetTaskEventValue(task->GetStorageTaskData(), + *metadata.result(), + *metadata.result())); + } else { + task->Fail(utils::GetTaskErrorEventValue( + task->GetStorageTaskData(), metadata.error(), + metadata.error_message()), + metadata.error_message()); + } + + task->Complete(); + }, + this); +} diff --git a/packages/firebase_storage/tizen/src/firebase_storage_task.h b/packages/firebase_storage/tizen/src/firebase_storage_task.h new file mode 100644 index 0000000..d60e7c8 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_task.h @@ -0,0 +1,160 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_FIREBASE_STORAGE_TASK_H_ +#define FLUTTER_PLUGIN_FIREBASE_STORAGE_TASK_H_ + +#include + +#include +#include + +#include "firebase_storage_listener.h" +#include "firebase_storage_utils.h" +#include "flutter_types.hpp" + +class StorageTask; +class StoragePutBytesTask; + +class StorageTaskHandler { + public: + static StorageTaskHandler& GetInstance(); + + void AddTask(int handle, std::unique_ptr task); + + std::optional GetTask(int handle); + + void RemoveTask(int handle); + + private: + StorageTaskHandler() = default; + StorageTaskHandler(const StorageTaskHandler&) = delete; + void operator=(const StorageTaskHandler&) = delete; + + std::unordered_map> tasks_; + std::mutex mutex_; +}; + +class StorageTask { + public: + enum Type { + kNone, + kPutData, + kPutString, + kPutFile, + kWriteToFile, + }; + + virtual ~StorageTask() = default; + + template + static T* Create(const std::shared_ptr channel, + std::unique_ptr&& args) { + auto task = std::make_unique(channel, std::move(args)); + auto instance = task.get(); + StorageTaskHandler::GetInstance().AddTask(task->GetHandle(), + std::move(task)); + return instance; + } + + const int GetHandle() { return listener_->GetStorageTaskData().handle; } + + std::string GetAppName() { return listener_->GetStorageTaskData().app_name; } + + std::string GetPath() { return listener_->GetStorageTaskData().path; } + + std::string GetBucket() { return listener_->GetStorageTaskData().bucket; } + + const utils::StorageTaskData& GetStorageTaskData() { + return listener_->GetStorageTaskData(); + } + + firebase::storage::Controller* GetController() { return &controller_; } + + StorageListener* GetListener() { return listener_.get(); } + + const char* GetTaskName(); + + FlMethodChannel* GetMethodChannel() { return channel_.get(); } + + void Complete(); + + void Success(const flutter::EncodableValue& result); + + void Fail(const flutter::EncodableValue& result, const char* error_message); + + virtual void Run() = 0; + + protected: + StorageTask(Type type, const std::shared_ptr channel, + std::unique_ptr&& args); + + Type type_; + std::shared_ptr channel_; + std::unique_ptr method_args_; + + firebase::storage::StorageReference storage_reference_; + firebase::storage::Controller controller_; + std::unique_ptr listener_; +}; + +class StoragePutTask : public StorageTask { + public: + StoragePutTask(Type type, const std::shared_ptr channel, + std::unique_ptr&& args) + : StorageTask(type, channel, std::move(args)) {} + + void Run() override; + virtual firebase::Future RunTaskImpl() = 0; +}; + +class StoragePutDataTask final : public StoragePutTask { + public: + StoragePutDataTask(const std::shared_ptr channel, + std::unique_ptr&& args) + : StoragePutTask(kPutData, channel, std::move(args)) {} + + virtual ~StoragePutDataTask() = default; + + firebase::Future RunTaskImpl() override; + + private: + std::vector buffer_; +}; + +class StoragePutStringTask final : public StoragePutTask { + public: + StoragePutStringTask(const std::shared_ptr channel, + std::unique_ptr&& args) + : StoragePutTask(kPutString, channel, std::move(args)) {} + + virtual ~StoragePutStringTask() = default; + + firebase::Future RunTaskImpl() override; + + private: + std::string buffer_; +}; + +class StoragePutFileTask final : public StoragePutTask { + public: + StoragePutFileTask(const std::shared_ptr channel, + std::unique_ptr&& args) + : StoragePutTask(kPutFile, channel, std::move(args)) {} + + virtual ~StoragePutFileTask() = default; + + firebase::Future RunTaskImpl() override; +}; + +class StorageWriteToFileTask final : public StorageTask { + public: + StorageWriteToFileTask(const std::shared_ptr channel, + std::unique_ptr&& args) + : StorageTask(kWriteToFile, channel, std::move(args)) {} + + void Run() override; +}; + +#endif diff --git a/packages/firebase_storage/tizen/src/firebase_storage_tizen_plugin.cc b/packages/firebase_storage/tizen/src/firebase_storage_tizen_plugin.cc new file mode 100644 index 0000000..27e2d71 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_tizen_plugin.cc @@ -0,0 +1,293 @@ +#include "firebase_storage_tizen_plugin.h" + +#include +#include +#include + +#include +#include +#include + +#include "firebase_storage_error.h" +#include "firebase_storage_task.h" +#include "flutter_types.hpp" +#include "log.h" + +namespace { + +class StorageReferenceWork { + public: + StorageReferenceWork(std::unique_ptr&& args, + std::unique_ptr&& result) + : reference_(utils::GetStorageReference(args.get())), + method_call_args_(std::move(args)), + method_result_(std::move(result)) {} + + firebase::storage::StorageReference* GetStorageReference() { + return &reference_; + } + + MethodCallArguments* GetMethodCallArguments() { + return method_call_args_.get(); + } + + void Success(const flutter::EncodableValue& result) { + method_result_->Success(result); + } + + void Success() { method_result_->Success(); } + + void Fail(int error_code) { + FirebaseStorageError error(error_code); + auto code = error.GetCodeString(); + auto message = error.GetMessage(); + + method_result_->Error( + code, message, + flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue("code"), flutter::EncodableValue(code)}, + {flutter::EncodableValue("message"), + flutter::EncodableValue(message)}})); + } + + private: + firebase::storage::StorageReference reference_; + std::unique_ptr method_call_args_; + std::unique_ptr method_result_; +}; + +class FirebaseStorageTizenPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) { + auto channel = std::make_shared( + registrar->messenger(), "plugins.flutter.io/firebase_storage", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(channel); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); + } + + FirebaseStorageTizenPlugin(std::shared_ptr channel) + : channel_(std::move(channel)) {} + + virtual ~FirebaseStorageTizenPlugin() {} + + private: + std::shared_ptr channel_; + + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr result) { + auto method_call_arguments = + std::get_if(method_call.arguments()); + if (!method_call_arguments) { + FirebaseStorageError error(FirebaseStorageError::Code::kInvalidArgument); + result->Error(error.GetCodeString(), "No arguments provided."); + return; + } + + auto args = std::make_unique(method_call_arguments); + const auto& method_name = method_call.method_name(); + + try { + if (method_name == "Storage#useEmulator") { + result->NotImplemented(); + } else if (method_name == "Reference#delete") { + ReferenceDelete(std::make_shared( + std::move(args), std::move(result))); + } else if (method_name == "Reference#getDownloadURL") { + ReferenceGetDownloadURL(std::make_shared( + std::move(args), std::move(result))); + } else if (method_name == "Reference#getMetadata") { + ReferenceGetMetadata(std::make_shared( + std::move(args), std::move(result))); + } else if (method_name == "Reference#getData") { + ReferenceGetData(std::make_shared( + std::move(args), std::move(result))); + } else if (method_name == "Reference#list") { + ReferenceList(std::make_shared( + std::move(args), std::move(result))); + } else if (method_name == "Reference#listAll") { + result->NotImplemented(); + } else if (method_name == "Reference#updateMetadata") { + ReferenceUpdateMetadata(std::make_shared( + std::move(args), std::move(result))); + } else if (method_name == "Task#startPutData") { + StorageTask::Create(channel_, std::move(args)) + ->Run(); + result->Success(); + } else if (method_name == "Task#startPutString") { + StorageTask::Create(channel_, std::move(args)) + ->Run(); + result->Success(); + } else if (method_name == "Task#startPutFile") { + StorageTask::Create(channel_, std::move(args)) + ->Run(); + result->Success(); + } else if (method_name == "Task#pause") { + TaskStorageControl( + std::move(args), std::move(result), + [](StorageTask* task) { return task->GetController()->Pause(); }); + } else if (method_name == "Task#resume") { + TaskStorageControl( + std::move(args), std::move(result), + [](StorageTask* task) { return task->GetController()->Resume(); }); + } else if (method_name == "Task#cancel") { + TaskStorageControl( + std::move(args), std::move(result), [](StorageTask* task) { + auto result = task->GetController()->Cancel(); + + if (result) { + task->GetMethodChannel()->InvokeMethod( + "Task#onCanceled", + std::make_unique( + utils::GetTaskEventValue(task->GetStorageTaskData()))); + } + + return result; + }); + } else if (method_name == "Task#writeToFile") { + StorageTask::Create(channel_, std::move(args)) + ->Run(); + result->Success(); + } else { + result->NotImplemented(); + } + } catch (const std::invalid_argument& error) { + result->Error("invalid-argument", error.what()); + } catch (const FirebaseStorageError& error) { + result->Error(error.GetCodeString(), error.GetMessage()); + } + } + + void ReferenceDelete(const std::shared_ptr& work) { + work->GetStorageReference()->Delete().OnCompletion( + [work](const firebase::Future& metadata) { + if (metadata.error() == firebase::storage::Error::kErrorNone) { + work->Success(); + } else { + work->Fail(metadata.error()); + } + }); + } + + void ReferenceGetDownloadURL( + const std::shared_ptr& work) { + work->GetStorageReference()->GetDownloadUrl().OnCompletion( + [work](const firebase::Future& metadata) { + if (metadata.error() == firebase::storage::Error::kErrorNone) { + work->Success(flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue("downloadURL"), + flutter::EncodableValue(*metadata.result())}})); + } else { + work->Fail(metadata.error()); + } + }); + } + + void ReferenceGetMetadata(const std::shared_ptr& work) { + work->GetStorageReference()->GetMetadata().OnCompletion( + [work](const firebase::Future& metadata) { + if (metadata.error() == firebase::storage::Error::kErrorNone) { + work->Success(utils::GetMetadataValue(metadata.result())); + } else { + work->Fail(metadata.error()); + } + }); + } + + void ReferenceGetData(const std::shared_ptr& work) { + auto max_size = + work->GetMethodCallArguments()->GetRequiredArg("maxSize"); + auto buffer = std::make_shared>(max_size); + + work->GetStorageReference() + ->GetBytes(buffer.get()->data(), max_size) + .OnCompletion( + [work, buffer, max_size](const firebase::Future& metadata) { + auto buffer_size = *metadata.result(); + assert(buffer_size <= max_size); + + if (metadata.error() == firebase::storage::Error::kErrorNone) { + auto buffer_data = buffer.get(); + buffer_data->resize(buffer_size); + + work->Success(flutter::EncodableValue(*buffer_data)); + } else { + work->Fail(metadata.error()); + } + }); + } + + void ReferenceList(const std::shared_ptr& work) { + auto options_map = + work->GetMethodCallArguments()->GetRequiredArg( + "options"); + + MethodCallArguments options(&options_map); + + int max_result = options.GetRequiredArg("maxResults"); + auto page_token = options.GetArg("pageToken").value_or(""); + work->GetStorageReference() + ->List(max_result, page_token) + .OnCompletion([work](const firebase::Future& metadata) { + if (metadata.error() == firebase::storage::Error::kErrorNone) { + work->Success(utils::ParseListResult(*metadata.result())); + } else { + work->Fail(metadata.error()); + } + }); + } + + void ReferenceUpdateMetadata( + const std::shared_ptr& work) { + auto metadata = + work->GetMethodCallArguments()->GetRequiredArg( + "metadata"); + + work->GetStorageReference() + ->UpdateMetadata(utils::ParseMetadata(metadata)) + .OnCompletion( + [work]( + const firebase::Future& metadata) { + if (metadata.error() == firebase::storage::Error::kErrorNone) { + work->Success(utils::GetMetadataValue(metadata.result())); + } else { + work->Fail(metadata.error()); + } + }); + } + + void TaskStorageControl(std::unique_ptr&& args, + std::unique_ptr&& result, + std::function control_callback) { + int handle = args->GetRequiredArg("handle"); + + auto task_value = StorageTaskHandler::GetInstance().GetTask(handle); + if (!task_value.has_value()) { + FirebaseStorageError error(FirebaseStorageError::Code::KTaskNotFound); + result->Error(error.GetCodeString(), error.GetMessage()); + return; + } + + auto task = *task_value; + bool status = control_callback(task); + result->Success(utils::GetTaskControlEventValue(status, task->GetPath(), + task->GetController())); + } +}; + +} // namespace + +void FirebaseStorageTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + FirebaseStorageTizenPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/firebase_storage/tizen/src/firebase_storage_utils.cc b/packages/firebase_storage/tizen/src/firebase_storage_utils.cc new file mode 100644 index 0000000..b94dd22 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_utils.cc @@ -0,0 +1,311 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "firebase_storage_utils.h" + +#include +#include +#include + +#include "firebase/app/src/base64.h" +#include "firebase/storage/controller.h" +#include "firebase_storage_error.h" + +namespace utils { + +firebase::storage::Storage* GetStorage(MethodCallArguments* args) { + auto appName = args->GetRequiredArg("appName"); + firebase::App* app = firebase::App::GetInstance(appName.data()); + if (!app) { + throw FirebaseStorageError(FirebaseStorageError::Code::kAppNotFound); + } + + firebase::storage::Storage* storage = nullptr; + auto bucket = args->GetArg("bucket"); + + if (bucket.has_value()) { + std::string url("gs://" + bucket.value()); + storage = firebase::storage::Storage::GetInstance(app, url.data()); + } else { + storage = firebase::storage::Storage::GetInstance(app); + } + + auto max_operation_retry_time = args->GetArg("maxOperationRetryTime"); + if (max_operation_retry_time.has_value()) { + storage->set_max_operation_retry_time(max_operation_retry_time.value()); + } + + auto max_download_retry_time = args->GetArg("maxDownloadRetryTime"); + if (max_download_retry_time.has_value()) { + storage->set_max_download_retry_time(max_download_retry_time.value()); + } + + auto max_upload_retry_time = args->GetArg("maxUploadRetryTime"); + if (max_upload_retry_time.has_value()) { + storage->set_max_upload_retry_time(max_upload_retry_time.value()); + } + + return storage; +} + +firebase::storage::StorageReference GetStorageReference( + MethodCallArguments* args) { + return GetStorage(args)->GetReference( + args->GetRequiredArg("path")); +} + +bool StringToByteData(const std::string& input, std::string* output, + int format) { + switch (format) { + case 1: // PutStringFormat.base64 + return firebase::internal::Base64Decode(input, output); + default: + std::ostringstream os; + os << "This format(" << format << ") is not supported yet."; + throw FirebaseStorageError(FirebaseStorageError::Code::kNotSupported, + os.str()); + } + + return false; +} + +firebase::storage::Metadata ParseMetadata(const flutter::EncodableMap& value) { + MethodCallArguments metadata(&value); + firebase::storage::Metadata out; + + auto cache_control = metadata.GetArg("cacheControl"); + if (cache_control.has_value()) { + out.set_cache_control(cache_control.value()); + } + + auto content_disposition = metadata.GetArg("contentDisposition"); + if (content_disposition.has_value()) { + out.set_content_disposition(content_disposition.value()); + } + + auto content_encoding = metadata.GetArg("contentEncoding"); + if (content_encoding.has_value()) { + out.set_content_encoding(content_encoding.value()); + } + + auto content_language = metadata.GetArg("contentLanguage"); + if (content_language.has_value()) { + out.set_content_language(content_language.value()); + } + + auto content_type = metadata.GetArg("contentType"); + if (content_type.has_value()) { + out.set_content_type(content_type.value()); + } + + auto custom_metadata = + metadata.GetArg("customMetadata"); + if (custom_metadata.has_value()) { + for (const auto& [key, value] : custom_metadata.value()) { + if (std::holds_alternative(key) && + std::holds_alternative(value)) { + (*out.custom_metadata())[std::get(key)] = + std::get(value); + } + } + } + + return out; +} + +flutter::EncodableValue GetMetadataValue( + const firebase::storage::Metadata* metadata) { + auto metadata_map = flutter::EncodableMap{ + {flutter::EncodableValue("bucket"), + flutter::EncodableValue(metadata->bucket())}, + {flutter::EncodableValue("cacheControl"), + flutter::EncodableValue(metadata->cache_control())}, + {flutter::EncodableValue("contentDisposition"), + flutter::EncodableValue(metadata->content_disposition())}, + {flutter::EncodableValue("contentEncoding"), + flutter::EncodableValue(metadata->content_encoding())}, + {flutter::EncodableValue("contentLanguage"), + flutter::EncodableValue(metadata->content_language())}, + {flutter::EncodableValue("contentType"), + flutter::EncodableValue(metadata->content_type())}, + {flutter::EncodableValue("fullPath"), + flutter::EncodableValue(metadata->path())}, + {flutter::EncodableValue("generation"), + flutter::EncodableValue(metadata->generation())}, + {flutter::EncodableValue("metadataGeneration"), + flutter::EncodableValue(metadata->metadata_generation())}, + {flutter::EncodableValue("md5Hash"), + flutter::EncodableValue(metadata->md5_hash())}, + {flutter::EncodableValue("metageneration"), + flutter::EncodableValue(metadata->metadata_generation())}, + {flutter::EncodableValue("name"), + flutter::EncodableValue(metadata->name())}, + {flutter::EncodableValue("size"), + flutter::EncodableValue(metadata->size_bytes())}, + {flutter::EncodableValue("creationTimeMillis"), + flutter::EncodableValue(metadata->creation_time())}, + {flutter::EncodableValue("updatedTimeMillis"), + flutter::EncodableValue(metadata->updated_time())}, + }; + + flutter::EncodableMap custom_metadata; + for (const auto& [key, value] : *(metadata->custom_metadata())) { + custom_metadata[flutter::EncodableValue(key)] = + flutter::EncodableValue(value); + } + metadata_map[flutter::EncodableValue("customMetadata")] = + flutter::EncodableValue(custom_metadata); + + return flutter::EncodableValue(metadata_map); +} + +static flutter::EncodableMap GetTaskEventMap(const int handle, + const std::string& name, + const std::string& bucket) { + return flutter::EncodableMap{ + {flutter::EncodableValue("handle"), flutter::EncodableValue(handle)}, + {flutter::EncodableValue("appName"), flutter::EncodableValue(name)}, + {flutter::EncodableValue("bucket"), flutter::EncodableValue(bucket)}}; +} + +static flutter::EncodableMap GetSnapshotMap(const std::string& path, + const int64_t bytes_transferred, + const int64_t total_bytes) { + return flutter::EncodableMap{ + {flutter::EncodableValue("path"), flutter::EncodableValue(path)}, + {flutter::EncodableValue("bytesTransferred"), + flutter::EncodableValue(bytes_transferred)}, + {flutter::EncodableValue("totalBytes"), + flutter::EncodableValue(total_bytes)}}; +} + +flutter::EncodableValue GetTaskEventValue(const StorageTaskData& data) { + return flutter::EncodableValue( + GetTaskEventMap(data.handle, data.app_name, data.bucket)); +} + +flutter::EncodableValue GetTaskEventValue(const StorageTaskData& data, + int64_t bytes_transferred, + int64_t total_bytes) { + flutter::EncodableMap map = + GetTaskEventMap(data.handle, data.app_name, data.bucket); + + flutter::EncodableMap snapshot = + GetSnapshotMap(data.path, bytes_transferred, total_bytes); + + map[flutter::EncodableValue("snapshot")] = flutter::EncodableValue(snapshot); + + return flutter::EncodableValue(map); +} + +flutter::EncodableValue GetTaskControlEventValue( + const bool status, const std::string& path, + const firebase::storage::Controller* controller) { + auto map = flutter::EncodableMap{ + {flutter::EncodableValue("status"), flutter::EncodableValue(status)}, + }; + if (status) { + flutter::EncodableMap snapshot = GetSnapshotMap( + path, controller->bytes_transferred(), controller->total_byte_count()); + + map[flutter::EncodableValue("snapshot")] = + flutter::EncodableValue(snapshot); + } + + return flutter::EncodableValue(map); +} + +flutter::EncodableValue GetPutTaskSuccessEventValue( + const StorageTaskData& data, const firebase::storage::Metadata* result) { + flutter::EncodableMap map = + GetTaskEventMap(data.handle, data.app_name, data.bucket); + + flutter::EncodableMap metadata{ + {flutter::EncodableValue("generation"), + flutter::EncodableValue(result->generation())}, + {flutter::EncodableValue("fullPath"), + flutter::EncodableValue(result->GetReference().full_path())}, + {flutter::EncodableValue("cacheControl"), + flutter::EncodableValue(result->cache_control())}, + {flutter::EncodableValue("bucket"), + flutter::EncodableValue(result->bucket())}, + {flutter::EncodableValue("metadataGeneration"), + flutter::EncodableValue(result->metadata_generation())}, + {flutter::EncodableValue("updatedTimeMillis"), + flutter::EncodableValue(result->updated_time())}, + {flutter::EncodableValue("size"), + flutter::EncodableValue(result->size_bytes())}, + {flutter::EncodableValue("md5Hash"), + flutter::EncodableValue(result->md5_hash())}, + {flutter::EncodableValue("creationTimeMillis"), + flutter::EncodableValue(result->creation_time())}, + {flutter::EncodableValue("contentDisposition"), + flutter::EncodableValue(result->content_disposition())}, + {flutter::EncodableValue("contentEncoding"), + flutter::EncodableValue(result->content_encoding())}, + {flutter::EncodableValue("contentLanguage"), + flutter::EncodableValue(result->content_language())}, + {flutter::EncodableValue("contentType"), + flutter::EncodableValue(result->content_type())}, + }; + + flutter::EncodableMap custom_metadata; + for (const auto& [key, value] : *(result->custom_metadata())) { + custom_metadata[flutter::EncodableValue(key)] = + flutter::EncodableValue(value); + } + metadata[flutter::EncodableValue("customMetadata")] = + flutter::EncodableValue(custom_metadata); + + flutter::EncodableMap snapshot = + GetSnapshotMap(data.path, result->size_bytes(), result->size_bytes()); + snapshot[flutter::EncodableValue("metadata")] = + flutter::EncodableValue(metadata); + map[flutter::EncodableValue("snapshot")] = flutter::EncodableValue(snapshot); + + return flutter::EncodableValue(map); +} + +flutter::EncodableValue GetTaskErrorEventValue(const StorageTaskData& data, + const int error_code, + const char* error_message) { + flutter::EncodableMap map = + GetTaskEventMap(data.handle, data.app_name, data.bucket); + + flutter::EncodableMap error = { + {flutter::EncodableValue("code"), flutter::EncodableValue(error_code)}, + {flutter::EncodableValue("message"), + flutter::EncodableValue(error_message)}}; + + map[flutter::EncodableValue("error")] = flutter::EncodableValue(error); + + return flutter::EncodableValue(map); +} + +flutter::EncodableValue ParseListResult( + const firebase::storage::ListResult& list_result) { + flutter::EncodableMap map; + + if (!list_result.GetPageToken().empty()) { + map[flutter::EncodableValue("nextPageToken")] = + flutter::EncodableValue(list_result.GetPageToken()); + } + + flutter::EncodableList items; + for (StorageReference& reference : list_result.GetItems()) { + items.push_back(flutter::EncodableValue(reference.full_path())); + } + + flutter::EncodableList prefixes; + for (StorageReference& reference : list_result.GetPrefixes()) { + prefixes.push_back(flutter::EncodableValue(reference.full_path())); + } + + map[flutter::EncodableValue("items")] = items; + map[flutter::EncodableValue("prefixes")] = prefixes; + + return flutter::EncodableValue(map); +} + +} // namespace utils diff --git a/packages/firebase_storage/tizen/src/firebase_storage_utils.h b/packages/firebase_storage/tizen/src/firebase_storage_utils.h new file mode 100644 index 0000000..248f778 --- /dev/null +++ b/packages/firebase_storage/tizen/src/firebase_storage_utils.h @@ -0,0 +1,58 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_FIREBASE_STORAGE_UTILS_H_ +#define FLUTTER_PLUGIN_FIREBASE_STORAGE_UTILS_H_ + +#include + +#include + +#include "firebase/storage.h" +#include "flutter_types.hpp" + +namespace utils { + +struct StorageTaskData { + int handle; + std::string app_name; + std::string path; + std::string bucket; +}; + +firebase::storage::Storage* GetStorage(MethodCallArguments* args); + +firebase::storage::StorageReference GetStorageReference( + MethodCallArguments* args); + +bool StringToByteData(const std::string& input, std::string* output, + int format); + +firebase::storage::Metadata ParseMetadata(const flutter::EncodableMap& value); + +flutter::EncodableValue GetMetadataValue( + const firebase::storage::Metadata* metadata); + +flutter::EncodableValue GetTaskEventValue(const StorageTaskData& data); +flutter::EncodableValue GetTaskEventValue(const StorageTaskData& data, + const int64_t bytes_transferred, + const int64_t total_bytes); + +flutter::EncodableValue GetTaskControlEventValue( + const bool status, const std::string& path, + const firebase::storage::Controller* controller); + +flutter::EncodableValue GetPutTaskSuccessEventValue( + const StorageTaskData& data, const firebase::storage::Metadata* result); + +flutter::EncodableValue GetTaskErrorEventValue(const StorageTaskData& data, + const int error_code, + const char* error_message); + +flutter::EncodableValue ParseListResult( + const firebase::storage::ListResult& list_result); + +} // namespace utils + +#endif diff --git a/packages/firebase_storage/tizen/src/flutter_types.hpp b/packages/firebase_storage/tizen/src/flutter_types.hpp new file mode 100644 index 0000000..af53ee1 --- /dev/null +++ b/packages/firebase_storage/tizen/src/flutter_types.hpp @@ -0,0 +1,63 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_FLUTTER_ARGUMENTS_H_ +#define FLUTTER_PLUGIN_FLUTTER_ARGUMENTS_H_ + +#include +#include +#include +#include +#include + +typedef flutter::MethodChannel FlMethodChannel; +typedef flutter::MethodResult FlMethodResult; + +class MethodCallArguments { +public: + MethodCallArguments(const flutter::EncodableMap *arguments) + : arguments_(arguments) {} + + template std::optional GetArg(const char *key) { + assert(arguments_); + + auto iter = arguments_->find(flutter::EncodableValue(key)); + if (iter != arguments_->end() && !iter->second.IsNull()) { + if (auto *value = std::get_if(&iter->second)) { + return *value; + } + } + return std::nullopt; + } + + template T GetRequiredArg(const char *key) { + assert(arguments_); + + auto value = GetArg(key); + if (value.has_value()) { + return value.value(); + } + std::string message = + "No " + std::string(key) + " provided or has invalid type or value."; + throw std::invalid_argument(message); + } + + std::string GetKeyString() { + assert(arguments_); + + std::string argument_keys; + for (const auto &[key, value] : *arguments_) { + argument_keys += std::get(key); + argument_keys += ", "; + } + + argument_keys.erase(argument_keys.size() - 2); + return argument_keys; + } + +private: + const flutter::EncodableMap *arguments_; +}; + +#endif diff --git a/packages/firebase_storage/tizen/src/log.h b/packages/firebase_storage/tizen/src/log.h new file mode 100644 index 0000000..08d5570 --- /dev/null +++ b/packages/firebase_storage/tizen/src/log.h @@ -0,0 +1,24 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "FirebaseStorageTizenPlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__ diff --git a/packages/firebase_storage/tizen/tar_url.sh b/packages/firebase_storage/tizen/tar_url.sh new file mode 100755 index 0000000..8ab36df --- /dev/null +++ b/packages/firebase_storage/tizen/tar_url.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e + +USAGE=$(cat << EOF +Usage: $(basename "$0") [DEST_DIR] [STRIP_COMPONENTS_NUMBER] + +Description: +A script to download a tar file from a given URL and extract it. It skips the extraction +processs if the contents of the file have already been extracted for the same URL. + +Arguments: + FILE_URL The URL address of the tar file to download. + DEST_DIR The directory path to the tar file for extraction. (default: \$HOME/.firebaseSDK) + STRIP_COMPONENTS_NUMBER + The number of leading components to strip during the tar extraction. (default: 1) + +Example: + $(basename "$0") https://example.com/file.tar.gz /path/to 0 +EOF +) + +if [ -z "$1" ]; then + echo "$USAGE" + exit 1 +fi + +FILE_URL=$1 +DEST_DIR=${2:-${FLUTTER_BUILD_DIR}/.firebaseSDK} +TAR_SNUM=${3:-1} + +RECORD_FILE="${DEST_DIR}/VERSION" + +if [ -e "$RECORD_FILE" ] && [ "$FILE_URL" = $(head -n 1 "$RECORD_FILE") ]; then + echo "$RECORD_FILE exists with the same URL." +else + [ ! -d "$DEST_DIR" ] && mkdir -v "$DEST_DIR" + curl -L $FILE_URL | tar -xz --strip-components=$TAR_SNUM -C "$DEST_DIR" + echo $FILE_URL > "$RECORD_FILE" +fi