diff --git a/.vscode/settings.json b/.vscode/settings.json index 134352eb..ef4b8cb0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,228 @@ { "dart.lineLength": 80, - "dart.runPubGetOnPubspecChanges": false + "dart.runPubGetOnPubspecChanges": false, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "packages/firebase_auth/firebase_auth_desktop/macos": true, + "packages/firebase_auth/firebase_auth_desktop/windows": true, + "packages/firebase_auth/firebase_auth_desktop/.gitignore": true, + "packages/firebase_auth/firebase_auth_desktop/.metadata": true, + "packages/firebase_auth/firebase_auth_desktop/.packages": true, + "packages/firebase_auth/firebase_auth_desktop/CHANGELOG.md": true, + "packages/firebase_auth/firebase_auth_desktop/LICENSE": true, + ".vscode": true, + ".dart_tool/melos_tool": true, + ".github": true, + "packages/firebase_auth/firebase_auth_dart/pubspec.lock": true, + "packages/firebase_auth/firebase_auth_dart/test": true, + "packages/firebase_auth/firebase_auth_dart/.gitignore": true, + "packages/firebase_auth/firebase_auth_dart/.packages": true, + "packages/firebase_auth/firebase_auth_dart/CHANGELOG.md": true, + "packages/firebase_auth/firebase_auth_dart/LICENSE": true, + "packages/firebase_functions/firebase_functions_dart/.dart_tool": true, + "packages/firebase_functions/firebase_functions_dart/.gitignore": true, + "packages/firebase_functions/firebase_functions_dart/.packages": true, + "packages/firebase_functions/firebase_functions_dart/CHANGELOG.md": true, + "packages/firebase_functions/firebase_functions_dart/LICENSE": true, + "packages/firebase_functions/firebase_functions_dart/pubspec.lock": true, + "packages/firebase_functions/firebase_functions_desktop/macos": true, + "packages/firebase_functions/firebase_functions_desktop/.dart_tool": true, + "packages/firebase_functions/firebase_functions_desktop/example": true, + "packages/firebase_functions/firebase_functions_desktop/linux": true, + "packages/firebase_functions/firebase_functions_desktop/test": true, + "packages/firebase_functions/firebase_functions_desktop/windows": true, + "packages/firebase_functions/firebase_functions_desktop/.gitignore": true, + "packages/firebase_functions/firebase_functions_desktop/.metadata": true, + "packages/firebase_functions/firebase_functions_desktop/.packages": true, + "packages/firebase_functions/firebase_functions_desktop/CHANGELOG.md": true, + "packages/firebase_functions/firebase_functions_desktop/LICENSE": true, + "packages/firebase_functions/firebase_functions_desktop/pubspec.lock": true, + "packages/firebase_functions/firebase_functions_desktop/README.md": true, + "packages/firebase_storage/firebase_storage_dart/pubspec.lock": true, + "packages/firebase_storage/firebase_storage_dart/test": true, + "packages/firebase_storage/firebase_storage_dart/test/firebase_storage_dart_test.dart": true, + "packages/firebase_storage/firebase_storage_dart/.gitignore": true, + "packages/firebase_storage/firebase_storage_dart/.packages": true, + "packages/firebase_storage/firebase_storage_dart/analysis_options.yaml": true, + "packages/firebase_storage/firebase_storage_dart/CHANGELOG.md": true, + "packages/firebase_storage/firebase_storage_dart/LICENSE": true, + "packages/firebase_storage/firebase_storage_dart/README.md": true, + "packages/firebase_storage/firebase_storage_desktop/firebase_storage_desktop.iml": true, + "packages/firebase_storage/firebase_storage_desktop/test": true, + "packages/firebase_storage/firebase_storage_desktop/test/firebase_storage_desktop_test.dart": true, + "packages/firebase_storage/firebase_storage_desktop/.gitignore": true, + "packages/firebase_storage/firebase_storage_desktop/.metadata": true, + "packages/firebase_storage/firebase_storage_desktop/.packages": true, + "packages/firebase_storage/firebase_storage_desktop/analysis_options.yaml": true, + "packages/firebase_storage/firebase_storage_desktop/CHANGELOG.md": true, + "packages/firebase_storage/firebase_storage_desktop/LICENSE": true, + "packages/firebase_storage/firebase_storage_desktop/pubspec.lock": true, + "packages/firebase_storage/firebase_storage_desktop/README.md": true, + "LICENSE": true, + "packages/firebase_storage/firebase_storage_desktop/.dart_tool": true, + "packages/firebase_storage/firebase_storage_dart/example": true, + "packages/firebase_storage/firebase_storage_dart/.dart_tool": true, + "packages/firebase_functions/firebase_functions_dart/README.md": true, + ".dart_tool": true, + "packages/firebase_auth/firebase_auth_dart/example": true, + "packages/firebase_auth/firebase_auth_dart/.dart_tool": true, + "packages/firebase_auth/firebase_auth_dart/README.md": true, + "packages/firebase_auth/firebase_auth_desktop/linux": true, + "packages/firebase_auth/firebase_auth_desktop/.dart_tool": true, + "packages/firebase_auth/firebase_auth_desktop/.dart_tool/package_config_subset": true, + "packages/firebase_auth/firebase_auth_desktop/.dart_tool/package_config.json": true, + "packages/firebase_auth/firebase_auth_desktop/.dart_tool/version": true, + "packages/firebase_auth/firebase_auth_desktop/example": true, + "packages/firebase_auth/firebase_auth_desktop/pubspec.lock": true, + "packages/firebase_auth/firebase_auth_desktop/pubspec.yaml": true, + "packages/firebase_auth/firebase_auth_desktop/README.md": true, + "README.md": true, + "tests": true, + ".gitignore": true, + "all_lint_rules.yaml": true, + "analysis_options.yaml": true, + "melos.yaml": true, + "packages/firebase_functions/firebase_functions_dart/pubspec.yaml": true, + "packages/firebase_storage/firebase_storage_desktop/pubspec.yaml": true, + "packages/firebase_storage/firebase_storage_desktop/.idea": true, + "packages/firebase_storage/firebase_storage_dart/pubspec.yaml": true, + "packages/firebase_core/firebase_core_desktop/.dart_tool": true, + "packages/firebase_core/firebase_core_desktop/example": true, + "packages/firebase_core/firebase_core_desktop/linux": true, + "packages/firebase_core/firebase_core_desktop/macos": true, + "packages/firebase_core/firebase_core_desktop/test": true, + "packages/firebase_core/firebase_core_desktop/windows": true, + "packages/firebase_core/firebase_core_desktop/.gitignore": true, + "packages/firebase_core/firebase_core_desktop/.packages": true, + "packages/firebase_core/firebase_core_desktop/CHANGELOG.md": true, + "packages/firebase_core/firebase_core_desktop/LICENSE": true, + "packages/firebase_core/firebase_core_desktop/pubspec.lock": true, + "packages/firebase_core/firebase_core_desktop/pubspec.yaml": true, + "packages/firebase_core/firebase_core_desktop/README.md": true, + "packages/firebase_auth/firebase_auth_dart/pubspec.yaml": true, + "packages/firebase_core/firebase_core_dart/.dart_tool": true, + "packages/firebase_core/firebase_core_dart/.packages": true, + "packages/firebase_core/firebase_core_dart/test": true, + "packages/firebase_core/firebase_core_dart/.gitignore": true, + "packages/firebase_core/firebase_core_dart/CHANGELOG.md": true, + "packages/firebase_core/firebase_core_dart/LICENSE": true, + "packages/firebase_core/firebase_core_dart/pubspec.lock": true, + "packages/firebase_core/firebase_core_dart/pubspec.yaml": true, + "packages/firebase_core/firebase_core_dart/README.md": true, + "packages/firebase_functions/firebase_functions_dart/test": true + }, + "hide-files.files": [ + "packages/firebase_auth/firebase_auth_desktop/macos", + "packages/firebase_auth/firebase_auth_desktop/windows", + "packages/firebase_auth/firebase_auth_desktop/.gitignore", + "packages/firebase_auth/firebase_auth_desktop/.metadata", + "packages/firebase_auth/firebase_auth_desktop/.packages", + "packages/firebase_auth/firebase_auth_desktop/CHANGELOG.md", + "packages/firebase_auth/firebase_auth_desktop/LICENSE", + ".vscode", + ".dart_tool/melos_tool", + ".github", + "packages/firebase_auth/firebase_auth_dart/pubspec.lock", + "packages/firebase_auth/firebase_auth_dart/test", + "packages/firebase_auth/firebase_auth_dart/.gitignore", + "packages/firebase_auth/firebase_auth_dart/.packages", + "packages/firebase_auth/firebase_auth_dart/CHANGELOG.md", + "packages/firebase_auth/firebase_auth_dart/LICENSE", + "packages/firebase_functions/firebase_functions_dart/.dart_tool", + "packages/firebase_functions/firebase_functions_dart/.gitignore", + "packages/firebase_functions/firebase_functions_dart/.packages", + "packages/firebase_functions/firebase_functions_dart/CHANGELOG.md", + "packages/firebase_functions/firebase_functions_dart/LICENSE", + "packages/firebase_functions/firebase_functions_dart/pubspec.lock", + "packages/firebase_functions/firebase_functions_desktop/macos", + "packages/firebase_functions/firebase_functions_desktop/.dart_tool", + "packages/firebase_functions/firebase_functions_desktop/example", + "packages/firebase_functions/firebase_functions_desktop/linux", + "packages/firebase_functions/firebase_functions_desktop/test", + "packages/firebase_functions/firebase_functions_desktop/windows", + "packages/firebase_functions/firebase_functions_desktop/.gitignore", + "packages/firebase_functions/firebase_functions_desktop/.metadata", + "packages/firebase_functions/firebase_functions_desktop/.packages", + "packages/firebase_functions/firebase_functions_desktop/CHANGELOG.md", + "packages/firebase_functions/firebase_functions_desktop/LICENSE", + "packages/firebase_functions/firebase_functions_desktop/pubspec.lock", + "packages/firebase_functions/firebase_functions_desktop/README.md", + "packages/firebase_storage/firebase_storage_dart/pubspec.lock", + "packages/firebase_storage/firebase_storage_dart/test", + "packages/firebase_storage/firebase_storage_dart/test/firebase_storage_dart_test.dart", + "packages/firebase_storage/firebase_storage_dart/.gitignore", + "packages/firebase_storage/firebase_storage_dart/.packages", + "packages/firebase_storage/firebase_storage_dart/analysis_options.yaml", + "packages/firebase_storage/firebase_storage_dart/CHANGELOG.md", + "packages/firebase_storage/firebase_storage_dart/LICENSE", + "packages/firebase_storage/firebase_storage_dart/README.md", + "packages/firebase_storage/firebase_storage_desktop/firebase_storage_desktop.iml", + "packages/firebase_storage/firebase_storage_desktop/test", + "packages/firebase_storage/firebase_storage_desktop/test/firebase_storage_desktop_test.dart", + "packages/firebase_storage/firebase_storage_desktop/.gitignore", + "packages/firebase_storage/firebase_storage_desktop/.metadata", + "packages/firebase_storage/firebase_storage_desktop/.packages", + "packages/firebase_storage/firebase_storage_desktop/analysis_options.yaml", + "packages/firebase_storage/firebase_storage_desktop/CHANGELOG.md", + "packages/firebase_storage/firebase_storage_desktop/LICENSE", + "packages/firebase_storage/firebase_storage_desktop/pubspec.lock", + "packages/firebase_storage/firebase_storage_desktop/README.md", + "LICENSE", + "packages/firebase_storage/firebase_storage_desktop/.dart_tool", + "packages/firebase_storage/firebase_storage_dart/example", + "packages/firebase_storage/firebase_storage_dart/.dart_tool", + "packages/firebase_functions/firebase_functions_dart/README.md", + ".dart_tool", + "packages/firebase_auth/firebase_auth_dart/example", + "packages/firebase_auth/firebase_auth_dart/.dart_tool", + "packages/firebase_auth/firebase_auth_dart/README.md", + "packages/firebase_auth/firebase_auth_desktop/linux", + "packages/firebase_auth/firebase_auth_desktop/.dart_tool", + "packages/firebase_auth/firebase_auth_desktop/.dart_tool/package_config_subset", + "packages/firebase_auth/firebase_auth_desktop/.dart_tool/package_config.json", + "packages/firebase_auth/firebase_auth_desktop/.dart_tool/version", + "packages/firebase_auth/firebase_auth_desktop/example", + "packages/firebase_auth/firebase_auth_desktop/pubspec.lock", + "packages/firebase_auth/firebase_auth_desktop/pubspec.yaml", + "packages/firebase_auth/firebase_auth_desktop/README.md", + "README.md", + "tests", + ".gitignore", + "all_lint_rules.yaml", + "analysis_options.yaml", + "melos.yaml", + "packages/firebase_functions/firebase_functions_dart/pubspec.yaml", + "packages/firebase_storage/firebase_storage_desktop/pubspec.yaml", + "packages/firebase_storage/firebase_storage_desktop/.idea", + "packages/firebase_storage/firebase_storage_dart/pubspec.yaml", + "packages/firebase_core/firebase_core_desktop/.dart_tool", + "packages/firebase_core/firebase_core_desktop/example", + "packages/firebase_core/firebase_core_desktop/linux", + "packages/firebase_core/firebase_core_desktop/macos", + "packages/firebase_core/firebase_core_desktop/test", + "packages/firebase_core/firebase_core_desktop/windows", + "packages/firebase_core/firebase_core_desktop/.gitignore", + "packages/firebase_core/firebase_core_desktop/.packages", + "packages/firebase_core/firebase_core_desktop/CHANGELOG.md", + "packages/firebase_core/firebase_core_desktop/LICENSE", + "packages/firebase_core/firebase_core_desktop/pubspec.lock", + "packages/firebase_core/firebase_core_desktop/pubspec.yaml", + "packages/firebase_core/firebase_core_desktop/README.md", + "packages/firebase_auth/firebase_auth_dart/pubspec.yaml", + "packages/firebase_core/firebase_core_dart/.dart_tool", + "packages/firebase_core/firebase_core_dart/.packages", + "packages/firebase_core/firebase_core_dart/test", + "packages/firebase_core/firebase_core_dart/.gitignore", + "packages/firebase_core/firebase_core_dart/CHANGELOG.md", + "packages/firebase_core/firebase_core_dart/LICENSE", + "packages/firebase_core/firebase_core_dart/pubspec.lock", + "packages/firebase_core/firebase_core_dart/pubspec.yaml", + "packages/firebase_core/firebase_core_dart/README.md", + "packages/firebase_functions/firebase_functions_dart/test" + ] } diff --git a/packages/firebase_storage/firebase_storage_dart/.gitignore b/packages/firebase_storage/firebase_storage_dart/.gitignore new file mode 100644 index 00000000..65c34dc8 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/.gitignore @@ -0,0 +1,10 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build outputs. +build/ + +# Omit committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/firebase_storage/firebase_storage_dart/CHANGELOG.md b/packages/firebase_storage/firebase_storage_dart/CHANGELOG.md new file mode 100644 index 00000000..effe43c8 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/firebase_storage/firebase_storage_dart/LICENSE b/packages/firebase_storage/firebase_storage_dart/LICENSE new file mode 100644 index 00000000..5841c4de --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/LICENSE @@ -0,0 +1,29 @@ +BSD-3-Clause +------------ + +Copyright (c) 2016-present Invertase Limited & Contributors + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder 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 HOLDER 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. + + +Creative Commons Attribution 3.0 License +---------------------------------------- + +Copyright (c) 2016-present Invertase Limited & Contributors + +Documentation and other instructional materials provided for this project +(including on a separate documentation repository or it's documentation website) are +licensed under the Creative Commons Attribution 3.0 License. Code samples/blocks +contained therein are licensed under the BSD-3-Clause License (the "License"), as above. + +You may obtain a copy of the Creative Commons Attribution 3.0 License at + + https://creativecommons.org/licenses/by/3.0/ diff --git a/packages/firebase_storage/firebase_storage_dart/README.md b/packages/firebase_storage/firebase_storage_dart/README.md new file mode 100644 index 00000000..8b55e735 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/firebase_storage/firebase_storage_dart/analysis_options.yaml b/packages/firebase_storage/firebase_storage_dart/analysis_options.yaml new file mode 100644 index 00000000..dee8927a --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_storage/firebase_storage_dart/example/firebase_storage_dart_example.dart b/packages/firebase_storage/firebase_storage_dart/example/firebase_storage_dart_example.dart new file mode 100644 index 00000000..9d6f760e --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/example/firebase_storage_dart_example.dart @@ -0,0 +1,3 @@ +import 'package:firebase_storage_dart/firebase_storage_dart.dart'; + +void main() {} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/firebase_storage_dart.dart b/packages/firebase_storage/firebase_storage_dart/lib/firebase_storage_dart.dart new file mode 100644 index 00000000..20de88b7 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/firebase_storage_dart.dart @@ -0,0 +1,32 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library firebase_storage_dart; + +import 'dart:async'; +import 'dart:convert' show base64, jsonDecode, utf8; +import 'dart:io' show File; +import 'dart:typed_data' show Uint8List; +import 'package:firebase_auth_dart/firebase_auth_dart.dart'; +import 'package:firebase_storage_dart/src/api/api.dart'; +import 'package:firebase_storage_dart/src/implementations/location.dart'; +import 'package:firebase_storage_dart/src/implementations/paths.dart' as paths; +import 'package:firebase_storage_dart/src/implementations/urls.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'package:firebase_core_dart/firebase_core_dart.dart'; +import 'package:firebase_storage_dart/src/data_models/full_metadata.dart'; +import 'package:firebase_storage_dart/src/data_models/list_options.dart'; +import 'package:firebase_storage_dart/src/data_models/put_string_format.dart'; +import 'package:firebase_storage_dart/src/data_models/settable_metadata.dart'; +import 'package:firebase_storage_dart/src/data_models/task_state.dart'; + +import 'package:firebase_storage_dart/src/utils.dart'; + +// TODO: Export any libraries intended for clients of this package. +// part 'src/utils.dart'; +part 'src/firebase_storage.dart'; +part 'src/reference.dart'; +part 'src/list_result.dart'; +part 'src/task.dart'; +part 'src/task_snapshot.dart'; diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/api/api.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/api/api.dart new file mode 100644 index 00000000..87f73603 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/api/api.dart @@ -0,0 +1,203 @@ +// Copyright 2021 Invertase Limited. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +// ignore_for_file: implementation_imports + +library api; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:firebase_auth_dart/firebase_auth_dart.dart'; +import 'package:firebase_core_dart/firebase_core_dart.dart'; +import 'package:firebase_storage_dart/firebase_storage_dart.dart'; +import 'package:firebase_storage_dart/src/api/errors.dart'; +import 'package:firebase_storage_dart/src/data_models/list_options.dart'; +import 'package:firebase_storage_dart/src/data_models/put_string_format.dart'; +import 'package:firebase_storage_dart/src/data_models/settable_metadata.dart'; +import 'package:firebase_storage_dart/src/firebase_storage_exception.dart'; +import 'package:firebase_storage_dart/src/implementations/location.dart'; +import 'package:firebase_storage_dart/src/implementations/urls.dart'; +import 'package:firebaseapis/firebasestorage/v1beta.dart'; +import 'package:firebaseapis/src/user_agent.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'package:googleapis_auth/auth_io.dart' + if (dart.library.html) 'package:googleapis_auth/auth_browser.dart'; +import 'package:_discoveryapis_commons/_discoveryapis_commons.dart' as commons; +part 'emulator.dart'; +part 'reference_api.dart'; +part 'ref_api.dart'; + +/// All API classes calling to IDP API must extend this template. +abstract class APIDelegate { + /// Construct a new [APIDelegate]. + const APIDelegate(this.api); + + /// The [API] instance containing required configurations to make the requests. + final API api; + + /// Convert [DetailedApiRequestError] thrown by idp to [FirebaseStorageException]. + FirebaseStorageException makeStorageException( + DetailedApiRequestError apiError) { + try { + final json = apiError.jsonResponse; + var serverErrorCode = apiError.message ?? ''; + + String? customMessage; + + if (json != null) { + if (json['error'] != null && + // ignore: avoid_dynamic_calls + json['error']['status'] != null) { + // ignore: avoid_dynamic_calls + serverErrorCode = apiError.jsonResponse!['error']['status']; + customMessage = apiError.message; + } + + // Solves a problem with incosistent error codes coming from the server. + if (serverErrorCode.contains(' ')) { + serverErrorCode = serverErrorCode.split(' ').first; + } + } + + final storageErrorCode = StorageErrorCode.values + .firstWhere((code) => code.name == serverErrorCode); + + return FirebaseStorageException( + storageErrorCode, + message: customMessage, + ); + } catch (e) { + rethrow; + } + } + + FirebaseStorageException handleResponseErrorCodes( + int errorStatus, String ErrorText, StorageErrorCode error) { + StorageErrorCode storageErrorCode; + if (errorStatus == 401) { + if ( + // This exact message string is the only consistent part of the + // server's error response that identifies it as an App Check error. + ErrorText.contains('Firebase App Check token is invalid')) { + storageErrorCode = StorageErrorCode.UNAUTHORIZED_APP; + } else { + storageErrorCode = StorageErrorCode.UNAUTHENTICATED; + } + } else { + if (errorStatus == 402) { + storageErrorCode = StorageErrorCode.QUOTA_EXCEEDED; + } else { + if (errorStatus == 403) { + storageErrorCode = StorageErrorCode.UNAUTHORIZED; + } else { + storageErrorCode = error; + } + } + } + return FirebaseStorageException( + storageErrorCode, + message: ErrorText, + ); + } +} + +/// Configurations necessary for making all idp requests. +@protected +class APIConfig { + /// Construct [APIConfig]. + APIConfig(this.apiKey, this.projectId); + + /// The API Key associated with the Firebase project used for initialization. + final String apiKey; + + /// The project Id associated with the Firebase project used for initialization. + final String projectId; + + EmulatorConfig? _emulator; + + /// Get the current [EmulatorConfig] or null. + EmulatorConfig? get emulator => _emulator; + + /// Set a new [EmulatorConfig] or null. + void setEmulator(String host, int port) { + _emulator = EmulatorConfig.use(host, port); + } +} + +/// Pure Dart service layer to perform all requests +/// with the underlying Identity Toolkit API. +/// +/// See: https://cloud.google.com/identity-platform/docs/use-rest-api +@protected +class API { + API._(this.apiConfig, {http.Client? client}) { + _client = client ?? clientViaApiKey(apiConfig.apiKey); + } + + /// Construct new or existing [API] instance for a given [APIConfig]. + factory API.instanceOf(APIConfig apiConfig, {http.Client? client}) { + return _instances.putIfAbsent( + apiConfig, + () => API._(apiConfig, client: client), + ); + } + + /// The API configurations of this instance. + final APIConfig apiConfig; + + static final Map _instances = {}; + + http.Client? _client; + + String? _languageCode; + + /// The current languageCode sent in the headers of all API requests. + /// If `null`, the default Firebase Console language will be used. + String? get languageCode => _languageCode; + + /// Change the HTTP client for the purpose of testing. + @internal + // ignore: avoid_setters_without_getters + set client(http.Client client) { + _client = client; + } + + /// Updates the [languageCode] for this instance. + void setLanguageCode(String? languageCode) { + _languageCode = languageCode; + requestHeaders.addAll({'X-Firebase-Locale': languageCode ?? ''}); + } + + /// Identity platform [ProjectsResource] initialized with this instance [APIConfig]. + @internal + ProjectsResource get firebaseStorage { + if (apiConfig.emulator != null) { + return FirebasestorageApi( + _client!, + rootUrl: apiConfig.emulator!.rootUrl, + ).projects; + } + return FirebasestorageApi( + _client!, + ).projects; + } + + /// A delegate getter used to perform all requests + /// for Identity platform profile related operations. + StorageEmulator get emulator => StorageEmulator(apiConfig); + + commons.ApiRequester get _requester { + if (apiConfig.emulator != null) { + return commons.ApiRequester( + _client!, apiConfig.emulator!.rootUrl, '', requestHeaders); + } + return commons.ApiRequester(_client!, + 'https://firebasestorage.googleapis.com/', '', requestHeaders); + } + + RefApi get refernceApi => RefApi(this); +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/api/emulator.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/api/emulator.dart new file mode 100644 index 00000000..ca2eaf86 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/api/emulator.dart @@ -0,0 +1,62 @@ +part of api; + +/// A type to hold the Auth Emulator configurations. +class EmulatorConfig { + EmulatorConfig._({ + required this.port, + required this.host, + }); + + /// Initialize the Emulator Config using the host and port printed once running `firebase emulators:start`. + factory EmulatorConfig.use(String host, int port) { + return EmulatorConfig._(port: port, host: host); + } + + /// The port on which the emulator suite is running. + final String host; + + /// The port on which the emulator suite is running. + final int port; + + /// The root URL used to make requests to the locally running emulator. + String get rootUrl => 'http://$host:$port/www.googleapis.com/'; +} + +/// Call the local auth emulator. +/// If found, set the instance API to use it for all future requests. +class StorageEmulator { + // ignore: public_member_api_docs + StorageEmulator(this._config); + + final APIConfig _config; + + /// Try to connect to the local runnning emulator by getting emulator-specific + /// configuration for the specified project. If the connection was successful, + /// set local emulator in API configurations to be used for all future requests. + Future> useEmulator(String host, int port) async { + // 1. Get the emulator project configs, it must be initialized first. + // http://localhost:9099/emulator/v1/projects/{project-id}/config + final localEmulator = Uri( + scheme: 'http', + host: host, + port: port, + path: '/emulator/v1/projects/${_config.projectId}/config', + ); + + http.Response response; + + try { + response = await http.get(localEmulator); + } catch (e) { + return {}; + } + + final Map emulatorProjectConfig = + json.decode(response.body); + + // set the the emulator config for this instance. + _config.setEmulator(host, port); + + return emulatorProjectConfig; + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/api/errors.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/api/errors.dart new file mode 100644 index 00000000..d55dd7e5 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/api/errors.dart @@ -0,0 +1,156 @@ +enum StorageErrorCode { + // Shared between all platforms + UNKNOWN, + OBJECT_NOT_FOUND, + BUCKET_NOT_FOUND, + PROJECT_NOT_FOUND, + QUOTA_EXCEEDED, + UNAUTHENTICATED, + UNAUTHORIZED, + UNAUTHORIZED_APP, + RETRY_LIMIT_EXCEEDED, + INVALID_CHECKSUM, + CANCELED, + // JS specific + INVALID_EVENT_NAME, + INVALID_URL, + INVALID_DEFAULT_BUCKET, + NO_DEFAULT_BUCKET, + CANNOT_SLICE_BLOB, + SERVER_FILE_WRONG_SIZE, + NO_DOWNLOAD_URL, + INVALID_ARGUMENT, + INVALID_ARGUMENT_COUNT, + APP_DELETED, + INVALID_ROOT_OPERATION, + INVALID_FORMAT, + INTERNAL_ERROR, + UNSUPPORTED_ENVIRONMENT, +} + +extension ToString on StorageErrorCode { + String get asString { + return name.toLowerCase().replaceAll('_', '-'); + } +} + +extension GetMessage on StorageErrorCode { + String get message { + switch (this) { + case StorageErrorCode.OBJECT_NOT_FOUND: + return "No object exists at the desired reference."; + case StorageErrorCode.BUCKET_NOT_FOUND: + return "No bucket is configured for Firebase Storage."; + case StorageErrorCode.PROJECT_NOT_FOUND: + return "No project is configured for Firebase Storage."; + case StorageErrorCode.QUOTA_EXCEEDED: + return "Quota on your Firebase Storage bucket has been exceeded."; + case StorageErrorCode.UNAUTHENTICATED: + return "User is unauthenticated. Authenticate and try again."; + case StorageErrorCode.UNAUTHORIZED: + return "User is not authorized to perform the desired action."; + case StorageErrorCode.RETRY_LIMIT_EXCEEDED: + return "The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded."; + case StorageErrorCode.INVALID_CHECKSUM: + return "File on the client does not match the checksum of the file received by the server."; + case StorageErrorCode.CANCELED: + return "User cancelled the operation."; + case StorageErrorCode.UNKNOWN: + default: + { + return "An unknown error occurred"; + } + } + } +} + +class StorageError { + late String _baseMessage; + String message; + final StorageErrorCode code; + /** + * Stores custom error data unque to StorageError. + */ + String? _serverResponse; + + /// + /// @param code - A StorageErrorCode string to be prefixed with 'storage/' and + /// added to the end of the message. + /// @param message - Error message. + /// + StorageError(this.code, this.message) { + _baseMessage = message; + } + + /// Compares a StorageErrorCode against this error's code, filtering out the prefix. + + bool _codeEquals(StorageErrorCode code) { + return prependCode(code) == this.code; + } + + /** + * Optional response message that was added by the server. + */ + String? get serverResponse { + return _serverResponse; + } + + set serverResponse(String? serverResponse) { + _serverResponse = serverResponse; + if (_serverResponse != null) { + message = '${_baseMessage}\n${_serverResponse}'; + } else { + message = _baseMessage; + } + } +} + +String prependCode(StorageErrorCode code) { + return 'storage/' + code.name; +} + +StorageError invalidDefaultBucket(String bucket) { + return StorageError(StorageErrorCode.INVALID_DEFAULT_BUCKET, + "Invalid default bucket '" + bucket + "'."); +} + +StorageError invalidUrl(String url) { + return StorageError( + StorageErrorCode.INVALID_URL, "Invalid URL '" + url + "'."); +} + +StorageError invalidArgument(String message) { + return StorageError(StorageErrorCode.INVALID_ARGUMENT, message); +} + +String codeForHTTPStatus(int status) { + switch (status) { + case 0: + // This can happen if the server returns 500. + return 'internal'; + case 400: + return 'invalid-argument'; + case 401: + return 'unauthenticated'; + case 403: + return 'permission-denied'; + case 404: + return 'not-found'; + case 409: + return 'aborted'; + case 429: + return 'resource-exhausted'; + case 499: + return 'cancelled'; + case 500: + return 'internal'; + case 501: + return 'unimplemented'; + case 503: + return 'unavailable'; + case 504: + return 'deadline-exceeded'; + default: + return 'unknown'; + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/api/ref_api.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/api/ref_api.dart new file mode 100644 index 00000000..96ab1ffa --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/api/ref_api.dart @@ -0,0 +1,415 @@ +part of api; + +/// Class wrapping methods that calls to the following endpoints: +/// - `securetoken.googleapis.com`: refresh a Firebase ID token. +@internal +class RefApi extends APIDelegate { + // ignore: public_member_api_docs + RefApi(API api) : super(api); + + /// Refresh a user's IdToken using a `refreshToken`, + /// will refresh even if the token hasn't expired. + /// + /// Common error codes: + /// - `TOKEN_EXPIRED`: The user's credential is no longer valid. The user must sign in again. + /// - `USER_DISABLED`: The user account has been disabled by an administrator. + /// - `USER_NOT_FOUND`: The user corresponding to the refresh token was not found. It is likely the user was deleted. + /// - `INVALID_REFRESH_TOKEN`: An invalid refresh token is provided. + /// - `INVALID_GRANT_TYPE`: the grant type specified is invalid. + /// - `MISSING_REFRESH_TOKEN`: no refresh token provided. + /// - API key not valid. Please pass a valid API key. (invalid API key provided) + Future refreshIdToken(String? refreshToken) async { + try { + return await _exchangeRefreshWithIdToken(refreshToken); + } catch (_) { + rethrow; + } + } + + Future _exchangeRefreshWithIdToken(String? refreshToken) async { + final baseUri = api.apiConfig.emulator != null + ? 'http://${api.apiConfig.emulator!.host}:${api.apiConfig.emulator!.port}' + '/securetoken.googleapis.com/v1/' + : 'https://securetoken.googleapis.com/v1/'; + + final _response = await http.post( + Uri.parse( + '${baseUri}token?key=${api.apiConfig.apiKey}', + ), + body: { + 'grant_type': 'refresh_token', + 'refresh_token': refreshToken, + }, + headers: {'Content-Typ': 'application/json'}, + ); + + final Map _data = json.decode(_response.body); + + return _data['access_token']; + } + + Future getRefListData( + {ListOptions? options, required Reference reference}) async { + final urlPart = reference.location.bucketOnlyServerUrl(); + final url = makeUrl(urlPart, reference.storage.host, "https"); + final urlparams = ReferenceListRessultRequestArgument( + location: reference.location, + delimiter: '/', + pageToken: options?.pageToken, + maxResults: options?.maxResults) + .toJson(); + final requestOptions =HttpsCallableOptions(method: RequestMethod.get, + timeout: reference.storage.maxOperationRetryTime, + url:url, + queryParam:urlparams ); + + listHandler(service, location.bucket), + + + requestInfo.errorHandler = sharedErrorHandler(location); + // return requestInfo; + // if (!this._deleted) { + // if (1 == 1) { + // //parse respone + // final auth = FirebaseAuth.instanceFor(app: storage.app); + // String? authToken; + // if (auth.currentUser != null) { + // authToken = await auth.currentUser!.getIdToken(); + // } + // final response = await http.get(Uri.parse('url'), headers: { + // // if (appCheckToken != null) + // // 'X-Firebase-AppCheck': '$appCheckToken' + // 'X-Firebase-GMPID': storage.app.options.appId, + // 'X-Firebase-Storage-Version': + // 'webjs/' + (firebaseVersion ?? 'AppManager'), + // if (authToken != null && authToken.isNotEmpty) + // 'Authorization': 'Firebase ' + authToken + // }).timeout(Duration(milliseconds: storage.maxOperationRetryTime), + // onTimeout: () { + // throw Exception(); + // }); + // if (response.statusCode == 200) { + // // print(response.body); + // try { + // final data = jsonDecode(response.body); + + // return ListResult( + // storage, + // data['nextPageToken'], + // (data['items'] as List).map((e) => e['name'] as String).toList(), + // (data['prefixes'] as List).map((String e) { + // final s = e.endsWith('/'); + // if (s) { + // return e.substring(0, e.length - 1); + // } + // return e; + // }).toList()); + // } on Exception catch (e) { + // throw Exception(); + // } + // } else { + // throw Exception(); + // } + // } else { + // return FailRequest(appDeleted()); + // } + } + + Future delete({required Reference reference}) { + throw UnimplementedError(); + } + + Future getDownloadURL({required Reference reference}) { + throw UnimplementedError(); + } + + Future getMetaData({required Reference reference}) { + final urlPart = reference.location.fullServerUrl(); + final url = makeUrl(urlPart, reference.storage.host, reference.storage.protocol); + final requestInfo =HttpsCallableOptions(method: RequestMethod.get, + timeout: reference.storage.maxOperationRetryTime, + url:url, ); + throw UnimplementedError(); + } + + Future getData({required Reference reference, required maxSize}) { + throw UnimplementedError(); + } + + putData( + {required Reference reference, + required Uint8List data, + SettableMetadata? metadata}) { + throw UnimplementedError(); + } + + putFile( + {required Reference reference, + required File file, + SettableMetadata? metadata}) { + throw UnimplementedError(); + } + + putBlob( + {required Reference reference, + required dynamic blob, + SettableMetadata? metadata}) { + throw UnimplementedError(); + } + + putString( + {required Reference reference, + required String data, + required PutStringFormat format, + SettableMetadata? metadata}) { + throw UnimplementedError(); + } + + updateMetadata( + {required Reference reference, required SettableMetadata metadata}) { + throw UnimplementedError(); + } + + writeToFile({required Reference reference, required File file}) { + throw UnimplementedError(); + } +} + +/// A [StorageRequest] instance that lets you call a cloud function. +class StorageRequest { + /// Creates an StorageRequest + @visibleForTesting + StorageRequest({ + required this.app, + required this.origin, + required this.options, + required http.Client client, + }) : _client = client; + + /// The [FirebaseApp] this function belongs to + final FirebaseApp app; + + /// Configuration options for timeout + final HttpsCallableOptions options; + + /// Origin specifies a different origin in the case of emulators. + @visibleForTesting + final String? origin; + + /// The Http Client used for making requests + final http.Client _client; + + Future makeRequest() async { + switch (options.method) { + case RequestMethod.get: + return get(); + case RequestMethod.post: + return post(); + case RequestMethod.delete: + return delete(); + case RequestMethod.patch: + return patch(); + } + } + + Future callRequest() async { + await addHeaders(); + await makeRequest(); + } + + Future addHeaders() async { + options.headers ??= {}; + // add auth headers + final auth = FirebaseAuth.instanceFor(app: app); + if (auth.currentUser != null) { + final authToken = await auth.currentUser!.getIdToken(); + options.headers!['Authorization'] = authToken; + } + // add version headers + options.headers!['X-Firebase-Storage-Version'] = + 'webjs/' + (firebaseVersion ?? 'AppManager'); + // add APP ID header + options.headers!.addAll({'X-Firebase-GMPID': app.options.appId}); + // add app check token header //TODO + options.headers!.addAll({'X-Firebase-AppCheck': app.options.appId}); + } + + /// Calls the function with the given data. + Future> call([dynamic data]) async { + assert(_debugIsValidParameterType(data), 'data must be json serialized'); + String encodedData; + try { + encodedData = json.encode({'data': data}); + } catch (e, st) { + throw FirebaseStorageException( + //TODO + StorageErrorCode.APP_DELETED, + message: 'Data was not json encodeable', + // code: 'internal', + // details: + // '${options.timeout} millisecond timeout occurred on request to $_url with $data', + // stackTrace: st, + ); + } + + try { + final response = await makeRequest(); + if (response.statusCode >= 200 && response.statusCode < 300) { + Map body; + try { + body = json.decode(response.body.isEmpty ? '{}' : response.body) + as Map; + } catch (e, st) { + throw FirebaseStorageException( + //TODO + + StorageErrorCode.APP_DELETED, + message: 'Failed to parse json response', + // code: 'internal', + // details: 'Result body from http call was ${response.body}', + // stackTrace: st, + ); + } + if (!body.containsKey('data') && !body.containsKey('result')) { + throw FirebaseStorageException( + //TODO + StorageErrorCode.APP_DELETED, + message: 'Response is missing data field', + // code: 'internal', + // details: 'Result body from http call was ${response.body}', + ); + } + // Result is for backwards compatibility + final result = body['data'] ?? body['result']; + + return HttpsCallableResult(result); + } else { + Map details; + try { + details = json.decode(response.body); + } catch (e) { + // fine if we can't parse explicit error data + details = {'details': response.body}; + } + throw _errorForResponse( + response.statusCode, + details, + ); + } + } on TimeoutException catch (e, st) { + throw FirebaseStorageException( + //TODO + StorageErrorCode.APP_DELETED, + message: 'Firebase functions timeout', + // code: 'timeout', + // details: + // '${options.timeout} millisecond timeout occurred on request to $_url with $encodedData', + // stackTrace: st, + ); + } + } + + Future post() async { + return await _client + .post( + Uri.parse(options.url).replace(queryParameters: options.queryParam), + headers: options.headers, + body: options.body) + .timeout(Duration(milliseconds: options.timeout)); + } + + Future get() async { + return await _client + .get( + Uri.parse(options.url).replace(queryParameters: options.queryParam), + headers: options.headers, + ) + .timeout(Duration(milliseconds: options.timeout)); + } + + Future patch() async { + return await _client + .patch( + Uri.parse(options.url).replace(queryParameters: options.queryParam), + headers: options.headers, + body: options.body) + .timeout(Duration(milliseconds: options.timeout)); + } + + Future delete() async { + return await _client + .delete( + Uri.parse(options.url).replace(queryParameters: options.queryParam), + headers: options.headers, + body: options.body) + .timeout(Duration(milliseconds: options.timeout)); + } +} + +/// The result of calling a HttpsCallable function. +class HttpsCallableResult { + /// Creates a new [HttpsCallableResult] + HttpsCallableResult(this._data); + + final T _data; + + /// Returns the data that was returned from the Callable HTTPS trigger. + T get data { + return _data; + } +} + +/// Options for configuring the behavior of a firebase cloud function +class HttpsCallableOptions { + /// Options for configuring the behavior of a firebase cloud function + HttpsCallableOptions( + {required this.url, + this.successCodes, + required this.method, + this.headers, + this.queryParam, + this.body, + required this.timeout}); + + /// The timeout for the function call + final int timeout; + final Map? queryParam; + Map? headers; + List? successCodes; + final RequestMethod method; + final String url; + final dynamic body; +} + +/// Whether a given call parameter is a valid type. +bool _debugIsValidParameterType(dynamic parameter, [bool isRoot = true]) { + if (parameter is List) { + for (final element in parameter) { + if (!_debugIsValidParameterType(element, false)) { + return false; + } + } + return true; + } + + if (parameter is Map) { + for (final key in parameter.keys) { + if (key is! String) { + return false; + } + } + for (final value in parameter.values) { + if (!_debugIsValidParameterType(value, false)) { + return false; + } + } + return true; + } + + return parameter == null || + parameter is String || + parameter is num || + parameter is bool; +} + +enum RequestMethod { get, post, delete, patch } diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/api/reference_api.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/api/reference_api.dart new file mode 100644 index 00000000..4248953f --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/api/reference_api.dart @@ -0,0 +1,35 @@ +part of api; + +/// Request to verify a custom token +class ReferenceListRessultRequestArgument { + Location location; + String? delimiter; + String? pageToken; + int? maxResults; + ReferenceListRessultRequestArgument({ + required this.location, + this.delimiter, + this.pageToken, + this.maxResults, + }); + + Map toJson() { + Map urlParams = {}; + if (location.isRoot) { + urlParams['prefix'] = ''; + } else { + urlParams['prefix'] = location.path + '/'; + } + final s = delimiter?.isNotEmpty; + if (delimiter != null && s == true) { + urlParams['delimiter'] = delimiter; + } + if (pageToken != null) { + urlParams['pageToken'] = pageToken; + } + if (maxResults != null) { + urlParams['maxResults'] = maxResults; + } + return urlParams; + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/full_metadata.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/full_metadata.dart new file mode 100644 index 00000000..6a07daf0 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/full_metadata.dart @@ -0,0 +1,98 @@ +import 'package:meta/meta.dart' show protected; + +/// The result of calling [getMetadata] on a storage object reference. +class FullMetadata { + // ignore: public_member_api_docs + @protected + FullMetadata(this._metadata); + + final Map _metadata; + + /// The bucket this object is contained in. + String? get bucket { + return _metadata['bucket']; + } + + /// Served as the 'Cache-Control' header on object download. + String? get cacheControl { + return _metadata['cacheControl']; + } + + /// Served as the 'Content-Disposition' HTTP header on object download. + String? get contentDisposition { + return _metadata['contentDisposition']; + } + + /// Served as the 'Content-Encoding' header on object download. + String? get contentEncoding { + return _metadata['contentEncoding']; + } + + /// Served as the 'Content-Language' header on object download. + String? get contentLanguage { + return _metadata['contentLanguage']; + } + + /// Served as the 'Content-Type' header on object download. + String? get contentType { + return _metadata['contentType']; + } + + /// Custom metadata set on this storage object. + Map? get customMetadata { + return _metadata['customMetadata'] == null + ? null + : Map.from(_metadata['customMetadata']); + } + + /// The full path of this object. + String get fullPath { + return _metadata['fullPath']; + } + + /// The object's generation. + String? get generation { + return _metadata['generation']; + } + + /// The object's metadata generation. + String? get metadataGeneration { + return _metadata['metadataGeneration']; + } + + /// A Base64-encoded MD5 hash of the object being uploaded. + String? get md5Hash { + return _metadata['md5Hash']; + } + + /// The object's metageneration. + String? get metageneration { + return _metadata['metageneration']; + } + + /// The short name of this object, which is the last component of the full path. + /// + /// For example, if fullPath is 'full/path/image.png', name is 'image.png'. + String get name { + return _metadata['name']; + } + + /// The size of this object, in bytes. + int? get size { + return _metadata['size']; + } + + /// A DateTime representing when this object was created. + DateTime? get timeCreated { + return _metadata['creationTimeMillis'] == null + ? null + : DateTime.fromMillisecondsSinceEpoch(_metadata['creationTimeMillis']); + } + + /// A DateTime representing when this object was updated. + DateTime? get updated { + return _metadata['updatedTimeMillis'] == null + ? null + : DateTime.fromMillisecondsSinceEpoch(_metadata['updatedTimeMillis']); + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/list_options.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/list_options.dart new file mode 100644 index 00000000..5a578578 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/list_options.dart @@ -0,0 +1,18 @@ +/// The options [FirebaseStoragePlatform.list] accepts. +class ListOptions { + /// Creates a new [ListOptions] instance. + const ListOptions({ + this.maxResults, + this.pageToken, + }); + + /// If set, limits the total number of `prefixes` and `items` to return. + /// + /// The default and maximum maxResults is 1000. + final int? maxResults; + + /// The nextPageToken from a previous call to list(). + /// + /// If provided, listing is resumed from the previous position. + final String? pageToken; +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/put_string_format.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/put_string_format.dart new file mode 100644 index 00000000..985724db --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/put_string_format.dart @@ -0,0 +1,15 @@ +/// The format in which a string can be uploaded to the storage bucket via +/// [Reference.putString]. +enum PutStringFormat { + /// A raw string. It will be uploaded as a Base64 string. + raw, + + /// A Base64 encoded string. + base64, + + /// A Base64 URL encoded string. + base64Url, + + /// A data url string. + dataUrl, +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/settable_metadata.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/settable_metadata.dart new file mode 100644 index 00000000..dece5579 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/settable_metadata.dart @@ -0,0 +1,52 @@ +/// The settable metadata a storage object reference can be set with. +class SettableMetadata { + /// Creates a new [SettableMetadata] instance. + SettableMetadata({ + this.cacheControl, + this.contentDisposition, + this.contentEncoding, + this.contentLanguage, + this.contentType, + this.customMetadata, + }); + + /// Served as the 'Cache-Control' header on object download. + /// + /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control. + final String? cacheControl; + + /// Served as the 'Content-Disposition' header on object download. + /// + /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition. + final String? contentDisposition; + + /// Served as the 'Content-Encoding' header on object download. + /// + /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding. + final String? contentEncoding; + + /// Served as the 'Content-Language' header on object download. + /// + /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language. + final String? contentLanguage; + + /// Served as the 'Content-Type' header on object download. + /// + /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type. + final String? contentType; + + /// Additional user-defined custom metadata. + final Map? customMetadata; + + /// Returns the settable metadata as a [Map]. + Map asMap() { + return { + 'cacheControl': cacheControl, + 'contentDisposition': contentDisposition, + 'contentEncoding': contentEncoding, + 'contentLanguage': contentLanguage, + 'contentType': contentType, + 'customMetadata': customMetadata, + }; + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/task_state.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/task_state.dart new file mode 100644 index 00000000..f09e69c3 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/data_models/task_state.dart @@ -0,0 +1,19 @@ +/// Represents the state of an on-going [Task]. +/// +/// The state can be accessed directly via a [TaskSnapshot]. +enum TaskState { + /// Indicates the task has been paused by the user. + paused, + + /// Indicates the task is currently in-progress. + running, + + /// Indicates the task has successfully completed. + success, + + /// Indicates the task was canceled. + canceled, + + /// Indicates the task failed with an error. + error, +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/firebase_storage.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/firebase_storage.dart new file mode 100644 index 00000000..bd25e005 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/firebase_storage.dart @@ -0,0 +1,232 @@ +// Copyright 2021 Invertase Limited. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +// ignore_for_file: require_trailing_commas + +part of firebase_storage_dart; + +/// Pure Dart FirebaseAuth implementation. +/// The entrypoint for [FirebaseStorage]. +class FirebaseStorage { + /// Creates Firebase Functions + // @visibleForTesting + FirebaseStorage({required this.app, required this.bucket}) { + _api = API.instanceOf( + APIConfig( + app.options.apiKey, + app.options.projectId, + ), + ); + } + + /// The [FirebaseApp] for this current [FirebaseStorage] instance. + FirebaseApp app; + final firebaseVesion = "0.6.30"; + String _host = 'firebasestorage.googleapis.com'; + String get host { + if (emulatorHost != null) { + return emulatorHost!; + } + return _host; + } + + /// Initialized [API] instance linked to this instance. + late final API _api; + + /// Change the HTTP client for the purpose of testing. + @visibleForTesting + // ignore: avoid_setters_without_getters + set client(http.Client client) { + _api.client = client; + } + + /// The storage bucket of this instance. + String bucket; + + // Same default as the method channel implementation + int _maxDownloadRetryTime = const Duration(minutes: 10).inMilliseconds; + + // Same default as the method channel implementation + int _maxOperationRetryTime = const Duration(minutes: 2).inMilliseconds; + // Same default as the method channel implementation + int _maxUploadRetryTime = const Duration(minutes: 10).inMilliseconds; + + static final Map _cachedInstances = {}; + + /// Returns an instance using the default [FirebaseApp]. + static FirebaseStorage get instance { + return FirebaseStorage.instanceFor( + app: Firebase.app(), + ); + } + + /// Returns an instance using a specified [FirebaseApp] and/or custom storage bucket. + /// + /// If [app] is not provided, the default Firebase app will be used. + /// If [bucket] is not provided, the default storage bucket will be used. + static FirebaseStorage instanceFor({ + FirebaseApp? app, + String? bucket, + }) { + app ??= Firebase.app(); + + if (bucket == null && app.options.storageBucket == null) { + if (app.name == defaultFirebaseAppName) { + _throwNoBucketError( + 'No default storage bucket could be found. Ensure you have correctly followed the Getting Started guide.'); + } else { + _throwNoBucketError( + "No storage bucket could be found for the app '${app.name}'. Ensure you have set the [storageBucket] on [FirebaseOptions] whilst initializing the secondary Firebase app."); + } + } + + String _bucket = bucket ?? app.options.storageBucket!; + + // Previous versions allow storage buckets starting with "gs://". + // Since we need to create a key using the bucket, it must not include "gs://" + // since native does not include it when requesting the bucket. This keeps + // the code backwards compatible but also works with the refactor. + if (_bucket.startsWith('gs://')) { + _bucket = _bucket.replaceFirst('gs://', ''); + } + + String key = '${app.name}|$_bucket'; + if (_cachedInstances.containsKey(key)) { + return _cachedInstances[key]!; + } + + FirebaseStorage newInstance = FirebaseStorage(app: app, bucket: _bucket); + _cachedInstances[key] = newInstance; + + return newInstance; + } + + /// The Storage emulator host this instance is configured to use. This + /// was required since iOS does not persist these settings on instances and + /// they need to be set every time when getting a `FIRStorage` instance. + String? emulatorHost; + + /// The Storage emulator port this instance is configured to use. This + /// was required since iOS does not persist these settings on instances and + /// they need to be set every time when getting a `FIRStorage` instance. + int? emulatorPort; + + /// Returns a new [Reference]. + /// + /// If the [path] is empty, the reference will point to the root of the + /// storage bucket. + Reference ref([String? path]) { + path ??= '/'; + return Reference.fromPath(storage: this, path: path); + } + + /// Returns a new [Reference] from a given URL. + /// + /// The [url] can either be a HTTP or Google Storage URL pointing to an object. + /// If the URL contains a storage bucket which is different to the current + /// [FirebaseStorage.bucket], a new [FirebaseStorage] instance for the + /// [Reference] will be used instead. + Reference refFromURL(String url) { + assert(url.startsWith('gs://') || url.startsWith('http'), + "'a url must start with 'gs://' or 'https://'"); + + String? bucket; + String? path; + + if (url.startsWith('http')) { + final parts = partsFromHttpUrl(url); + + assert(parts != null, + "url could not be parsed, ensure it's a valid storage url"); + + bucket = parts!['bucket']; + path = parts['path']; + _host = parts['host'] ?? _host; + } else { + bucket = bucketFromGoogleStorageUrl(url); + path = pathFromGoogleStorageUrl(url); + } + + return FirebaseStorage.instanceFor(app: app, bucket: 'gs://$bucket') + .ref(path); + } + + /// The maximum time to retry operations other than uploads or downloads in milliseconds. + int get maxOperationRetryTime { + return _maxOperationRetryTime; + } + + /// The maximum time to retry uploads in milliseconds. + int get maxUploadRetryTime { + return _maxUploadRetryTime; + } + + /// The maximum time to retry downloads in milliseconds. + int get maxDownloadRetryTime { + return _maxDownloadRetryTime; + } + + /// Sets the new maximum operation retry time. + void setMaxOperationRetryTime(int time) { + assert(!time.isNegative); + _maxOperationRetryTime = time; + } + + /// Sets the new maximum upload retry time. + void setMaxUploadRetryTime(int time) { + assert(!time.isNegative); + _maxUploadRetryTime = time; + } + + /// Sets the new maximum download retry time. + void setMaxDownloadRetryTime(int time) { + assert(!time.isNegative); + _maxDownloadRetryTime = time; + } + + /// Changes this instance to point to a Storage emulator running locally. + /// + /// Set the [host] (ex: localhost) and [port] (ex: 9199) of the local emulator. + /// + /// Note: Must be called immediately, prior to accessing storage methods. + /// Do not use with production credentials as emulator traffic is not encrypted. + @Deprecated( + 'Will be removed in future release. ' + 'Use useStorageEmulator().', + ) + Future useEmulator({required String host, required int port}) async { + assert(host.isNotEmpty); + assert(!port.isNegative); + + await useStorageEmulator(host: host, port: port); + } + + /// Changes this instance to point to a Storage emulator running locally. + /// + /// Set the [host] of the local emulator, such as "localhost" + /// Set the [port] of the local emulator, such as "9199" (port 9199 is default for storage package) + /// + /// Note: Must be called immediately, prior to accessing storage methods. + /// Do not use with production credentials as emulator traffic is not encrypted. + Future useStorageEmulator( + {String host = 'localhost', int port = 9199}) async { + assert(host.isNotEmpty); + assert(!port.isNegative); + + try { + emulatorHost = host; + return await _api.emulator.useEmulator(host, port); + } catch (e) { + rethrow; + } + } + + @override + String toString() => '$FirebaseStorage(app: ${app.name}, bucket: $bucket)'; +} + +void _throwNoBucketError(String message) { + throw FirebaseException( + plugin: 'firebase_storage', code: 'no-bucket', message: message); +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/firebase_storage_exception.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/firebase_storage_exception.dart new file mode 100644 index 00000000..ca8f05b9 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/firebase_storage_exception.dart @@ -0,0 +1,18 @@ +// Copyright 2021 Invertase Limited. 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_dart/firebase_core_dart.dart'; +import 'package:firebase_storage_dart/src/api/errors.dart'; + +/// Wrap the errors from the Identity Platform REST API, usually of type `DetailedApiRequestError` +/// in a in a Firebase-friendly format to users. +class FirebaseStorageException extends FirebaseException implements Exception { + // ignore: public_member_api_docs + FirebaseStorageException(StorageErrorCode errorCode, {String? message}) + : super( + plugin: 'firebase_auth', + code: errorCode.asString, + message: message ?? errorCode.message, + ); +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/constants.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/constants.dart new file mode 100644 index 00000000..978324c3 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/constants.dart @@ -0,0 +1,15 @@ +/// +///Domain name for firebase storage. +/// +const DEFAULT_HOST = 'firebasestorage.googleapis.com'; + +/// +///The key in Firebase config json for the storage bucket. +/// +const CONFIG_STORAGE_BUCKET_KEY = 'storageBucket'; + +/// +///This is the value of Number.MIN_SAFE_INTEGER, which is not well supported +///enough for us to use it directly. +/// +const MIN_SAFE_INTEGER = -9007199254740991; diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/location.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/location.dart new file mode 100644 index 00000000..4187a5b9 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/location.dart @@ -0,0 +1,21 @@ +class Location { + Location(String path, this.bucket) { + _path = path; + } + final String bucket; + late String _path; + String get path => _path; + + bool get isRoot => path.isEmpty; + + String fullServerUrl() { + return '/b/' + + Uri.encodeComponent(bucket) + + '/o/' + + Uri.encodeComponent(path); + } + + String bucketOnlyServerUrl() { + return '/b/' + Uri.encodeComponent(bucket) + '/o'; + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/paths.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/paths.dart new file mode 100644 index 00000000..f246f6ec --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/paths.dart @@ -0,0 +1,36 @@ +String? parent(String path) { + if (path.isEmpty) { + return null; + } + final index = path.lastIndexOf('/'); + if (index == -1) { + return ''; + } + return path.substring(0, index); +} + +String child(String path, String childPath) { + final canonicalChildPath = childPath.split('/') + ..retainWhere((element) => element.isNotEmpty); + final joinedCanonicalChildPath = canonicalChildPath.join('/'); + if (path.isEmpty) { + return joinedCanonicalChildPath; + } else { + return path + '/' + joinedCanonicalChildPath; + } +} + +/// +///Returns the last component of a path. +///'/foo/bar' -> 'bar' +///'/foo/bar/baz/' -> 'baz/' +///'/a' -> 'a' +/// +String lastComponent(String path) { + final index = path.lastIndexOf('/', path.length - 2); + if (index == -1) { + return path; + } else { + return path.substring(index + 1); + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/urls.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/urls.dart new file mode 100644 index 00000000..60a2bf77 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/implementations/urls.dart @@ -0,0 +1,11 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +String makeUrl(String urlPart, String host, String? protocol) { + var origin = host; + if (protocol == null) { + origin = 'https://$host'; + } + return '$protocol://$origin/v0$urlPart'; +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/internal/pointer.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/internal/pointer.dart new file mode 100644 index 00000000..2543b706 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/internal/pointer.dart @@ -0,0 +1,67 @@ +/// Internal helper class used to manage storage reference paths. +class Pointer { + /// Constructs a new [Pointer] with a given path. + Pointer(String? path) : _path = path ?? '/' { + if (_path.isEmpty) { + _path = '/'; + } else { + String _parsedPath = _path; + + // Remove trailing slashes + if (_path.length > 1 && _path.endsWith('/')) { + _parsedPath = _parsedPath.substring(0, _parsedPath.length - 1); + } + + // Remove starting slashes + if (_path.startsWith('/') && _path.length > 1) { + _parsedPath = _parsedPath.substring(1, _parsedPath.length); + } + + _path = _parsedPath; + } + } + + String _path; + + /// Returns whether the path points to the root of a bucket. + bool get isRoot { + return path == '/'; + } + + /// Returns the serialized path. + String get path { + return _path; + } + + /// Returns the name of the path. + /// + /// For example, a paths "foo/bar.jpg" name is "bar.jpg". + String get name { + return path.split('/').last; + } + + /// Returns the parent path. + /// + /// If the current path is root, `null` wil be returned. + String? get parent { + if (isRoot) { + return null; + } + + List chunks = path.split('/'); + chunks.removeLast(); + return chunks.join('/'); + } + + /// Returns a child path relative to the current path. + String child(String childPath) { + Pointer childPointer = Pointer(childPath); + + // If already at + if (isRoot) { + return childPointer.path; + } + + return '$path/${childPointer.path}'; + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/list_result.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/list_result.dart new file mode 100644 index 00000000..e6bab30f --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/list_result.dart @@ -0,0 +1,45 @@ +part of firebase_storage_dart; + +/// Class returned as a result of calling a list method ([list] or [listAll]) +/// on a [Reference]. +class ListResult { + ListResult(this.storage, String? nextPageToken, List items, + List prefixes) + : _nextPageToken = nextPageToken, + _items = items, + _prefixes = prefixes; + + /// The [FirebaseStorage] instance for this result. + final FirebaseStorage storage; + final String? _nextPageToken; + final List _items; + + final List _prefixes; + + /// Objects in this directory. + /// + /// Returns a [List] of [Reference] instances. + List get items { + return _items + .map((path) => Reference._(storage, Location(path, storage.bucket))) + .toList(); + } + + /// If set, there might be more results for this list. + /// + /// Use this token to resume the list with [ListOptions]. + String? get nextPageToken => _nextPageToken; + + /// References to prefixes (sub-folders). You can call list() on them to get + /// its contents. + /// + /// Folders are implicit based on '/' in the object paths. For example, if a + /// bucket has two objects '/a/b/1' and '/a/b/2', list('/a') will return '/a/b' + /// as a prefix. + + List get prefixes { + return _prefixes + .map((path) => Reference._(storage, Location(path, storage.bucket))) + .toList(); + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/reference.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/reference.dart new file mode 100644 index 00000000..02f3008b --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/reference.dart @@ -0,0 +1,300 @@ +part of firebase_storage_dart; + +class Reference { + Reference.fromPath({required this.storage, required String path}) + : location = Location(path, storage.bucket); + Reference._(this.storage, this.location); + + /// The storage service associated with this reference. + final FirebaseStorage storage; + + ///An fbs.location, or the URL at + /// which to base this object, in one of the following forms: + /// gs:/// + /// http[s]://firebasestorage.googleapis.com/ + /// /b//o/ + /// Any query or fragment strings will be ignored in the http[s] + /// format. If no value is passed, the storage object will use a URL based on + /// the project ID of the base firebase.App instance. + /// + final Location location; + + /// The name of the bucket containing this reference's object. + String get bucket => location.bucket; + + /// The full path of this object. + String get fullPath => location.path; + + /// The short name of this object, which is the last component of the full path. + /// + /// For example, if fullPath is 'full/path/image.png', name is 'image.png'. + String get name => paths.lastComponent(location.path); + + /// A reference pointing to the parent location of this reference, or `null` + /// if this reference is the root. + Reference? get parent { + final newPath = paths.parent(this.location.path); + if (newPath == null) { + return null; + } + final newLocation = Location( + newPath, + location.bucket, + ); + return Reference._(storage, newLocation); + } + + /// A reference to the root of this reference's bucket. + Reference get root { + final newLocation = Location( + '', + location.bucket, + ); + return Reference._(storage, newLocation); + } + + /// Returns a reference to a relative path from this reference. + /// + /// [path] The relative path from this reference. Leading, trailing, and + /// consecutive slashes are removed. + Reference child(String path) { + final newPath = paths.child(location.path, path); + final newLocation = Location( + newPath, + location.bucket, + ); + return Reference._(storage, newLocation); + } + + /// Deletes the object at this reference's location. + Future delete() { + throw UnimplementedError(); + // final urlPart = location.fullServerUrl(); + // final url = makeUrl(urlPart, service.host, service._protocol); + // const method = 'DELETE'; + // final timeout = storage.maxOperationRetryTime; + + // void handler(Connection _xhr,String _text) {} + // final requestInfo = RequestInfo(url, method, handler, timeout); + // requestInfo.successCodes = [200, 204]; + // requestInfo.errorHandler = objectErrorHandler(location); + // return requestInfo; + } + + /// Fetches a long lived download URL for this object. + Future getDownloadURL() async { + // ref._throwIfRoot('getDownloadURL'); + try { + return await storage._api.refernceApi.getDownloadURL(reference: this); + } catch (_) { + rethrow; + } + } + + /// Fetches metadata for the object at this location, if one exists. + Future getMetadata() async { + try { + return await storage._api.refernceApi.getMetaData( + reference: this, + ); + } catch (_) { + rethrow; + } + } + + /// List items (files) and prefixes (folders) under this storage reference. + /// + /// List API is only available for Firebase Rules Version 2. + /// + /// GCS is a key-blob store. Firebase Storage imposes the semantic of '/' + /// delimited folder structure. Refer to GCS's List API if you want to learn more. + /// + /// To adhere to Firebase Rules's Semantics, Firebase Storage does not support + /// objects whose paths end with "/" or contain two consecutive "/"s. Firebase + /// Storage List API will filter these unsupported objects. [list] may fail + /// if there are too many unsupported objects in the bucket. + Future list([ListOptions? options]) async { + assert(options == null || + options.maxResults == null || + options.maxResults! > 0 && options.maxResults! <= 1000); + try { + return await storage._api.refernceApi + .getRefListData(reference: this, options: options); + } catch (_) { + rethrow; + } + } + + /// List all items (files) and prefixes (folders) under this storage reference. + /// + /// This is a helper method for calling [list] repeatedly until there are no + /// more results. The default pagination size is 1000. + /// + /// Note: The results may not be consistent if objects are changed while this + /// operation is running. + /// + /// Warning: [listAll] may potentially consume too many resources if there are + /// too many results. + Future listAll() async { + ListResult result = await list(); + List _items = result._items.toList(); + List _prefixes = result._prefixes.toList(); + while (result.nextPageToken != null) { + result = await list(ListOptions(pageToken: result.nextPageToken)); + _items.addAll(result._items); + _prefixes.addAll(result._prefixes); + } + return ListResult(storage, null, _items, _prefixes); + } + + /// Asynchronously downloads the object at the StorageReference to a list in memory. + /// + /// Returns a [Uint8List] of the data. + /// + /// If the [maxSize] (in bytes) is exceeded, the operation will be canceled. By + /// default the [maxSize] is 10mb (10485760 bytes). + Future getData([int maxSize = 10485760]) async { + assert(maxSize > 0); + try { + return await storage._api.refernceApi + .getData(reference: this, maxSize: maxSize); + } catch (_) { + rethrow; + } + } + + /// Uploads data to this reference's location. + /// + /// Use this method to upload fixed sized data as a [Uint8List]. + /// + /// Optionally, you can also set metadata onto the uploaded object. + UploadTask putData(Uint8List data, [SettableMetadata? metadata]) { + try { + return storage._api.refernceApi + .putData(reference: this, data: data, metadata: metadata); + } catch (_) { + rethrow; + } + } + + /// Upload a [Blob]. Note; this is only supported on web platforms. + /// + /// Optionally, you can also set metadata onto the uploaded object. + UploadTask putBlob(dynamic blob, [SettableMetadata? metadata]) { + assert(blob != null); + try { + return storage._api.refernceApi + .putBlob(reference: this, blob: blob, metadata: metadata); + } catch (_) { + rethrow; + } + } + + /// Upload a [File] from the filesystem. The file must exist. + /// + /// Optionally, you can also set metadata onto the uploaded object. + UploadTask putFile(File file, [SettableMetadata? metadata]) { + assert(file.absolute.existsSync()); + try { + return storage._api.refernceApi + .putFile(reference: this, file: file, metadata: metadata); + } catch (_) { + rethrow; + } + } + + /// Upload a [String] value as a storage object. + /// + /// Use [PutStringFormat] to correctly encode the string: + /// - [PutStringFormat.raw] the string will be encoded in a Base64 format. + /// - [PutStringFormat.dataUrl] the string must be in a data url format + /// (e.g. "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="). If no + /// [SettableMetadata.mimeType] is provided as part of the [metadata] + /// argument, the [mimeType] will be automatically set. + /// - [PutStringFormat.base64] will be encoded as a Base64 string. + /// - [PutStringFormat.base64Url] will be encoded as a Base64 string safe URL. + UploadTask putString( + String data, { + PutStringFormat format = PutStringFormat.raw, + SettableMetadata? metadata, + }) { + String _data = data; + PutStringFormat _format = format; + SettableMetadata? _metadata = metadata; + + // Convert any raw string values into a Base64 format + if (format == PutStringFormat.raw) { + _data = base64.encode(utf8.encode(_data)); + _format = PutStringFormat.base64; + } + + // Convert a data_url into a Base64 format + if (format == PutStringFormat.dataUrl) { + _format = PutStringFormat.base64; + UriData uri = UriData.fromUri(Uri.parse(data)); + assert(uri.isBase64); + _data = uri.contentText; + + if (_metadata == null && uri.mimeType.isNotEmpty) { + _metadata = SettableMetadata( + contentType: uri.mimeType, + ); + } + + // If the data_url contains a mime-type & the user has not provided it, + // set it + if ((_metadata!.contentType == null || _metadata.contentType!.isEmpty) && + uri.mimeType.isNotEmpty) { + _metadata = SettableMetadata( + cacheControl: metadata!.cacheControl, + contentDisposition: metadata.contentDisposition, + contentEncoding: metadata.contentEncoding, + contentLanguage: metadata.contentLanguage, + contentType: uri.mimeType, + ); + } + } + + try { + return storage._api.refernceApi.putString( + reference: this, data: _data, format: _format, metadata: metadata); + } catch (_) { + rethrow; + } + } + + /// Updates the metadata on a storage object. + Future updateMetadata(SettableMetadata metadata) { + try { + return storage._api.refernceApi + .updateMetadata(reference: this, metadata: metadata); + } catch (_) { + rethrow; + } + } + + /// Writes a remote storage object to the local filesystem. + /// + /// If a file already exists at the given location, it will be overwritten. + DownloadTask writeToFile(File file) { + try { + return storage._api.refernceApi.writeToFile(reference: this, file: file); + } catch (_) { + rethrow; + } + } + + @override + bool operator ==(Object other) => + other is Reference && + other.fullPath == fullPath && + other.storage == storage; + + @override + String toString() => + '$Reference(app: ${storage.app.name}, fullPath: $fullPath)'; + + @override + // TODO: implement hashCode + int get hashCode => super.hashCode; +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/task.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/task.dart new file mode 100644 index 00000000..249e30b4 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/task.dart @@ -0,0 +1,121 @@ +part of firebase_storage_dart; + +/// A class representing an on-going storage task that additionally delegates to a [Future]. +abstract class Task implements Future { + Task._( + this.storage, + ); + + /// The [FirebaseStorage] instance associated with this task. + final FirebaseStorage storage; + + /// Returns a [Stream] of [TaskSnapshot] events. + /// + /// If the task is canceled or fails, the stream will send an error event. + /// See [TaskState] for more information of the different event types. + /// + /// If you do not need to know about on-going stream events, you can instead + /// await this [Task] directly. + Stream get snapshotEvents { + throw UnimplementedError(); + // return _delegate.snapshotEvents + // .map((snapshotDelegate) => TaskSnapshot._(storage, snapshotDelegate)); + } + + /// The latest [TaskSnapshot] for this task. + TaskSnapshot get snapshot { + throw UnimplementedError(); + // return TaskSnapshot._(storage, _delegate.snapshot); + } + + /// Pauses the current task. + /// + /// Calling this method will trigger a snapshot event with a [TaskState.paused] + /// state. + Future pause() async { + if (snapshot.state == TaskState.paused) { + return true; + } + throw UnimplementedError(); + // final paused = _task.pause(); + // // Wait until the snapshot is paused, then return the value of paused... + // return snapshotEvents + // .takeWhile((snapshot) => snapshot.state != TaskState.paused) + // .last + // .then((_) => paused); + } + + /// Resumes the current task. + /// + /// Calling this method will trigger a snapshot event with a [TaskState.running] + /// state. + Future resume() { + throw UnimplementedError(); + // _delegate.resume(); + } + + /// Cancels the current task. + /// + /// Calling this method will cause the task to fail. Both the delegating task Future + /// and stream ([snapshotEvents]) will trigger an error with a [FirebaseException]. + Future cancel() { + throw UnimplementedError(); + // _delegate.cancel(); + } + + @override + Stream asStream() { + throw UnimplementedError(); + // _delegate.onComplete.asStream().map((_) => snapshot); + } + + @override + Future catchError(Function onError, + {bool Function(Object error)? test}) async { + throw UnimplementedError(); + // await _delegate.onComplete.catchError(onError, test: test); + // return snapshot; + } + + @override + Future then(FutureOr Function(TaskSnapshot) onValue, + {Function? onError}) { + throw UnimplementedError(); + // _delegate.onComplete.then((_) { + // return onValue(snapshot); + // }, onError: onError); + } + + @override + Future whenComplete(FutureOr Function() action) async { + throw UnimplementedError(); + // await _delegate.onComplete.whenComplete(action); + // return snapshot; + } + + @override + Future timeout(Duration timeLimit, + {FutureOr Function()? onTimeout}) { + throw UnimplementedError(); + // _delegate.onComplete + // .then((_) => snapshot) + // .timeout(timeLimit, onTimeout: onTimeout); + } +} + +/// A class which indicates an on-going upload task. +class UploadTask extends Task { + UploadTask._( + FirebaseStorage storage, + ) : super._( + storage, + ); +} + +/// A class which indicates an on-going download task. +class DownloadTask extends Task { + DownloadTask._(FirebaseStorage storage) + : super._( + storage, + ); +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/task_snapshot.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/task_snapshot.dart new file mode 100644 index 00000000..3adcce3d --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/task_snapshot.dart @@ -0,0 +1,45 @@ +part of firebase_storage_dart; + +/// A [TaskSnapshot] is returned as the result or on-going process of a [Task]. +class TaskSnapshot { + TaskSnapshot._(this.storage, this._data); + + /// The [FirebaseStorage] instance used to create the task. + final FirebaseStorage storage; + final Map _data; + + /// The current transferred bytes of this task. + int get bytesTransferred => _data['bytesTransferred']; + + /// The [FullMetadata] associated with this task. + /// + /// May be `null` if no metadata exists. + FullMetadata? get metadata => _data['metadata']; + + /// The [Reference] for this snapshot. + Reference get ref { + return Reference._(storage, _data['path']); + } + + /// The current task snapshot state. + /// + /// The state indicates the current progress of the task, such as whether it + /// is running, paused or completed. + TaskState get state => _data['state']; + + /// The total bytes of the task. + /// + /// Note; when performing a download task, the value of `-1` will be provided + /// whilst the total size of the remote file is being determined. + int get totalBytes => _data['totalBytes']; + + @override + bool operator ==(Object other) => + other is TaskSnapshot && other.ref == ref && other.storage == storage; + + @override + int get hashCode => Object.hashAll([storage, ref]); + + @override + String toString() => '$TaskSnapshot(ref: $ref, state: $state)'; +} diff --git a/packages/firebase_storage/firebase_storage_dart/lib/src/utils.dart b/packages/firebase_storage/firebase_storage_dart/lib/src/utils.dart new file mode 100644 index 00000000..e0d13cc8 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/lib/src/utils.dart @@ -0,0 +1,95 @@ +/// Returns a bucket from a given `gs://` URL. +String bucketFromGoogleStorageUrl(String url) { + assert(url.startsWith('gs://')); + int stopIndex = url.indexOf('/', 5); + int stop = stopIndex == -1 ? url.length : stopIndex; + return url.substring(5, stop); +} + +/// Returns a path from a given `gs://` URL. +/// +/// If no path exists, the root path will be returned. +String pathFromGoogleStorageUrl(String url) { + assert(url.startsWith('gs://')); + int stopIndex = url.indexOf('/', 5); + if (stopIndex == -1) return '/'; + return url.substring(stopIndex + 1, url.length); +} + +const String _firebaseStorageHost = 'firebasestorage.googleapis.com'; +const String _cloudStorageHost = + '(?:storage.googleapis.com|storage.cloud.google.com)'; +const String _bucketDomain = r'([A-Za-z0-9.\-_]+)'; +const String _version = 'v[A-Za-z0-9_]+'; +const String _firebaseStoragePath = r'(/([^?#]*).*)?$'; +const String _cloudStoragePath = r'([^?#]*)*$'; +const String _optionalPort = r'(?::\d+)?'; + +/// Returns a path from a given `http://` or `https://` URL. +/// +/// If url fails to parse, null is returned +/// If no path exists, the root path will be returned. +Map? partsFromHttpUrl(String url) { + assert(url.startsWith('http')); + String? decodedUrl = _decodeHttpUrl(url); + if (decodedUrl == null) { + return null; + } + + // firebase storage url + String? storageHost; + if (decodedUrl.contains(_firebaseStorageHost) || + decodedUrl.contains('localhost')) { + String origin; + if (decodedUrl.contains('localhost')) { + Uri uri = Uri.parse(url); + origin = '^http?://${uri.host}:${uri.port}'; + } else { + origin = '^https?://$_firebaseStorageHost'; + storageHost = _firebaseStorageHost; + } + + RegExp firebaseStorageRegExp = RegExp( + '$origin$_optionalPort/$_version/b/$_bucketDomain/o$_firebaseStoragePath', + caseSensitive: false, + ); + + RegExpMatch? match = firebaseStorageRegExp.firstMatch(decodedUrl); + + if (match == null) { + return null; + } + + return { + 'bucket': match.group(1), + 'path': match.group(3), + if (storageHost != null) 'host': storageHost, + }; + // google cloud storage url + } else { + RegExp cloudStorageRegExp = RegExp( + '^https?://($_cloudStorageHost)$_optionalPort/$_bucketDomain/$_cloudStoragePath', + caseSensitive: false, + ); + + RegExpMatch? match = cloudStorageRegExp.firstMatch(decodedUrl); + + if (match == null) { + return null; + } + + return { + 'host': match.group(1), + 'bucket': match.group(2), + 'path': match.group(3), + }; + } +} + +String? _decodeHttpUrl(String url) { + try { + return Uri.decodeFull(url); + } catch (_) { + return null; + } +} diff --git a/packages/firebase_storage/firebase_storage_dart/pubspec.yaml b/packages/firebase_storage/firebase_storage_dart/pubspec.yaml new file mode 100644 index 00000000..e6083008 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/pubspec.yaml @@ -0,0 +1,16 @@ +name: firebase_storage_dart +description: A starting point for Dart libraries or applications. +version: 0.1.0-dev.0 +# homepage: https://www.example.com + +environment: + sdk: '>=2.16.2 <3.0.0' + + +dependencies: + firebase_auth_dart: ^0.1.1 + firebase_core_dart: ^0.1.1 + firebaseapis: ^0.1.1 +dev_dependencies: + lints: ^1.0.0 + test: ^1.16.0 diff --git a/packages/firebase_storage/firebase_storage_dart/test/firebase_storage_dart_test.dart b/packages/firebase_storage/firebase_storage_dart/test/firebase_storage_dart_test.dart new file mode 100644 index 00000000..7c134017 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_dart/test/firebase_storage_dart_test.dart @@ -0,0 +1,6 @@ +import 'package:firebase_storage_dart/firebase_storage_dart.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () {}); +} diff --git a/packages/firebase_storage/firebase_storage_desktop/.gitignore b/packages/firebase_storage/firebase_storage_desktop/.gitignore new file mode 100644 index 00000000..9be145fd --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# 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/firebase_storage_desktop/.metadata b/packages/firebase_storage/firebase_storage_desktop/.metadata new file mode 100644 index 00000000..50694c14 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: c860cba910319332564e1e9d470a17074c1f2dfd + channel: stable + +project_type: package diff --git a/packages/firebase_storage/firebase_storage_desktop/CHANGELOG.md b/packages/firebase_storage/firebase_storage_desktop/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_storage/firebase_storage_desktop/LICENSE b/packages/firebase_storage/firebase_storage_desktop/LICENSE new file mode 100644 index 00000000..5841c4de --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/LICENSE @@ -0,0 +1,29 @@ +BSD-3-Clause +------------ + +Copyright (c) 2016-present Invertase Limited & Contributors + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder 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 HOLDER 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. + + +Creative Commons Attribution 3.0 License +---------------------------------------- + +Copyright (c) 2016-present Invertase Limited & Contributors + +Documentation and other instructional materials provided for this project +(including on a separate documentation repository or it's documentation website) are +licensed under the Creative Commons Attribution 3.0 License. Code samples/blocks +contained therein are licensed under the BSD-3-Clause License (the "License"), as above. + +You may obtain a copy of the Creative Commons Attribution 3.0 License at + + https://creativecommons.org/licenses/by/3.0/ diff --git a/packages/firebase_storage/firebase_storage_desktop/README.md b/packages/firebase_storage/firebase_storage_desktop/README.md new file mode 100644 index 00000000..8b55e735 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/firebase_storage/firebase_storage_desktop/analysis_options.yaml b/packages/firebase_storage/firebase_storage_desktop/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_storage/firebase_storage_desktop/lib/firebase_storage_desktop.dart b/packages/firebase_storage/firebase_storage_desktop/lib/firebase_storage_desktop.dart new file mode 100644 index 00000000..4266c2e3 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/lib/firebase_storage_desktop.dart @@ -0,0 +1,181 @@ +// Copyright 2022 Invertase Limited. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. +library firebase_storage_desktop; + +import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_core_dart/firebase_core_dart.dart' as core_dart; + +class FirebaseStorageDesktop extends FirebaseStoragePlatform { + FirebaseStorageDesktop({required FirebaseApp app, required String bucket}) + : _app = core_dart.Firebase.app(app.name), + super(appInstance: app, bucket: bucket); + + /// Stub initializer to allow creating an instance without + /// registering delegates or listeners. + /// + // ignore: prefer_constructors_over_static_methods + static FirebaseStorageDesktop get instance { + return FirebaseStorageDesktop.instanceFor( + app: Firebase.app(), + ); + } + + /// Returns an instance using a specified [FirebaseApp] and/or custom storage bucket. + /// + /// If [app] is not provided, the default Firebase app will be used. + /// If [bucket] is not provided, the default storage bucket will be used. + static FirebaseStorageDesktop instanceFor({ + FirebaseApp? app, + String? bucket, + }) { + app ??= Firebase.app(); + + if (bucket == null && app.options.storageBucket == null) { + if (app.name == defaultFirebaseAppName) { + _throwNoBucketError( + 'No default storage bucket could be found. Ensure you have correctly followed the Getting Started guide.'); + } else { + _throwNoBucketError( + "No storage bucket could be found for the app '${app.name}'. Ensure you have set the [storageBucket] on [FirebaseOptions] whilst initializing the secondary Firebase app."); + } + } + + String _bucket = bucket ?? app.options.storageBucket!; + + // Previous versions allow storage buckets starting with "gs://". + // Since we need to create a key using the bucket, it must not include "gs://" + // since native does not include it when requesting the bucket. This keeps + // the code backwards compatible but also works with the refactor. + if (_bucket.startsWith('gs://')) { + _bucket = _bucket.replaceFirst('gs://', ''); + } + + String key = '${app.name}|$_bucket'; + if (_cachedInstances.containsKey(key)) { + return _cachedInstances[key]!; + } + + FirebaseStorageDesktop newInstance = + FirebaseStorageDesktop(app: app, bucket: _bucket); + _cachedInstances[key] = newInstance; + + return newInstance; + } + + static final Map _cachedInstances = {}; + final core_dart.FirebaseApp? _app; + FirebaseStoragePlatform? _delegatePackingProperty; + FirebaseStoragePlatform get _delegate { + return _delegatePackingProperty ??= FirebaseStoragePlatform.instanceFor( + app: app, + bucket: bucket, + ); + } + + /// The maximum time to retry operations other than uploads or downloads in milliseconds. + @override + int get maxOperationRetryTime { + return _delegate.maxOperationRetryTime; + } + + /// The maximum time to retry uploads in milliseconds. + @override + int get maxUploadRetryTime { + return _delegate.maxUploadRetryTime; + } + + /// The maximum time to retry downloads in milliseconds. + @override + int get maxDownloadRetryTime { + return _delegate.maxDownloadRetryTime; + } + + /// Enables delegates to create new instances of themselves if a none default + /// [FirebaseApp] instance is required by the user. + @override + FirebaseStoragePlatform delegateFor( + {required FirebaseApp app, required String bucket}) { + if (app.options.storageBucket == null) { + if (app.name == defaultFirebaseAppName) { + _throwNoBucketError( + 'No default storage bucket could be found. Ensure you have correctly followed the Getting Started guide.'); + } else { + _throwNoBucketError( + "No storage bucket could be found for the app '${app.name}'. Ensure you have set the [storageBucket] on [FirebaseOptions] whilst initializing the secondary Firebase app."); + } + } + + String _bucket = bucket; + + // Previous versions allow storage buckets starting with "gs://". + // Since we need to create a key using the bucket, it must not include "gs://" + // since native does not include it when requesting the bucket. This keeps + // the code backwards compatible but also works with the refactor. + if (bucket.startsWith('gs://')) { + _bucket = _bucket.replaceFirst('gs://', ''); + } + + String key = '${app.name}|$_bucket'; + if (_cachedInstances.containsKey(key)) { + return _cachedInstances[key]!; + } + + FirebaseStorageDesktop newInstance = + FirebaseStorageDesktop(app: app, bucket: _bucket); + _cachedInstances[key] = newInstance; + + return newInstance; + } + + /// Returns a reference for the given path in the default bucket. + /// + /// [path] A relative path to initialize the reference with, for example + /// `path/to/image.jpg`. If not passed, the returned reference points to + /// the bucket root. + @override + ReferencePlatform ref(String path) { + throw UnimplementedError('ref() is not implemented'); + } + + /// Changes this instance to point to a Storage emulator running locally. + /// + /// Set the [host] (ex: localhost) and [port] (ex: 9199) of the local emulator. + /// + /// Note: Must be called immediately, prior to accessing storage methods. + /// Do not use with production credentials as emulator traffic is not encrypted. + /// + /// Note: storage emulator is not supported for web yet. firebase-js-sdk does not support + /// storage.useStorageEmulator until v9 + @override + Future useStorageEmulator(String host, int port) { + throw UnimplementedError('useStorageEmulator() is not implemented'); + } + + /// The new maximum operation retry time in milliseconds. + @override + void setMaxOperationRetryTime(int time) { + assert(!time.isNegative); + return _delegate.setMaxOperationRetryTime(time); + } + + /// The new maximum upload retry time in milliseconds. + @override + void setMaxUploadRetryTime(int time) { + assert(!time.isNegative); + return _delegate.setMaxUploadRetryTime(time); + } + + /// The new maximum download retry time in milliseconds. + @override + void setMaxDownloadRetryTime(int time) { + assert(!time.isNegative); + return _delegate.setMaxDownloadRetryTime(time); + } +} + +void _throwNoBucketError(String message) { + throw FirebaseException( + plugin: 'firebase_storage', code: 'no-bucket', message: message); +} diff --git a/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_firebase_storage.dart b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_firebase_storage.dart new file mode 100644 index 00000000..81f55d6c --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_firebase_storage.dart @@ -0,0 +1,188 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020 The Chromium Authors. 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 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_core_dart/firebase_core_dart.dart' as core_dart; +import 'package:firebase_storage_desktop/src/desktop_reference.dart'; +import 'package:firebase_storage_desktop/src/desktop_task_snapshot.dart'; +import 'package:firebase_storage_desktop/src/utils/exceptions.dart'; +import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart'; +import 'package:firebase_storage_dart/firebase_storage_dart.dart' + as storage_dart; +import 'package:flutter/services.dart'; + +/// Method Channel delegate for [FirebaseStoragePlatform]. +class DesktopFirebaseStorage extends FirebaseStoragePlatform { + /// Creates a new [DesktopFirebaseStorage] instance with an [app] and/or + /// [bucket]. + DesktopFirebaseStorage({required FirebaseApp app, required String bucket}) + : _app = core_dart.Firebase.app(app.name), + super(appInstance: app, bucket: bucket) { + // The channel setMethodCallHandler callback is not app specific, so there + // is no need to register the caller more than once. + if (_initialized) return; + + _initialized = true; + } + + /// Internal stub class initializer. + /// + /// When the user code calls an storage method, the real instance is + /// then initialized via the [delegateFor] method. + DesktopFirebaseStorage._() + : _app = null, + super(appInstance: null, bucket: ''); + + /// Keep an internal reference to whether the [DesktopFirebaseStorage] + /// class has already been initialized. + static bool _initialized = false; + + /// Returns a unique key to identify the instance by [FirebaseApp] name and + /// any custom storage buckets. + static String _getInstanceKey(String /*!*/ appName, String bucket) { + return '$appName|$bucket'; + } + + /// Instance of storage from Identity Provider API service. + storage_dart.FirebaseStorage? get _delegate => _app == null + ? null + : storage_dart.FirebaseStorage.instanceFor(app: _app!); + + final core_dart.FirebaseApp? _app; + + static Map _desktopFirebaseStorageInstances = + {}; + + ///Method call handleer + ///Todo need restructring + Future handleMethodCalls(MethodCall call) async { + Map arguments = call.arguments; + + switch (call.method) { + case 'Task#onProgress': + return _handleTaskStateChange(TaskState.running, arguments); + case 'Task#onPaused': + return _handleTaskStateChange(TaskState.paused, arguments); + case 'Task#onSuccess': + return _handleTaskStateChange(TaskState.success, arguments); + case 'Task#onCanceled': + return _sendTaskException( + arguments['handle'], + FirebaseException( + plugin: 'firebase_storage', + code: 'canceled', + message: 'User canceled the upload/download.', + )); + case 'Task#onFailure': + Map errorMap = + Map.from(arguments['error']); + return _sendTaskException( + arguments['handle'], + FirebaseException( + plugin: 'firebase_storage', + code: errorMap['code'], + message: errorMap['message'], + )); + } + } + + /// Returns a stub instance to allow the platform interface to access + /// the class instance statically. + static DesktopFirebaseStorage get instance { + return DesktopFirebaseStorage._(); + } + + static int _methodChannelHandleId = 0; + + /// Increments and returns the next channel ID handler for Storage. + static int get nextDesktopHandleId => _methodChannelHandleId++; + + /// A map containing all Task stream observers, keyed by their handle. + static final Map> taskObservers = + >{}; + + @override + int maxOperationRetryTime = const Duration(minutes: 2).inMilliseconds; + + @override + int maxUploadRetryTime = const Duration(minutes: 10).inMilliseconds; + + @override + int maxDownloadRetryTime = const Duration(minutes: 10).inMilliseconds; + + Future _handleTaskStateChange( + TaskState taskState, Map arguments) async { + // Get & cast native snapshot data to a Map + Map snapshotData = + Map.from(arguments['snapshot']); + + // Get the cached Storage instance. + FirebaseStoragePlatform storage = _desktopFirebaseStorageInstances[ + _getInstanceKey(arguments['appName'], arguments['bucket'])]!; + + // Create a snapshot. + TaskSnapshotPlatform snapshot = + DesktopTaskSnapshot(storage, taskState, snapshotData); + + // Fire a snapshot event. + taskObservers[arguments['handle']]!.add(snapshot); + } + + void _sendTaskException(int handle, FirebaseException exception) { + taskObservers[handle]!.addError(exception); + } + + @override + FirebaseStoragePlatform delegateFor( + {required FirebaseApp app, required String bucket}) { + String key = _getInstanceKey(app.name, bucket); + + return _desktopFirebaseStorageInstances[key] ??= + DesktopFirebaseStorage(app: app, bucket: bucket); + } + + @override + ReferencePlatform ref(String path) { + return DesktopReference(this, path); + } + + @override + Future useStorageEmulator(String host, int port) async { + emulatorHost = host; + emulatorPort = port; + try { + await _delegate!.useStorageEmulator(host: host, port: port); + // await DesktopFirebaseStorage.channel + // .invokeMethod('Storage#useEmulator', { + // 'appName': app.name, + // 'maxOperationRetryTime': maxOperationRetryTime, + // 'maxUploadRetryTime': maxUploadRetryTime, + // 'maxDownloadRetryTime': maxDownloadRetryTime, + // 'bucket': bucket, + // 'host': emulatorHost, + // 'port': emulatorPort + // }); + } catch (e, s) { + convertPlatformException(e, s); + } + } + + @override + void setMaxOperationRetryTime(int time) { + maxOperationRetryTime = time; + } + + @override + void setMaxUploadRetryTime(int time) { + maxUploadRetryTime = time; + } + + @override + Future setMaxDownloadRetryTime(int time) async { + maxDownloadRetryTime = time; + } +} diff --git a/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_list_result.dart b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_list_result.dart new file mode 100644 index 00000000..d51a9354 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_list_result.dart @@ -0,0 +1,34 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020, 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_desktop/src/desktop_reference.dart'; +import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart'; + +/// Implementation for a [ListResultPlatform]. +class DesktopListResult extends ListResultPlatform { + // ignore: public_member_api_docs + DesktopListResult( + FirebaseStoragePlatform storage, { + String? nextPageToken, + List? items, + List? prefixes, + }) : _items = items ?? [], + _prefixes = prefixes ?? [], + super(storage, nextPageToken); + + List _items; + + List _prefixes; + + @override + List get items { + return _items.map((path) => DesktopReference(storage!, path)).toList(); + } + + @override + List get prefixes { + return _prefixes.map((path) => DesktopReference(storage!, path)).toList(); + } +} diff --git a/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_reference.dart b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_reference.dart new file mode 100644 index 00000000..b73deff3 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_reference.dart @@ -0,0 +1,230 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020, 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'; +import 'dart:typed_data'; + +import 'package:firebase_storage_desktop/src/desktop_firebase_storage.dart'; +import 'package:firebase_storage_desktop/src/desktop_task.dart'; +import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart'; + +/// An implementation of [ReferencePlatform] that uses [Desktop] to +/// communicate with Firebase plugins. +class DesktopReference extends ReferencePlatform { + /// Creates a [ReferencePlatform] that is implemented using [Desktop]. + DesktopReference(FirebaseStoragePlatform storage, String path) + : super(storage, path); + + @override + Future delete() async { + throw UnimplementedError('delete() is not implemented'); + // try { + // await DesktopFirebaseStorage.channel + // .invokeMethod('Reference#delete', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'path': fullPath, + // }); + // } catch (e, stack) { + // convertPlatformException(e, stack); + // } + } + + @override + Future getDownloadURL() async { + throw UnimplementedError('getDownloadURL() is not implemented'); + // try { + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod( + // 'Reference#getDownloadURL', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'path': fullPath, + // }); + + // return data!['downloadURL']; + // } catch (e, stack) { + // convertPlatformException(e, stack); + // } + } + + @override + Future getMetadata() async { + throw UnimplementedError('getMetadata() is not implemented'); + // try { + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod( + // 'Reference#getMetadata', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'path': fullPath, + // }); + + // return FullMetadata(data!); + // } catch (e, stack) { + // convertPlatformException(e, stack); + // } + } + + @override + Future list([ListOptions? options]) async { + throw UnimplementedError('getMetadata() is not implemented'); + // try { + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod('Reference#list', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'path': fullPath, + // 'options': { + // 'maxResults': options?.maxResults ?? 1000, + // 'pageToken': options?.pageToken, + // }, + // }); + + // return DesktopListResult( + // storage, + // nextPageToken: data!['nextPageToken'], + // items: List.from(data['items']), + // prefixes: List.from(data['prefixes']), + // ); + // } catch (e, stack) { + // convertPlatformException(e, stack); + // } + } + + @override + Future listAll() async { + throw UnimplementedError('listAll() is not implemented'); + // try { + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod( + // 'Reference#listAll', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'path': fullPath, + // }); + // return DesktopListResult( + // storage, + // nextPageToken: data!['nextPageToken'], + // items: List.from(data['items']), + // prefixes: List.from(data['prefixes']), + // ); + // } catch (e, stack) { + // convertPlatformException(e, stack); + // } + } + + @override + Future getData(int maxSize) { + throw UnimplementedError('getData(int maxSize) is not implemented'); + // try { + // return DesktopFirebaseStorage.channel + // .invokeMethod('Reference#getData', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'path': fullPath, + // 'maxSize': maxSize, + // }); + // } catch (e, stack) { + // convertPlatformException(e, stack); + // } + } + + @override + TaskPlatform putData(Uint8List data, [SettableMetadata? metadata]) { + int handle = DesktopFirebaseStorage.nextDesktopHandleId; + DesktopFirebaseStorage.taskObservers[handle] = + StreamController.broadcast(); + return DesktopPutTask(handle, storage, fullPath, data, metadata); + } + + @override + TaskPlatform putBlob(dynamic data, [SettableMetadata? metadata]) { + throw UnimplementedError( + 'putBlob() is not supported on native platforms. Use [put], [putFile] or [putString] instead.'); + } + + @override + TaskPlatform putFile(File file, [SettableMetadata? metadata]) { + int handle = DesktopFirebaseStorage.nextDesktopHandleId; + DesktopFirebaseStorage.taskObservers[handle] = + StreamController.broadcast(); + return DesktopPutFileTask(handle, storage, fullPath, file, metadata); + } + + @override + TaskPlatform putString(String data, PutStringFormat format, + [SettableMetadata? metadata]) { + int handle = DesktopFirebaseStorage.nextDesktopHandleId; + DesktopFirebaseStorage.taskObservers[handle] = + StreamController.broadcast(); + return DesktopPutStringTask( + handle, storage, fullPath, data, format, metadata); + } + + @override + Future updateMetadata(SettableMetadata metadata) async { + throw UnimplementedError( + 'updateMetadata(SettableMetadata metadata) is not implemented'); + // try { + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod( + // 'Reference#updateMetadata', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'path': fullPath, + // 'metadata': metadata.asMap(), + // }); + + // return FullMetadata(data!); + // } catch (e, stack) { + // convertPlatformException(e, stack); + // } + } + + @override + TaskPlatform writeToFile(File file) { + int handle = DesktopFirebaseStorage.nextDesktopHandleId; + DesktopFirebaseStorage.taskObservers[handle] = + StreamController.broadcast(); + return DesktopDownloadTask(handle, storage, fullPath, file); + } +} diff --git a/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_task.dart b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_task.dart new file mode 100644 index 00000000..f4952e4a --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_task.dart @@ -0,0 +1,326 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020, 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'; +import 'dart:typed_data'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_storage_desktop/src/desktop_firebase_storage.dart'; +import 'package:firebase_storage_desktop/src/desktop_task_snapshot.dart'; +import 'package:firebase_storage_desktop/src/utils/exceptions.dart'; +import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart'; + +/// Implementation for a [TaskPlatform]. +/// +/// Other implementations for specific tasks should extend this class. +class DesktopTask extends TaskPlatform { + /// Creates a new [DesktopTask] with a given task. + DesktopTask( + this._handle, + this.storage, + String path, + this._initialTask, + ) : super() { + // Keep reference to whether the initial "start" task has completed. + _initialTaskCompleter = Completer(); + _snapshot = DesktopTaskSnapshot(storage, TaskState.running, { + 'path': path, + 'bytesTransferred': 0, + 'totalBytes': 1, + }); + _initialTask().then((_) { + _initialTaskCompleter.complete(); + }).catchError((Object e, StackTrace stackTrace) { + _initialTaskCompleter.completeError(e, stackTrace); + _didComplete = true; + _exception = e; + _stackTrace = stackTrace; + if (_completer != null) { + catchFuturePlatformException(e, stackTrace) + .catchError(_completer!.completeError); + } + }); + + // Get the task stream. + _stream = DesktopFirebaseStorage.taskObservers[_handle]!.stream + as Stream; + late StreamSubscription _subscription; + + // Listen for stream events. + _subscription = _stream.listen((TaskSnapshotPlatform snapshot) async { + if (_snapshot.state != TaskState.canceled) { + _snapshot = snapshot; + } + + // If the stream event is complete, trigger the + // completer to resolve with the snapshot. + if (snapshot.state == TaskState.success) { + _didComplete = true; + _completer?.complete(snapshot); + await _subscription.cancel(); + } + }, onError: (Object e, StackTrace stackTrace) { + if (e is FirebaseException && e.code == 'canceled') { + _snapshot = DesktopTaskSnapshot(storage, TaskState.canceled, { + 'path': path, + 'bytesTransferred': _snapshot.bytesTransferred, + 'totalBytes': _snapshot.totalBytes, + }); + } else { + _snapshot = DesktopTaskSnapshot(storage, TaskState.error, { + 'path': path, + 'bytesTransferred': _snapshot.bytesTransferred, + 'totalBytes': _snapshot.totalBytes, + }); + } + _didComplete = true; + _exception = e; + _stackTrace = stackTrace; + if (_completer != null) { + catchFuturePlatformException(e, stackTrace) + .catchError(_completer!.completeError); + } + }, cancelOnError: true); + } + + Object? _exception; + + late StackTrace _stackTrace; + + bool _didComplete = false; + + Completer? _completer; + + late Stream _stream; + + late Completer _initialTaskCompleter; + + Future Function() _initialTask; + + final int _handle; + + /// The [FirebaseStoragePlatform] used to create the task. + final FirebaseStoragePlatform storage; + + late TaskSnapshotPlatform _snapshot; + + @override + Stream get snapshotEvents { + return DesktopFirebaseStorage.taskObservers[_handle]!.stream + as Stream; + } + + @override + TaskSnapshotPlatform get snapshot => _snapshot; + + @override + Future get onComplete async { + if (_didComplete && _exception == null) { + return Future.value(snapshot); + } else if (_didComplete && _exception != null) { + return catchFuturePlatformException(_exception!, _stackTrace); + } else { + _completer ??= Completer(); + return _completer!.future; + } + } + + @override + Future pause() async { + throw UnimplementedError('pause() is not implemented'); + // try { + // if (!_initialTaskCompleter.isCompleted) { + // await _initialTaskCompleter.future; + // } + + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod('Task#pause', { + // 'handle': _handle, + // }); + + // bool success = data!['status']; + // if (success) { + // _snapshot = DesktopTaskSnapshot(storage, TaskState.paused, + // Map.from(data['snapshot'])); + // } + // return success; + // } catch (e, stack) { + // return catchFuturePlatformException(e, stack); + // } + } + + @override + Future resume() async { + throw UnimplementedError('resume() is not implemented'); + // try { + // if (!_initialTaskCompleter.isCompleted) { + // await _initialTaskCompleter.future; + // } + + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod('Task#resume', { + // 'handle': _handle, + // }); + + // bool success = data!['status']; + // if (success) { + // _snapshot = DesktopTaskSnapshot(storage, TaskState.running, + // Map.from(data['snapshot'])); + // } + // return success; + // } catch (e, stack) { + // return catchFuturePlatformException(e, stack); + } +} + +@override +Future cancel() async { + throw UnimplementedError('cancel() is not implemented'); + // try { + // if (!_initialTaskCompleter.isCompleted) { + // await _initialTaskCompleter.future; + // } + + // Map? data = await DesktopFirebaseStorage.channel + // .invokeMapMethod('Task#cancel', { + // 'handle': _handle, + // }); + + // bool success = data!['status']; + // if (success) { + // _snapshot = DesktopTaskSnapshot(storage, TaskState.canceled, + // Map.from(data['snapshot'])); + // } + // return success; + // } catch (e, stack) { + // return catchFuturePlatformException(e, stack); + // } + // } +} + +/// Implementation for [putFile] tasks. +class DesktopPutFileTask extends DesktopTask { + // ignore: public_member_api_docs + DesktopPutFileTask(int handle, FirebaseStoragePlatform storage, String path, + File file, SettableMetadata? metadata) + : super(handle, storage, path, + _getTask(handle, storage, path, file, metadata)); + + static Future Function() _getTask( + int handle, + FirebaseStoragePlatform storage, + String path, + File file, + SettableMetadata? metadata) { + throw UnimplementedError('_geTask is not implemented'); + // return () => DesktopFirebaseStorage.channel + // .invokeMethod('Task#startPutFile', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'bucket': storage.bucket, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'handle': handle, + // 'path': path, + // 'filePath': file.absolute.path, + // 'metadata': metadata?.asMap(), + // }); + } +} + +/// Implementation for [putString] tasks. +class DesktopPutStringTask extends DesktopTask { + // ignore: public_member_api_docs + DesktopPutStringTask(int handle, FirebaseStoragePlatform storage, String path, + String data, PutStringFormat format, SettableMetadata? metadata) + : super(handle, storage, path, + _getTask(handle, storage, path, data, format, metadata)); + + static Future Function() _getTask( + int handle, + FirebaseStoragePlatform storage, + String path, + String data, + PutStringFormat format, + SettableMetadata? metadata) { + throw UnimplementedError('_geTask is not implemented'); + // return () => DesktopFirebaseStorage.channel + // .invokeMethod('Task#startPutString', { + // 'appName': storage.app.name, + // 'bucket': storage.bucket, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'handle': handle, + // 'path': path, + // 'data': data, + // 'format': format.index, + // 'metadata': metadata?.asMap(), + // }); + } +} + +/// Implementation for [put] tasks. +class DesktopPutTask extends DesktopTask { + // ignore: public_member_api_docs + DesktopPutTask(int handle, FirebaseStoragePlatform storage, String path, + Uint8List data, SettableMetadata? metadata) + : super(handle, storage, path, + _getTask(handle, storage, path, data, metadata)); + + static Future Function() _getTask( + int handle, + FirebaseStoragePlatform storage, + String path, + Uint8List data, + SettableMetadata? metadata) { + throw UnimplementedError('_geTask is not implemented'); + // return () => DesktopFirebaseStorage.channel + // .invokeMethod('Task#startPutData', { + // 'appName': storage.app.name, + // 'bucket': storage.bucket, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'handle': handle, + // 'path': path, + // 'data': data, + // 'metadata': metadata?.asMap(), + // }); + } +} + +/// Implementation for [writeToFile] tasks. +class DesktopDownloadTask extends DesktopTask { + // ignore: public_member_api_docs + DesktopDownloadTask( + int handle, FirebaseStoragePlatform storage, String path, File file) + : super(handle, storage, path, _getTask(handle, storage, path, file)); + + static Future Function() _getTask( + int handle, FirebaseStoragePlatform storage, String path, File file) { + throw UnimplementedError('_geTask is not implemented'); + // return () => DesktopFirebaseStorage.channel + // .invokeMethod('Task#writeToFile', { + // 'appName': storage.app.name, + // 'maxOperationRetryTime': storage.maxOperationRetryTime, + // 'maxUploadRetryTime': storage.maxUploadRetryTime, + // 'maxDownloadRetryTime': storage.maxDownloadRetryTime, + // 'host': storage.emulatorHost, + // 'port': storage.emulatorPort, + // 'bucket': storage.bucket, + // 'handle': handle, + // 'path': path, + // 'filePath': file.path, + // }); + } +} diff --git a/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_task_snapshot.dart b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_task_snapshot.dart new file mode 100644 index 00000000..33f51088 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/lib/src/desktop_task_snapshot.dart @@ -0,0 +1,24 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020, 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_desktop/src/desktop_reference.dart'; +import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart'; + +/// Implementation for a [TaskSnapshotPlatform]. +class DesktopTaskSnapshot extends TaskSnapshotPlatform { + // ignore: public_member_api_docs + DesktopTaskSnapshot(this.storage, TaskState state, this._data) + : super(state, _data..addAll({'state': state})); + + /// The [FirebaseStoragePlatform] used to create the task. + final FirebaseStoragePlatform storage; + + final Map _data; + + @override + ReferencePlatform get ref { + return DesktopReference(storage, _data['path']); + } +} diff --git a/packages/firebase_storage/firebase_storage_desktop/lib/src/utils/exceptions.dart b/packages/firebase_storage/firebase_storage_desktop/lib/src/utils/exceptions.dart new file mode 100644 index 00000000..55bf2db7 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/lib/src/utils/exceptions.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/services.dart'; + +/// Catches a [PlatformException] and returns an [Exception]. +/// +/// If the [Exception] is a [PlatformException], a [FirebaseException] is returned. +Never convertPlatformException( + dynamic exception, + StackTrace stackTrace, +) { + if (exception is! Exception || exception is! PlatformException) { + Error.throwWithStackTrace(exception, stackTrace); + } + + Error.throwWithStackTrace( + platformExceptionToFirebaseException(exception, stackTrace), + stackTrace, + ); +} + +/// Catches a [PlatformException] and converts it into a [FirebaseException] if +/// it was intentionally caught on the native platform. +Future catchFuturePlatformException( + Object exception, + StackTrace stackTrace, +) { + if (exception is! Exception || exception is! PlatformException) { + return Future.error(exception, stackTrace); + } + + return Future.error( + platformExceptionToFirebaseException(exception, stackTrace), + stackTrace, + ); +} + +/// Converts a [PlatformException] into a [FirebaseException]. +/// +/// A [PlatformException] can only be converted to a [FirebaseException] if the +/// `details` of the exception exist. Firebase returns specific codes and messages +/// which can be converted into user friendly exceptions. +FirebaseException platformExceptionToFirebaseException( + PlatformException platformException, + StackTrace stackTrace, +) { + Map? details = platformException.details != null + ? Map.from(platformException.details) + : null; + + String code = 'unknown'; + String message = platformException.message ?? ''; + + if (details != null) { + code = details['code'] ?? code; + message = details['message'] ?? message; + } + + // TODO(ehesp): Add stack trace support when it lands + return FirebaseException( + plugin: 'firebase_storage', code: code, message: message); +} diff --git a/packages/firebase_storage/firebase_storage_desktop/pubspec.yaml b/packages/firebase_storage/firebase_storage_desktop/pubspec.yaml new file mode 100644 index 00000000..3efc72ea --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/pubspec.yaml @@ -0,0 +1,70 @@ +name: firebase_storage_desktop +description: A new Flutter package project. +version: 0.1.0-dev.0 +homepage: + +environment: + sdk: ">=2.16.2 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + firebase_storage_platform_interface: ^4.1.4 + firebase_core_dart: ^0.1.1 + firebase_storage_dart: + path: ../firebase_storage_dart + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + plugin: + implements: firebase_storage + platforms: + macos: + dartPluginClass: FirebaseStorageDesktop + pluginClass: none + linux: + dartPluginClass: FirebaseStorageDesktop + pluginClass: none + windows: + dartPluginClass: FirebaseStorageDesktop + pluginClass: none + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_storage/firebase_storage_desktop/test/firebase_storage_desktop_test.dart b/packages/firebase_storage/firebase_storage_desktop/test/firebase_storage_desktop_test.dart new file mode 100644 index 00000000..0070a8b3 --- /dev/null +++ b/packages/firebase_storage/firebase_storage_desktop/test/firebase_storage_desktop_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:firebase_storage_desktop/firebase_storage_desktop.dart'; + +void main() { + test('adds one to input values', () {}); +}