diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 04fd0cbf69..d8452c5f24 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -613,6 +613,12 @@ workflow use only during the development of your app, not for publicly shipping code. ## Release Notes +### Upcoming +- Changes + - Storage: Add support for Firebase Storage emulator via `UseEmulator`. + The `UseEmulator` method should be called before invoking any other + methods on a new instance of Storage. Default port is 9199. + ### 13.2.0 - Changes - General (Android): Update to Firebase Android BoM version 34.4.0. diff --git a/storage/src/android/storage_android.cc b/storage/src/android/storage_android.cc index c4d2c953cb..eecb6ed459 100644 --- a/storage/src/android/storage_android.cc +++ b/storage/src/android/storage_android.cc @@ -62,7 +62,9 @@ namespace internal { "(Ljava/lang/String;)" \ "Lcom/google/firebase/storage/StorageReference;"), \ X(GetApp, "getApp", \ - "()Lcom/google/firebase/FirebaseApp;") + "()Lcom/google/firebase/FirebaseApp;"), \ + X(UseEmulator, "useEmulator", \ + "(Ljava/lang/String;I)V") // clang-format on METHOD_LOOKUP_DECLARATION(firebase_storage, FIREBASE_STORAGE_METHODS) @@ -471,6 +473,23 @@ void StorageInternal::set_max_operation_retry_time( millis); } +void StorageInternal::UseEmulator(const char* host, int port) { + JNIEnv* env = app_->GetJNIEnv(); + FIREBASE_ASSERT_MESSAGE_RETURN_VOID((host != nullptr && host[0] != '\0'), + "Emulator host cannot be null or empty.") + FIREBASE_ASSERT_MESSAGE_RETURN_VOID( + (port > 0), "Emulator port must be a positive number.") + + jobject host_string = env->NewStringUTF(host); + jint port_num = static_cast(port); + + env->CallVoidMethod( + obj_, firebase_storage::GetMethodId(firebase_storage::kUseEmulator), + host_string, port_num); + env->DeleteLocalRef(host_string); + util::CheckAndClearJniExceptions(env); +} + } // namespace internal } // namespace storage } // namespace firebase diff --git a/storage/src/android/storage_android.h b/storage/src/android/storage_android.h index 4ed8774a47..b25fa42e03 100644 --- a/storage/src/android/storage_android.h +++ b/storage/src/android/storage_android.h @@ -92,6 +92,10 @@ class StorageInternal { // if a failure occurs. void set_max_operation_retry_time(double max_transfer_retry_seconds); + // Configures the Storage SDK to use an emulated backend instead of call the + // default remote backend + void UseEmulator(const char* host, int port); + // Convert an error code obtained from a Java StorageException into a C++ // Error enum. Error ErrorFromJavaErrorCode(jint java_error_code) const; diff --git a/storage/src/common/storage.cc b/storage/src/common/storage.cc index 7678bf6a31..9e5392b524 100644 --- a/storage/src/common/storage.cc +++ b/storage/src/common/storage.cc @@ -226,5 +226,9 @@ void Storage::set_max_operation_retry_time(double max_transfer_retry_seconds) { return internal_->set_max_operation_retry_time(max_transfer_retry_seconds); } +void Storage::UseEmulator(const char* host, int port) { + if (internal_) internal_->UseEmulator(host, port); +} + } // namespace storage } // namespace firebase diff --git a/storage/src/desktop/metadata_desktop.cc b/storage/src/desktop/metadata_desktop.cc index d0d3fc86c1..a931f248fd 100644 --- a/storage/src/desktop/metadata_desktop.cc +++ b/storage/src/desktop/metadata_desktop.cc @@ -133,7 +133,8 @@ const char* MetadataInternal::download_url() const { std::string MetadataInternal::GetPathFromToken(const std::string& token) const { std::string http_url = - StoragePath("gs://" + bucket_ + "/" + path_).AsHttpUrl(); + StoragePath(storage_internal_, "gs://" + bucket_ + "/" + path_) + .AsHttpUrl(); if (!token.empty()) http_url += "&token=" + token; return http_url; } diff --git a/storage/src/desktop/storage_desktop.cc b/storage/src/desktop/storage_desktop.cc index 10ed0896b5..4d311ea11c 100644 --- a/storage/src/desktop/storage_desktop.cc +++ b/storage/src/desktop/storage_desktop.cc @@ -23,6 +23,7 @@ #include "app/src/app_common.h" #include "app/src/function_registry.h" #include "app/src/include/firebase/app.h" +#include "app/src/log.h" #include "storage/src/desktop/rest_operation.h" #include "storage/src/desktop/storage_reference_desktop.h" @@ -35,10 +36,10 @@ StorageInternal::StorageInternal(App* app, const char* url) { if (url) { url_ = url; - root_ = StoragePath(url_); + root_ = StoragePath(this, url_); } else { const char* bucket = app->options().storage_bucket(); - root_ = StoragePath(bucket ? std::string(kGsScheme) + bucket : ""); + root_ = StoragePath(this, bucket ? std::string(kGsScheme) + bucket : ""); } // LINT.IfChange @@ -76,20 +77,22 @@ StorageInternal::~StorageInternal() { } // Get a StorageReference to the root of the database. -StorageReferenceInternal* StorageInternal::GetReference() const { +StorageReferenceInternal* StorageInternal::GetReference() { + configured_ = true; return new StorageReferenceInternal(url_, const_cast(this)); } // Get a StorageReference for the specified path. -StorageReferenceInternal* StorageInternal::GetReference( - const char* path) const { +StorageReferenceInternal* StorageInternal::GetReference(const char* path) { + configured_ = true; return new StorageReferenceInternal(root_.GetChild(path), const_cast(this)); } // Get a StorageReference for the provided URL. StorageReferenceInternal* StorageInternal::GetReferenceFromUrl( - const char* url) const { + const char* url) { + configured_ = true; return new StorageReferenceInternal(url, const_cast(this)); } @@ -134,6 +137,30 @@ void StorageInternal::CleanupCompletedOperations() { } } +void StorageInternal::UseEmulator(const char* host, int port) { + if (host == nullptr || host[0] == '\0') { + LogError("Emulator host cannot be null or empty."); + return; + } + + if (port <= 0) { + LogError("Emulator port must be a positive number."); + return; + } + + if (configured_) { + LogError( + "Cannot connect to emulator after Storage SDK initialization. " + "Call use_emulator(host, port) before creating a Storage " + "reference or trying to load data."); + return; + } + + scheme_ = "http"; + port_ = port; + host_ = host; +} + } // namespace internal } // namespace storage } // namespace firebase diff --git a/storage/src/desktop/storage_desktop.h b/storage/src/desktop/storage_desktop.h index d20f544952..d0020c96ea 100644 --- a/storage/src/desktop/storage_desktop.h +++ b/storage/src/desktop/storage_desktop.h @@ -43,13 +43,13 @@ class StorageInternal { std::string url() { return url_; } // Get a StorageReference to the root of the database. - StorageReferenceInternal* GetReference() const; + StorageReferenceInternal* GetReference(); // Get a StorageReference for the specified path. - StorageReferenceInternal* GetReference(const char* path) const; + StorageReferenceInternal* GetReference(const char* path); // Get a StorageReference for the provided URL. - StorageReferenceInternal* GetReferenceFromUrl(const char* url) const; + StorageReferenceInternal* GetReferenceFromUrl(const char* url); // Returns the maximum time (in seconds) to retry a download if a failure // occurs. @@ -99,6 +99,19 @@ class StorageInternal { // Remove an operation from the list of outstanding operations. void RemoveOperation(RestOperation* operation); + // Configures the Storage SDK to use an emulated backend instead of call the + // default remote backend + void UseEmulator(const char* host, int port); + + // Returns the Host for the storage backend + std::string get_host() { return host_; } + + // Returns the Port for the storage backend + int get_port() { return port_; } + + // Returns the url scheme currenly in use for the storage backend + std::string get_scheme() { return scheme_; } + private: // Clean up completed operations. void CleanupCompletedOperations(); @@ -119,6 +132,10 @@ class StorageInternal { std::string user_agent_; Mutex operations_mutex_; std::vector operations_; + std::string host_ = "firebasestorage.googleapis.com"; + std::string scheme_ = "https"; + int port_ = 443; + bool configured_ = false; }; } // namespace internal diff --git a/storage/src/desktop/storage_path.cc b/storage/src/desktop/storage_path.cc index 56d0666e84..c07766d987 100644 --- a/storage/src/desktop/storage_path.cc +++ b/storage/src/desktop/storage_path.cc @@ -16,10 +16,12 @@ #include +#include #include #include "app/rest/util.h" #include "app/src/include/firebase/internal/common.h" +#include "storage/src/desktop/storage_desktop.h" namespace firebase { namespace storage { @@ -38,8 +40,10 @@ const char kBucketStartString[] = "firebasestorage.googleapis.com/v0/b/"; const size_t kBucketStartStringLength = FIREBASE_STRLEN(kBucketStartString); const char kBucketEndString[] = "/o/"; const size_t kBucketEndStringLength = FIREBASE_STRLEN(kBucketEndString); +const char kBucketIdentifierString[] = "/v0/b/"; -StoragePath::StoragePath(const std::string& path) { +StoragePath::StoragePath(StorageInternal* storage, const std::string& path) { + storage_internal_ = storage; bucket_ = ""; path_ = Path(""); if (path.compare(0, kGsSchemeLength, kGsScheme) == 0) { @@ -56,8 +60,9 @@ StoragePath::StoragePath(const std::string& path) { // Constructs a storage path, based on raw strings for the bucket, path, and // object. -StoragePath::StoragePath(const std::string& bucket, const std::string& path, - const std::string& object) { +StoragePath::StoragePath(StorageInternal* storage, const std::string& bucket, + const std::string& path, const std::string& object) { + storage_internal_ = storage; bucket_ = bucket; path_ = Path(path).GetChild(object); } @@ -97,15 +102,20 @@ void StoragePath::ConstructFromHttpUrl(const std::string& url, int path_start) { std::string StoragePath::AsHttpUrl() const { static const char* kUrlEnd = "?alt=media"; // Construct the URL. Final format is: - // https://[projectname].googleapis.com/v0/b/[bucket]/o/[path and/or object] + // http[s]://[host]:[port]/v0/b/[bucket]/o/[path and/or object] return AsHttpMetadataUrl() + kUrlEnd; } std::string StoragePath::AsHttpMetadataUrl() const { // Construct the URL. Final format is: - // https://[projectname].googleapis.com/v0/b/[bucket]/o/[path and/or object] - std::string result = kHttpsScheme; - result += kBucketStartString; + // [scheme]://[host]:[port]/v0/b/[bucket]/o/[path and/or object] + + std::string result = storage_internal_->get_scheme(); + result += "://"; + result += storage_internal_->get_host(); + result += ":"; + result += std::to_string(storage_internal_->get_port()); + result += kBucketIdentifierString; result += bucket_; result += kBucketEndString; result += rest::util::EncodeUrl(path_.str()); diff --git a/storage/src/desktop/storage_path.h b/storage/src/desktop/storage_path.h index 4f7bc6ac19..bffc7c79bb 100644 --- a/storage/src/desktop/storage_path.h +++ b/storage/src/desktop/storage_path.h @@ -25,6 +25,8 @@ namespace internal { extern const char kGsScheme[]; +class StorageInternal; + // Class for managing paths for firebase storage. // Storage paths are made up of a bucket, a path, // and (optionally) an object, located at that path. @@ -35,12 +37,12 @@ class StoragePath { // Constructs a storage path, based on an input URL. The URL can either be // an HTTP[s] link, or a gs URI. - explicit StoragePath(const std::string& path); + explicit StoragePath(StorageInternal* storage, const std::string& path); // Constructs a storage path, based on raw strings for the bucket, path, and // object. - StoragePath(const std::string& bucket, const std::string& path, - const std::string& object = ""); + StoragePath(StorageInternal* storage, const std::string& bucket, + const std::string& path, const std::string& object = ""); // The bucket portion of this path. // In the path: MyBucket/folder/object, it would return "MyBucket". @@ -60,14 +62,14 @@ class StoragePath { // in a path where bucket is "bucket", local_path is "path/otherchild/" and // object is an empty string. StoragePath GetChild(const std::string& path) const { - return StoragePath(bucket_, path_.GetChild(path)); + return StoragePath(storage_internal_, bucket_, path_.GetChild(path)); } // Returns the location one folder up from the current location. If the // path is at already at the root level, this returns the path unchanged. // The Object in the result is always set to empty. StoragePath GetParent() const { - return StoragePath(bucket_, path_.GetParent()); + return StoragePath(storage_internal_, bucket_, path_.GetParent()); } // Returns the path as a HTTP URL to the asset. @@ -82,14 +84,16 @@ class StoragePath { private: static const char* const kSeparator; - StoragePath(const std::string& bucket, const Path& path) - : bucket_(bucket), path_(path) {} + StoragePath(StorageInternal* storage, const std::string& bucket, + const Path& path) + : storage_internal_(storage), bucket_(bucket), path_(path) {} void ConstructFromGsUri(const std::string& uri, int path_start); void ConstructFromHttpUrl(const std::string& url, int path_start); std::string bucket_; Path path_; + StorageInternal* storage_internal_; }; } // namespace internal diff --git a/storage/src/desktop/storage_reference_desktop.cc b/storage/src/desktop/storage_reference_desktop.cc index aa99863e3b..11c1be720a 100644 --- a/storage/src/desktop/storage_reference_desktop.cc +++ b/storage/src/desktop/storage_reference_desktop.cc @@ -48,7 +48,7 @@ namespace internal { StorageReferenceInternal::StorageReferenceInternal( const std::string& storageUri, StorageInternal* storage) - : storage_(storage), storageUri_(storageUri) { + : storage_(storage), storageUri_(storage, storageUri) { storage_->future_manager().AllocFutureApi(this, kStorageReferenceFnCount); } diff --git a/storage/src/include/firebase/storage.h b/storage/src/include/firebase/storage.h index 8d081e4c0e..94c7b8fc94 100644 --- a/storage/src/include/firebase/storage.h +++ b/storage/src/include/firebase/storage.h @@ -139,6 +139,17 @@ class Storage { /// download if a failure occurs. Defaults to 120 seconds (2 minutes). void set_max_operation_retry_time(double max_transfer_retry_seconds); + /// @brief Configures the Storage SDK to use an emulated backend instead of + /// the default remote backend. This method should be called before invoking + /// any other methods on a new instance of Storage + void UseEmulator(const std::string& host, int port) { + UseEmulator(host.c_str(), port); + } + /// @brief Configures the Storage SDK to use an emulated backend instead of + /// the default remote backend. This method should be called before invoking + /// any other methods on a new instance of Storage + void UseEmulator(const char* host, int port); + private: /// @cond FIREBASE_APP_INTERNAL friend class Metadata; diff --git a/storage/src/ios/storage_ios.h b/storage/src/ios/storage_ios.h index 9a0a09dcd0..8cd23fc506 100644 --- a/storage/src/ios/storage_ios.h +++ b/storage/src/ios/storage_ios.h @@ -92,6 +92,10 @@ class StorageInternal { // if a failure occurs. void set_max_operation_retry_time(double max_transfer_retry_seconds); + // Configures the Storage SDK to use an emulated backend instead of call the + // default remote backend + void UseEmulator(const char* _Nullable host, int port); + FutureManager& future_manager() { return future_manager_; } // Whether this object was successfully initialized by the constructor. diff --git a/storage/src/ios/storage_ios.mm b/storage/src/ios/storage_ios.mm index ccad7e3b2d..ef3fd970b7 100644 --- a/storage/src/ios/storage_ios.mm +++ b/storage/src/ios/storage_ios.mm @@ -22,6 +22,7 @@ #include "storage/src/ios/storage_reference_ios.h" #import "FirebaseStorage-Swift.h" +#include namespace firebase { namespace storage { @@ -97,6 +98,17 @@ impl().maxOperationRetryTime = max_transfer_retry_seconds; } +void StorageInternal::UseEmulator(const char* host, int port) { + + NSCAssert(host && host[0] != '\0', @"Emulator host cannot be null or empty."); + + NSCAssert(port > 0, @"Emulator port must be a positive number."); + + NSString *hostString = [NSString stringWithUTF8String:host]; + + [impl() useEmulatorWithHost:hostString port:port]; +} + // Whether this object was successfully initialized by the constructor. bool StorageInternal::initialized() const { return impl() != nil; } diff --git a/storage/tests/desktop/storage_desktop_utils_tests.cc b/storage/tests/desktop/storage_desktop_utils_tests.cc index 7322cab8a6..79d4db83a1 100644 --- a/storage/tests/desktop/storage_desktop_utils_tests.cc +++ b/storage/tests/desktop/storage_desktop_utils_tests.cc @@ -48,23 +48,23 @@ TEST_F(StorageDesktopUtilsTests, testGSStoragePathConstructors) { StoragePath test_path; // Test basic case: - test_path = StoragePath("gs://Bucket/path/Object"); + test_path = StoragePath(nullptr, "gs://Bucket/path/Object"); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path/Object"); // Test a more complex path: - test_path = StoragePath("gs://Bucket/path/morepath/Object"); + test_path = StoragePath(nullptr, "gs://Bucket/path/morepath/Object"); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path/morepath/Object"); // Extra slashes: - test_path = StoragePath("gs://Bucket/path////Object"); + test_path = StoragePath(nullptr, "gs://Bucket/path////Object"); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path/Object"); // Path with no Object: - test_path = StoragePath("gs://Bucket/path////more////"); + test_path = StoragePath(nullptr, "gs://Bucket/path////more////"); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path/more"); } @@ -76,29 +76,30 @@ TEST_F(StorageDesktopUtilsTests, testHTTPStoragePathConstructors) { std::string intended_path_result = "path/to/Object/Object.data"; // Test basic case: - test_path = StoragePath( - "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" - "path%2fto%2FObject%2fObject.data"); + test_path = StoragePath(nullptr, + "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2fto%2FObject%2fObject.data"); EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); // httpS (instead of http): - test_path = StoragePath( - "https://firebasestorage.googleapis.com/v0/b/Bucket/o/" - "path%2fto%2FObject%2fObject.data"); + test_path = + StoragePath(nullptr, + "https://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2fto%2FObject%2fObject.data"); EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); // Extra slashes: - test_path = StoragePath( - "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" - "path%2f%2f%2f%2fto%2FObject%2f%2f%2f%2fObject.data"); + test_path = StoragePath(nullptr, + "http://firebasestorage.googleapis.com/v0/b/Bucket/o/" + "path%2f%2f%2f%2fto%2FObject%2f%2f%2f%2fObject.data"); EXPECT_STREQ(test_path.GetBucket().c_str(), intended_bucket_result.c_str()); EXPECT_STREQ(test_path.GetPath().c_str(), intended_path_result.c_str()); } TEST_F(StorageDesktopUtilsTests, testInvalidConstructors) { - StoragePath bad_path("argleblargle://Bucket/path1/path2/Object"); + StoragePath bad_path(nullptr, "argleblargle://Bucket/path1/path2/Object"); EXPECT_FALSE(bad_path.IsValid()); } @@ -107,12 +108,12 @@ TEST_F(StorageDesktopUtilsTests, testStoragePathParent) { StoragePath test_path; // Test parent, when there is an GetObject. - test_path = StoragePath("gs://Bucket/path/Object").GetParent(); + test_path = StoragePath(nullptr, "gs://Bucket/path/Object").GetParent(); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path"); // Test parent with no GetObject. - test_path = StoragePath("gs://Bucket/path/morepath/").GetParent(); + test_path = StoragePath(nullptr, "gs://Bucket/path/morepath/").GetParent(); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path"); } @@ -122,27 +123,32 @@ TEST_F(StorageDesktopUtilsTests, testStoragePathChild) { StoragePath test_path; // Test child when there is no object. - test_path = StoragePath("gs://Bucket/path/morepath/").GetChild("newobj"); + test_path = + StoragePath(nullptr, "gs://Bucket/path/morepath/").GetChild("newobj"); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path/morepath/newobj"); // Test child when there is an object. - test_path = StoragePath("gs://Bucket/path/object").GetChild("newpath/"); + test_path = + StoragePath(nullptr, "gs://Bucket/path/object").GetChild("newpath/"); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path/object/newpath"); } TEST_F(StorageDesktopUtilsTests, testUrlConverter) { - StoragePath test_path("gs://Bucket/path1/path2/Object"); + std::unique_ptr app(firebase::testing::CreateApp()); + StorageInternal* storage = new StorageInternal(app.get(), "gs://Bucket"); + + StoragePath test_path(storage, "gs://Bucket/path1/path2/Object"); EXPECT_STREQ(test_path.GetBucket().c_str(), "Bucket"); EXPECT_STREQ(test_path.GetPath().c_str(), "path1/path2/Object"); EXPECT_STREQ(test_path.AsHttpUrl().c_str(), - "https://firebasestorage.googleapis.com" + "https://firebasestorage.googleapis.com:443" "/v0/b/Bucket/o/path1%2Fpath2%2FObject?alt=media"); EXPECT_STREQ(test_path.AsHttpMetadataUrl().c_str(), - "https://firebasestorage.googleapis.com" + "https://firebasestorage.googleapis.com:443" "/v0/b/Bucket/o/path1%2Fpath2%2FObject"); }