diff --git a/storage/CMakeLists.txt b/storage/CMakeLists.txt index e2fd88e72f..e4a722fd9f 100644 --- a/storage/CMakeLists.txt +++ b/storage/CMakeLists.txt @@ -19,6 +19,7 @@ set(common_SRCS src/common/common.cc src/common/controller.cc src/common/listener.cc + src/common/list_result.cc src/common/metadata.cc src/common/storage.cc src/common/storage_reference.cc @@ -36,6 +37,7 @@ binary_to_array("storage_resources" set(android_SRCS ${storage_resources_source} src/android/controller_android.cc + src/android/list_result_android.cc src/android/metadata_android.cc src/android/storage_android.cc src/android/storage_reference_android.cc) @@ -44,6 +46,7 @@ set(android_SRCS set(ios_SRCS src/ios/controller_ios.mm src/ios/listener_ios.mm + src/ios/list_result_ios.mm src/ios/metadata_ios.mm src/ios/storage_ios.mm src/ios/storage_reference_ios.mm @@ -54,6 +57,7 @@ set(desktop_SRCS src/desktop/controller_desktop.cc src/desktop/curl_requests.cc src/desktop/listener_desktop.cc + src/desktop/list_result_desktop.cc src/desktop/metadata_desktop.cc src/desktop/rest_operation.cc src/desktop/storage_desktop.cc diff --git a/storage/integration_test/src/integration_test.cc b/storage/integration_test/src/integration_test.cc index f430de37de..8f305993c2 100644 --- a/storage/integration_test/src/integration_test.cc +++ b/storage/integration_test/src/integration_test.cc @@ -20,6 +20,7 @@ #include #include #include // NOLINT +#include // For std::vector in list tests #include "app_framework.h" // NOLINT #include "firebase/app.h" @@ -80,6 +81,8 @@ using app_framework::PathForResource; using app_framework::ProcessEvents; using firebase_test_framework::FirebaseTest; using testing::ElementsAreArray; +using testing::IsEmpty; +using testing::UnorderedElementsAreArray; class FirebaseStorageTest : public FirebaseTest { public: @@ -96,7 +99,6 @@ class FirebaseStorageTest : public FirebaseTest { // Called after each test. void TearDown() override; - // File references that we need to delete on test exit. protected: // Initialize Firebase App and Firebase Auth. static void InitializeAppAndAuth(); @@ -118,6 +120,17 @@ class FirebaseStorageTest : public FirebaseTest { // Create a unique working folder and return a reference to it. firebase::storage::StorageReference CreateFolder(); + // Uploads a string as a file to the given StorageReference. + void UploadStringAsFile(firebase::storage::StorageReference& ref, + const std::string& content, + const char* content_type = nullptr); + + // Verifies the contents of a ListResult. + void VerifyListResultContains( + const firebase::storage::ListResult& list_result, + const std::vector& expected_item_names, + const std::vector& expected_prefix_names); + static firebase::App* shared_app_; static firebase::auth::Auth* shared_auth_; @@ -212,6 +225,7 @@ void FirebaseStorageTest::TerminateAppAndAuth() { void FirebaseStorageTest::SetUp() { FirebaseTest::SetUp(); InitializeStorage(); + // list_test_root_ removed from SetUp } void FirebaseStorageTest::TearDown() { @@ -313,6 +327,65 @@ void FirebaseStorageTest::SignOut() { EXPECT_FALSE(shared_auth_->current_user().is_valid()); } +void FirebaseStorageTest::UploadStringAsFile( + firebase::storage::StorageReference& ref, const std::string& content, + const char* content_type) { + LogDebug("Uploading string content to: gs://%s%s", ref.bucket().c_str(), + ref.full_path().c_str()); + firebase::storage::Metadata metadata; + if (content_type) { + metadata.set_content_type(content_type); + } + firebase::Future future = + RunWithRetry([&]() { + return ref.PutBytes(content.c_str(), content.length(), metadata); + }); + WaitForCompletion(future, "UploadStringAsFile"); + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << "Failed to upload to " << ref.full_path() << ": " + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + // On some platforms (iOS), size_bytes might not be immediately available or + // might be 0 if the upload was very fast and metadata propagation is slow. + // For small files, this is less critical than the content being there. + // For larger files in other tests, size_bytes is asserted. + // ASSERT_EQ(future.result()->size_bytes(), content.length()); + cleanup_files_.push_back(ref); +} + +void FirebaseStorageTest::VerifyListResultContains( + const firebase::storage::ListResult& list_result, + const std::vector& expected_item_names, + const std::vector& expected_prefix_names) { + ASSERT_TRUE(list_result.is_valid()); + + std::vector actual_item_names; + for (const auto& item_ref : list_result.items()) { + actual_item_names.push_back(item_ref.name()); + } + std::sort(actual_item_names.begin(), actual_item_names.end()); + std::vector sorted_expected_item_names = expected_item_names; + std::sort(sorted_expected_item_names.begin(), + sorted_expected_item_names.end()); + + EXPECT_THAT(actual_item_names, + ::testing::ContainerEq(sorted_expected_item_names)) + << "Item names do not match expected."; + + std::vector actual_prefix_names; + for (const auto& prefix_ref : list_result.prefixes()) { + actual_prefix_names.push_back(prefix_ref.name()); + } + std::sort(actual_prefix_names.begin(), actual_prefix_names.end()); + std::vector sorted_expected_prefix_names = expected_prefix_names; + std::sort(sorted_expected_prefix_names.begin(), + sorted_expected_prefix_names.end()); + + EXPECT_THAT(actual_prefix_names, + ::testing::ContainerEq(sorted_expected_prefix_names)) + << "Prefix names do not match expected."; +} + firebase::storage::StorageReference FirebaseStorageTest::CreateFolder() { // Generate a folder for the test data based on the time in milliseconds. int64_t time_in_microseconds = GetCurrentTimeInMicroseconds(); @@ -1622,4 +1695,213 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) { InitializeAppAndAuth(); } +TEST_F(FirebaseStorageTest, ListAllBasic) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // Removed + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_all_basic_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListAllBasic is not valid."; + + UploadStringAsFile(test_root.Child("file_a.txt"), "content_a"); + UploadStringAsFile(test_root.Child("file_b.txt"), "content_b"); + UploadStringAsFile(test_root.Child("prefix1/file_c.txt"), + "content_c_in_prefix1"); + UploadStringAsFile(test_root.Child("prefix2/file_e.txt"), + "content_e_in_prefix2"); + + LogDebug("Calling ListAll() on gs://%s%s", test_root.bucket().c_str(), + test_root.full_path().c_str()); + firebase::Future future = test_root.ListAll(); + WaitForCompletion(future, "ListAllBasic"); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {"file_a.txt", "file_b.txt"}, + {"prefix1/", "prefix2/"}); + EXPECT_TRUE(result->page_token().empty()) + << "Page token should be empty for ListAll."; +} + +TEST_F(FirebaseStorageTest, ListPaginated) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // Removed + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_paginated_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListPaginated is not valid."; + + // Expected total entries: file_aa.txt, file_bb.txt, file_ee.txt, prefix_x/, + // prefix_y/ (5 entries) + UploadStringAsFile(test_root.Child("file_aa.txt"), "content_aa"); + UploadStringAsFile(test_root.Child("prefix_x/file_cc.txt"), + "content_cc_in_prefix_x"); + UploadStringAsFile(test_root.Child("file_bb.txt"), "content_bb"); + UploadStringAsFile(test_root.Child("prefix_y/file_dd.txt"), + "content_dd_in_prefix_y"); + UploadStringAsFile(test_root.Child("file_ee.txt"), "content_ee"); + + std::vector all_item_names_collected; + std::vector all_prefix_names_collected; + std::string page_token = ""; + const int page_size = 2; + int page_count = 0; + const int max_pages = 5; // Safety break for loop + + LogDebug("Starting paginated List() on gs://%s%s with page_size %d", + test_root.bucket().c_str(), test_root.full_path().c_str(), + page_size); + + do { + page_count++; + LogDebug("Fetching page %d, token: '%s'", page_count, page_token.c_str()); + firebase::Future future = + page_token.empty() ? test_root.List(page_size) + : test_root.List(page_size, page_token.c_str()); + WaitForCompletion(future, + "ListPaginated - Page " + std::to_string(page_count)); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + ASSERT_TRUE(result->is_valid()); + + LogDebug("Page %d items: %zu, prefixes: %zu", page_count, + result->items().size(), result->prefixes().size()); + for (const auto& item : result->items()) { + all_item_names_collected.push_back(item.name()); + LogDebug(" Item: %s", item.name().c_str()); + } + for (const auto& prefix : result->prefixes()) { + all_prefix_names_collected.push_back(prefix.name()); + LogDebug(" Prefix: %s", prefix.name().c_str()); + } + + page_token = result->page_token(); + + size_t entries_on_page = result->items().size() + result->prefixes().size(); + + if (!page_token.empty()) { + EXPECT_EQ(entries_on_page, page_size) + << "A non-last page should have full page_size entries."; + } else { + // This is the last page + size_t total_entries = 5; + size_t expected_entries_on_last_page = total_entries % page_size; + if (expected_entries_on_last_page == 0 && + total_entries > 0) { // if total is a multiple of page_size + expected_entries_on_last_page = page_size; + } + EXPECT_EQ(entries_on_page, expected_entries_on_last_page); + } + } while (!page_token.empty() && page_count < max_pages); + + EXPECT_LT(page_count, max_pages) + << "Exceeded max_pages, possible infinite loop."; + EXPECT_EQ(page_count, (5 + page_size - 1) / page_size) + << "Unexpected number of pages."; + + std::vector expected_final_items = {"file_aa.txt", "file_bb.txt", + "file_ee.txt"}; + std::vector expected_final_prefixes = {"prefix_x/", "prefix_y/"}; + + // VerifyListResultContains needs a ListResult object. We can't directly use + // it with collected names. Instead, we sort and compare the collected names. + std::sort(all_item_names_collected.begin(), all_item_names_collected.end()); + std::sort(all_prefix_names_collected.begin(), + all_prefix_names_collected.end()); + std::sort(expected_final_items.begin(), expected_final_items.end()); + std::sort(expected_final_prefixes.begin(), expected_final_prefixes.end()); + + EXPECT_THAT(all_item_names_collected, + ::testing::ContainerEq(expected_final_items)); + EXPECT_THAT(all_prefix_names_collected, + ::testing::ContainerEq(expected_final_prefixes)); +} + +TEST_F(FirebaseStorageTest, ListEmpty) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed as it's a lightweight + // test. + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_empty_root"); + ASSERT_TRUE(test_root.is_valid()) << "Test root for ListEmpty is not valid."; + + // Do not upload anything to test_root. + + LogDebug("Calling ListAll() on empty folder: gs://%s%s", + test_root.bucket().c_str(), test_root.full_path().c_str()); + firebase::Future future = test_root.ListAll(); + WaitForCompletion(future, "ListEmpty"); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {}, {}); + EXPECT_TRUE(result->page_token().empty()); +} + +TEST_F(FirebaseStorageTest, ListWithMaxResultsGreaterThanActual) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed. + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_max_greater_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListWithMaxResultsGreaterThanActual is not valid."; + + UploadStringAsFile(test_root.Child("only_file.txt"), "content_only"); + UploadStringAsFile(test_root.Child("only_prefix/another.txt"), + "content_another_in_prefix"); + + LogDebug("Calling List(10) on gs://%s%s", test_root.bucket().c_str(), + test_root.full_path().c_str()); + firebase::Future future = + test_root.List(10); // Max results (10) > actual (1 file + 1 prefix = 2) + WaitForCompletion(future, "ListWithMaxResultsGreaterThanActual"); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {"only_file.txt"}, {"only_prefix/"}); + EXPECT_TRUE(result->page_token().empty()); +} + +TEST_F(FirebaseStorageTest, ListNonExistentPath) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed. + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_non_existent_parent_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListNonExistentPath is not valid."; + + firebase::storage::StorageReference non_existent_ref = + test_root.Child("this_folder_truly_does_not_exist"); + // No cleanup needed as nothing is created. + + LogDebug("Calling ListAll() on non-existent path: gs://%s%s", + non_existent_ref.bucket().c_str(), + non_existent_ref.full_path().c_str()); + firebase::Future future = + non_existent_ref.ListAll(); + WaitForCompletion(future, "ListNonExistentPath"); + + // Listing a non-existent path should not be an error, it's just an empty + // list. + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {}, {}); + EXPECT_TRUE(result->page_token().empty()); +} + } // namespace firebase_testapp_automated diff --git a/storage/src/android/list_result_android.cc b/storage/src/android/list_result_android.cc new file mode 100644 index 0000000000..84a6157192 --- /dev/null +++ b/storage/src/android/list_result_android.cc @@ -0,0 +1,243 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/android/list_result_android.h" + +#include "app/src/include/firebase/app.h" +#include "app/src/util_android.h" +#include "storage/src/android/storage_android.h" +#include "storage/src/android/storage_reference_android.h" +#include "storage/src/common/common_android.h" + +namespace firebase { +namespace storage { +namespace internal { + +// clang-format off +#define LIST_RESULT_METHODS(X) \ + X(GetItems, "getItems", "()Ljava/util/List;"), \ + X(GetPrefixes, "getPrefixes", "()Ljava/util/List;"), \ + X(GetPageToken, "getPageToken", "()Ljava/lang/String;") +// clang-format on +METHOD_LOOKUP_DECLARATION(list_result, LIST_RESULT_METHODS) +METHOD_LOOKUP_DEFINITION(list_result, "com/google/firebase/storage/ListResult", + LIST_RESULT_METHODS) + +// clang-format off +#define JAVA_LIST_METHODS(X) \ + X(Size, "size", "()I"), \ + X(Get, "get", "(I)Ljava/lang/Object;") +// clang-format on +METHOD_LOOKUP_DECLARATION(java_list, JAVA_LIST_METHODS) +METHOD_LOOKUP_DEFINITION(java_list, "java/util/List", JAVA_LIST_METHODS) + +bool ListResultInternal::Initialize(App* app) { + JNIEnv* env = app->GetJNIEnv(); + if (!list_result::CacheMethodIds(env, app->activity())) { + return false; + } + if (!java_list::CacheMethodIds(env, app->activity())) { + // Release already cached list_result methods if java_list fails. + list_result::ReleaseClass(env); + return false; + } + return true; +} + +void ListResultInternal::Terminate(App* app) { + JNIEnv* env = app->GetJNIEnv(); + list_result::ReleaseClass(env); + java_list::ReleaseClass(env); +} + +ListResultInternal::ListResultInternal(StorageInternal* storage_internal, + jobject java_list_result) + : storage_internal_(storage_internal), + list_result_java_ref_(nullptr), + items_converted_(false), + prefixes_converted_(false), + page_token_converted_(false) { + FIREBASE_ASSERT(storage_internal != nullptr); + FIREBASE_ASSERT(java_list_result != nullptr); + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + list_result_java_ref_ = env->NewGlobalRef(java_list_result); +} + +ListResultInternal::ListResultInternal(const ListResultInternal& other) + : storage_internal_(other.storage_internal_), + list_result_java_ref_(nullptr), + items_cache_(other.items_cache_), + prefixes_cache_(other.prefixes_cache_), + page_token_cache_(other.page_token_cache_), + items_converted_(other.items_converted_), + prefixes_converted_(other.prefixes_converted_), + page_token_converted_(other.page_token_converted_) { + FIREBASE_ASSERT(storage_internal_ != nullptr); + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + if (other.list_result_java_ref_ != nullptr) { + list_result_java_ref_ = env->NewGlobalRef(other.list_result_java_ref_); + } +} + +ListResultInternal& ListResultInternal::operator=( + const ListResultInternal& other) { + if (&other == this) { + return *this; + } + storage_internal_ = other.storage_internal_; + FIREBASE_ASSERT(storage_internal_ != nullptr); + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + if (list_result_java_ref_ != nullptr) { + env->DeleteGlobalRef(list_result_java_ref_); + list_result_java_ref_ = nullptr; + } + if (other.list_result_java_ref_ != nullptr) { + list_result_java_ref_ = env->NewGlobalRef(other.list_result_java_ref_); + } + items_cache_ = other.items_cache_; + prefixes_cache_ = other.prefixes_cache_; + page_token_cache_ = other.page_token_cache_; + items_converted_ = other.items_converted_; + prefixes_converted_ = other.prefixes_converted_; + page_token_converted_ = other.page_token_converted_; + return *this; +} + +ListResultInternal::~ListResultInternal() { + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + if (list_result_java_ref_ != nullptr) { + env->DeleteGlobalRef(list_result_java_ref_); + list_result_java_ref_ = nullptr; + } +} + +std::vector ListResultInternal::ProcessJavaReferenceList( + jobject java_list_ref) const { + std::vector cpp_references; + if (java_list_ref == nullptr) { + return cpp_references; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jint size = env->CallIntMethod(java_list_ref, + java_list::GetMethodId(java_list::kSize)); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + LogError("Failed to get size of Java List in ListResultInternal"); + return cpp_references; + } + + for (jint i = 0; i < size; ++i) { + jobject java_storage_ref = env->CallObjectMethod( + java_list_ref, java_list::GetMethodId(java_list::kGet), i); + if (env->ExceptionCheck() || java_storage_ref == nullptr) { + env->ExceptionClear(); + LogError( + "Failed to get StorageReference object from Java List at index %d", + i); + if (java_storage_ref) env->DeleteLocalRef(java_storage_ref); + continue; + } + // Create a C++ StorageReferenceInternal from the Java StorageReference. + // StorageReferenceInternal constructor will create a global ref for the + // java obj. + StorageReferenceInternal* sfr_internal = + new StorageReferenceInternal(storage_internal_, java_storage_ref); + cpp_references.push_back(StorageReference(sfr_internal)); + env->DeleteLocalRef(java_storage_ref); + } + return cpp_references; +} + +std::vector ListResultInternal::items() const { + if (!list_result_java_ref_) return items_cache_; // Return empty if no ref + if (items_converted_) { + return items_cache_; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jobject java_items_list = env->CallObjectMethod( + list_result_java_ref_, list_result::GetMethodId(list_result::kGetItems)); + if (env->ExceptionCheck() || java_items_list == nullptr) { + env->ExceptionClear(); + LogError("Failed to call getItems() on Java ListResult"); + if (java_items_list) env->DeleteLocalRef(java_items_list); + // In case of error, still mark as "converted" to avoid retrying JNI call, + // return whatever might be in cache (empty at this point). + items_converted_ = true; + return items_cache_; + } + + items_cache_ = ProcessJavaReferenceList(java_items_list); + env->DeleteLocalRef(java_items_list); + items_converted_ = true; + return items_cache_; +} + +std::vector ListResultInternal::prefixes() const { + if (!list_result_java_ref_) return prefixes_cache_; + if (prefixes_converted_) { + return prefixes_cache_; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jobject java_prefixes_list = env->CallObjectMethod( + list_result_java_ref_, + list_result::GetMethodId(list_result::kGetPrefixes)); + if (env->ExceptionCheck() || java_prefixes_list == nullptr) { + env->ExceptionClear(); + LogError("Failed to call getPrefixes() on Java ListResult"); + if (java_prefixes_list) env->DeleteLocalRef(java_prefixes_list); + prefixes_converted_ = true; + return prefixes_cache_; + } + + prefixes_cache_ = ProcessJavaReferenceList(java_prefixes_list); + env->DeleteLocalRef(java_prefixes_list); + prefixes_converted_ = true; + return prefixes_cache_; +} + +std::string ListResultInternal::page_token() const { + if (!list_result_java_ref_) return page_token_cache_; + if (page_token_converted_) { + return page_token_cache_; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jstring page_token_jstring = static_cast(env->CallObjectMethod( + list_result_java_ref_, + list_result::GetMethodId(list_result::kGetPageToken))); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + LogError("Failed to call getPageToken() on Java ListResult"); + if (page_token_jstring) env->DeleteLocalRef(page_token_jstring); + page_token_converted_ = true; + return page_token_cache_; // Return empty if error + } + + if (page_token_jstring != nullptr) { + page_token_cache_ = util::JniStringToString(env, page_token_jstring); + env->DeleteLocalRef(page_token_jstring); + } else { + page_token_cache_ = ""; // Explicitly set to empty if Java string is null + } + + page_token_converted_ = true; + return page_token_cache_; +} + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/src/android/list_result_android.h b/storage/src/android/list_result_android.h new file mode 100644 index 0000000000..7b9e2c8327 --- /dev/null +++ b/storage/src/android/list_result_android.h @@ -0,0 +1,100 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_ANDROID_LIST_RESULT_ANDROID_H_ +#define FIREBASE_STORAGE_SRC_ANDROID_LIST_RESULT_ANDROID_H_ + +#include + +#include +#include + +#include "app/src/util_android.h" +#include "firebase/app.h" +#include "firebase/storage/storage_reference.h" +#include "storage/src/android/storage_android.h" + +namespace firebase { +namespace storage { + +// Forward declaration for platform-specific ListResultInternal. +class ListResult; + +namespace internal { + +// Contains the Android-specific implementation of ListResultInternal. +class ListResultInternal { + public: + // Constructor. + // + // @param[in] storage_internal Pointer to the StorageInternal object. + // @param[in] java_list_result Java ListResult object. This function will + // retain a global reference to this object. + ListResultInternal(StorageInternal* storage_internal, + jobject java_list_result); + + // Copy constructor. + ListResultInternal(const ListResultInternal& other); + + // Copy assignment operator. + ListResultInternal& operator=(const ListResultInternal& other); + + // Destructor. + ~ListResultInternal(); + + // Gets the items (files) in this result. + std::vector items() const; + + // Gets the prefixes (folders) in this result. + std::vector prefixes() const; + + // Gets the page token for the next page of results. + // Returns an empty string if there are no more results. + std::string page_token() const; + + // Returns the StorageInternal object associated with this ListResult. + StorageInternal* storage_internal() const { return storage_internal_; } + + // Initializes ListResultInternal JNI. + static bool Initialize(App* app); + + // Terminates ListResultInternal JNI. + static void Terminate(App* app); + + private: + friend class firebase::storage::ListResult; + + // Converts a Java List of Java StorageReference objects to a C++ vector of + // C++ StorageReference objects. + std::vector ProcessJavaReferenceList( + jobject java_list_ref) const; + + StorageInternal* storage_internal_; // Not owned. + // Global reference to Java com.google.firebase.storage.ListResult object. + jobject list_result_java_ref_; + + // Caches for converted data + mutable std::vector items_cache_; + mutable std::vector prefixes_cache_; + mutable std::string page_token_cache_; + mutable bool items_converted_; + mutable bool prefixes_converted_; + mutable bool page_token_converted_; +}; + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_ANDROID_LIST_RESULT_ANDROID_H_ diff --git a/storage/src/android/storage_reference_android.cc b/storage/src/android/storage_reference_android.cc index 99b9d40280..ef1c7d155a 100644 --- a/storage/src/android/storage_reference_android.cc +++ b/storage/src/android/storage_reference_android.cc @@ -50,6 +50,12 @@ namespace internal { "()Ljava/lang/String;"), \ X(GetStorage, "getStorage", \ "()Lcom/google/firebase/storage/FirebaseStorage;"), \ + X(List, "list", \ + "(I)Lcom/google/android/gms/tasks/Task;"), \ + X(ListWithPageToken, "list", \ + "(ILjava/lang/String;)Lcom/google/android/gms/tasks/Task;"), \ + X(ListAll, "listAll", \ + "()Lcom/google/android/gms/tasks/Task;"), \ X(PutStream, "putStream", \ "(Ljava/io/InputStream;)Lcom/google/firebase/storage/UploadTask;"), \ X(PutStreamWithMetadata, "putStream", \ @@ -105,17 +111,26 @@ enum StorageReferenceFn { kStorageReferenceFnUpdateMetadata, kStorageReferenceFnPutBytes, kStorageReferenceFnPutFile, + kStorageReferenceFnList, kStorageReferenceFnCount, }; bool StorageReferenceInternal::Initialize(App* app) { JNIEnv* env = app->GetJNIEnv(); jobject activity = app->activity(); - return storage_reference::CacheMethodIds(env, activity); + if (!storage_reference::CacheMethodIds(env, activity)) { + return false; + } + if (!ListResultInternal::Initialize(app)) { + storage_reference::ReleaseClass(env); + return false; + } + return true; } void StorageReferenceInternal::Terminate(App* app) { JNIEnv* env = app->GetJNIEnv(); + ListResultInternal::Terminate(app); storage_reference::ReleaseClass(env); util::CheckAndClearJniExceptions(env); } @@ -309,11 +324,37 @@ void StorageReferenceInternal::FutureCallback(JNIEnv* env, jobject result, file_download_task_task_snapshot::kGetBytesTransferred)); data->impl->Complete(data->handle, kErrorNone, status_message, [bytes](size_t* size) { *size = bytes; }); + } else if (result && env->IsInstanceOf(result, list_result::GetClass())) { + // Complete a Future from a Java ListResult object. + LogDebug("FutureCallback: Completing a Future from a ListResult."); + // Create a local reference for the ListResultInternal constructor + jobject result_ref = env->NewLocalRef(result); + ListResultInternal* list_result_internal_ptr = + new ListResultInternal(data->storage, result_ref); + env->DeleteLocalRef(result_ref); // ListResultInternal made a global ref. + + data->impl->Complete( + data->handle, kErrorNone, status_message, + [list_result_internal_ptr](ListResult* out_list_result) { + *out_list_result = ListResult(list_result_internal_ptr); + }); } else { LogDebug("FutureCallback: Completing a Future from a default result."); // Unknown or null result type, treat this as a Future and just // return success. - data->impl->Complete(data->handle, kErrorNone, status_message); + // This case might need adjustment if List operations that fail end up here + // without a specific exception being caught by result_code check. + if (data->func == kStorageReferenceFnList) { + // If it was a list operation but didn't result in a ListResult object + // (e.g. error not caught as exception) complete with an error and an + // invalid ListResult. + data->impl->CompleteWithResult( + data->handle, kErrorUnknown, + "List operation failed to produce a valid ListResult.", + ListResult(nullptr)); + } else { + data->impl->Complete(data->handle, kErrorNone, status_message); + } } if (data->listener != nullptr) { env->CallVoidMethod(data->listener, @@ -687,6 +728,76 @@ Future StorageReferenceInternal::PutFileLastResult() { future()->LastResult(kStorageReferenceFnPutFile)); } +Future StorageReferenceInternal::List(int32_t max_results) { + JNIEnv* env = storage_->app()->GetJNIEnv(); + ReferenceCountedFutureImpl* future_impl = future(); + FutureHandle handle = future_impl->Alloc(kStorageReferenceFnList); + + jobject task = env->CallObjectMethod( + obj_, storage_reference::GetMethodId(storage_reference::kList), + static_cast(max_results)); + + util::RegisterCallbackOnTask( + env, task, FutureCallback, + new FutureCallbackData(handle, future_impl, storage_, + kStorageReferenceFnList), + storage_->jni_task_id()); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(task); + return ListLastResult(); +} + +Future StorageReferenceInternal::List(int32_t max_results, + const char* page_token) { + JNIEnv* env = storage_->app()->GetJNIEnv(); + ReferenceCountedFutureImpl* future_impl = future(); + FutureHandle handle = future_impl->Alloc(kStorageReferenceFnList); + + jstring page_token_jstring = + page_token ? env->NewStringUTF(page_token) : nullptr; + + jobject task = env->CallObjectMethod( + obj_, + storage_reference::GetMethodId(storage_reference::kListWithPageToken), + static_cast(max_results), page_token_jstring); + + if (page_token_jstring) { + env->DeleteLocalRef(page_token_jstring); + } + + util::RegisterCallbackOnTask( + env, task, FutureCallback, + new FutureCallbackData(handle, future_impl, storage_, + kStorageReferenceFnList), + storage_->jni_task_id()); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(task); + return ListLastResult(); +} + +Future StorageReferenceInternal::ListAll() { + JNIEnv* env = storage_->app()->GetJNIEnv(); + ReferenceCountedFutureImpl* future_impl = future(); + FutureHandle handle = future_impl->Alloc(kStorageReferenceFnList); + + jobject task = env->CallObjectMethod( + obj_, storage_reference::GetMethodId(storage_reference::kListAll)); + + util::RegisterCallbackOnTask( + env, task, FutureCallback, + new FutureCallbackData(handle, future_impl, storage_, + kStorageReferenceFnList), + storage_->jni_task_id()); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(task); + return ListLastResult(); +} + +Future StorageReferenceInternal::ListLastResult() { + return static_cast&>( + future()->LastResult(kStorageReferenceFnList)); +} + ReferenceCountedFutureImpl* StorageReferenceInternal::future() { return storage_->future_manager().GetFutureApi(this); } diff --git a/storage/src/android/storage_reference_android.h b/storage/src/android/storage_reference_android.h index 6643a4d8bd..8eae3d2287 100644 --- a/storage/src/android/storage_reference_android.h +++ b/storage/src/android/storage_reference_android.h @@ -129,6 +129,16 @@ class StorageReferenceInternal { // Returns the result of the most recent call to PutFile(); Future PutFileLastResult(); + // Asynchronously lists objects and common prefixes under this reference. + Future List(int32_t max_results); + Future List(int32_t max_results, const char* page_token); + + // Asynchronously lists all objects and common prefixes under this reference. + Future ListAll(); + + // Returns the result of the most recent List operation. + Future ListLastResult(); + // Initialize JNI bindings for this class. static bool Initialize(App* app); static void Terminate(App* app); diff --git a/storage/src/common/list_result.cc b/storage/src/common/list_result.cc new file mode 100644 index 0000000000..a4ed4a36e2 --- /dev/null +++ b/storage/src/common/list_result.cc @@ -0,0 +1,198 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/include/firebase/storage/list_result.h" + +#include + +#include "app/src/cleanup_notifier.h" +#include "app/src/include/firebase/app.h" +#include "app/src/util.h" +#include "storage/src/common/storage_reference_internal.h" + +// Platform specific ListResultInternal definitions +#if FIREBASE_PLATFORM_ANDROID +#include "storage/src/android/list_result_android.h" +#include "storage/src/android/storage_android.h" +#elif FIREBASE_PLATFORM_IOS +#include "storage/src/ios/list_result_ios.h" +#include "storage/src/ios/storage_ios.h" +#elif FIREBASE_PLATFORM_DESKTOP +#include "storage/src/desktop/list_result_desktop.h" +#include "storage/src/desktop/storage_desktop.h" +#endif // FIREBASE_PLATFORM_ANDROID, FIREBASE_PLATFORM_IOS, + // FIREBASE_PLATFORM_DESKTOP + +namespace firebase { +namespace storage { + +using internal::ListResultInternal; + +// Global function to be called by CleanupNotifier +// This function is responsible for cleaning up the internal state of a +// ListResult object when the App is being shut down. +static void GlobalCleanupListResult(void* list_result_void) { + if (list_result_void) { + ListResult* list_result = static_cast(list_result_void); + // This method will delete internal_ and set it to nullptr. + list_result->ClearInternalForCleanup(); + } +} + +ListResult::ListResult() : internal_(nullptr) {} + +ListResult::ListResult(internal::ListResultInternal* internal) + : internal_(internal) { + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } +} + +ListResult::ListResult(const ListResult& other) : internal_(nullptr) { + if (other.internal_) { + internal_ = new internal::ListResultInternal(*other.internal_); + } + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } +} + +ListResult& ListResult::operator=(const ListResult& other) { + if (this == &other) { + return *this; + } + + // Unregister and delete current internal object + if (internal_) { + if (internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().UnregisterObject(this); + } + delete internal_; + internal_ = nullptr; + } + + // Copy from other + if (other.internal_) { + internal_ = new internal::ListResultInternal(*other.internal_); + } + + // Register new internal object + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } + return *this; +} + +ListResult::ListResult(ListResult&& other) : internal_(nullptr) { + // Unregister 'other' as it will no longer manage its internal_ + if (other.internal_ && other.internal_->storage_internal() && + other.internal_->storage_internal()->app_valid()) { + other.internal_->storage_internal()->cleanup().UnregisterObject(&other); + } + + // Move internal pointer + internal_ = other.internal_; + other.internal_ = nullptr; + + // Register 'this' if it now owns a valid internal object + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } +} + +ListResult& ListResult::operator=(ListResult&& other) { + if (this == &other) { + return *this; + } + + // Unregister and delete current internal object for 'this' + if (internal_) { + if (internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().UnregisterObject(this); + } + delete internal_; + internal_ = nullptr; + } + + // Unregister 'other' as it will no longer manage its internal_ + if (other.internal_ && other.internal_->storage_internal() && + other.internal_->storage_internal()->app_valid()) { + other.internal_->storage_internal()->cleanup().UnregisterObject(&other); + } + + // Move internal pointer + internal_ = other.internal_; + other.internal_ = nullptr; + + // Register 'this' if it now owns a valid internal object + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } + return *this; +} + +ListResult::~ListResult() { + if (internal_) { + if (internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().UnregisterObject(this); + } + delete internal_; + internal_ = nullptr; + } +} + +void ListResult::ClearInternalForCleanup() { + // This method is called by GlobalCleanupListResult. + // The object is already unregistered from the CleanupNotifier by the notifier + // itself before this callback is invoked. So, no need to call + // UnregisterObject here. + delete internal_; + internal_ = nullptr; +} + +std::vector ListResult::items() const { + assert(internal_ != nullptr); + if (!internal_) return std::vector(); + return internal_->items(); +} + +std::vector ListResult::prefixes() const { + assert(internal_ != nullptr); + if (!internal_) return std::vector(); + return internal_->prefixes(); +} + +std::string ListResult::page_token() const { + assert(internal_ != nullptr); + if (!internal_) return ""; + return internal_->page_token(); +} + +bool ListResult::is_valid() const { return internal_ != nullptr; } + +} // namespace storage +} // namespace firebase diff --git a/storage/src/common/storage_reference.cc b/storage/src/common/storage_reference.cc index 54bc6983b7..a7e6ad3f9f 100644 --- a/storage/src/common/storage_reference.cc +++ b/storage/src/common/storage_reference.cc @@ -15,6 +15,8 @@ #include "storage/src/include/firebase/storage/storage_reference.h" #include "app/src/assert.h" +#include "firebase/storage/list_result.h" +#include "storage/src/include/firebase/storage/future_details.h" #ifdef __APPLE__ #include "TargetConditionals.h" @@ -248,5 +250,29 @@ Future StorageReference::PutFileLastResult() { bool StorageReference::is_valid() const { return internal_ != nullptr; } +Future StorageReference::List(int32_t max_results) { + if (!internal_) return Future(); + return internal_->List(max_results); +} + +Future StorageReference::List(int32_t max_results, + const char* page_token) { + if (!internal_) return Future(); + // Pass an empty string if page_token is nullptr, as internal methods + // might expect a non-null, though possibly empty, string. + return internal_->List(max_results, page_token ? page_token : ""); +} + +Future StorageReference::ListAll() { + if (!internal_) return Future(); + return internal_->ListAll(); +} + +Future StorageReference::ListLastResult() { + if (!internal_) return Future(); + return static_cast&>( + internal_->future()->LastResult(kStorageReferenceFnList)); +} + } // namespace storage } // namespace firebase diff --git a/storage/src/desktop/list_result_desktop.cc b/storage/src/desktop/list_result_desktop.cc new file mode 100644 index 0000000000..44014b2c78 --- /dev/null +++ b/storage/src/desktop/list_result_desktop.cc @@ -0,0 +1,58 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/desktop/list_result_desktop.h" + +#include "storage/src/desktop/storage_desktop.h" + +namespace firebase { +namespace storage { +namespace internal { + +ListResultInternal::ListResultInternal(StorageInternal* storage_internal) + : storage_internal_(storage_internal) {} + +ListResultInternal::ListResultInternal( + StorageInternal* storage_internal, + const std::vector& items, + const std::vector& prefixes, + const std::string& page_token) + : storage_internal_(storage_internal), + items_stub_(items), + prefixes_stub_(prefixes), + page_token_stub_(page_token) {} + +ListResultInternal::ListResultInternal(const ListResultInternal& other) + : storage_internal_(other.storage_internal_), + items_stub_(other.items_stub_), + prefixes_stub_(other.prefixes_stub_), + page_token_stub_(other.page_token_stub_) {} + +ListResultInternal& ListResultInternal::operator=( + const ListResultInternal& other) { + if (&other == this) { + return *this; + } + storage_internal_ = other.storage_internal_; + items_stub_ = other.items_stub_; + prefixes_stub_ = other.prefixes_stub_; + page_token_stub_ = other.page_token_stub_; + return *this; +} + +// Methods are already stubbed inline in the header. + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/src/desktop/list_result_desktop.h b/storage/src/desktop/list_result_desktop.h new file mode 100644 index 0000000000..56f53e4460 --- /dev/null +++ b/storage/src/desktop/list_result_desktop.h @@ -0,0 +1,83 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_DESKTOP_LIST_RESULT_DESKTOP_H_ +#define FIREBASE_STORAGE_SRC_DESKTOP_LIST_RESULT_DESKTOP_H_ + +#include +#include + +#include "firebase/storage/storage_reference.h" +#include "storage/src/desktop/storage_desktop.h" + +namespace firebase { +namespace storage { + +// Forward declaration for platform-specific ListResultInternal. +class ListResult; + +namespace internal { + +// Contains the Desktop-specific implementation of ListResultInternal (stubs). +class ListResultInternal { + public: + // Constructor. + explicit ListResultInternal(StorageInternal* storage_internal); + + // Constructor that can take pre-populated data (though stubs won't use it). + ListResultInternal(StorageInternal* storage_internal, + const std::vector& items, + const std::vector& prefixes, + const std::string& page_token); + + // Destructor (default is fine). + ~ListResultInternal() = default; + + // Copy constructor. + ListResultInternal(const ListResultInternal& other); + + // Copy assignment operator. + ListResultInternal& operator=(const ListResultInternal& other); + + // Gets the items (files) in this result (stub). + std::vector items() const { + return std::vector(); + } + + // Gets the prefixes (folders) in this result (stub). + std::vector prefixes() const { + return std::vector(); + } + + // Gets the page token for the next page of results (stub). + std::string page_token() const { return ""; } + + // Returns the StorageInternal object associated with this ListResult. + StorageInternal* storage_internal() const { return storage_internal_; } + + private: + friend class firebase::storage::ListResult; + + StorageInternal* storage_internal_; // Not owned. + // Desktop stubs don't actually store these, but defined to match constructor. + std::vector items_stub_; + std::vector prefixes_stub_; + std::string page_token_stub_; +}; + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_DESKTOP_LIST_RESULT_DESKTOP_H_ diff --git a/storage/src/desktop/storage_reference_desktop.cc b/storage/src/desktop/storage_reference_desktop.cc index aa99863e3b..f7a97857e7 100644 --- a/storage/src/desktop/storage_reference_desktop.cc +++ b/storage/src/desktop/storage_reference_desktop.cc @@ -28,8 +28,10 @@ #include "app/src/function_registry.h" #include "app/src/include/firebase/app.h" #include "app/src/thread.h" +#include "firebase/storage/list_result.h" #include "storage/src/common/common_internal.h" #include "storage/src/desktop/controller_desktop.h" +#include "storage/src/desktop/list_result_desktop.h" #include "storage/src/desktop/metadata_desktop.h" #include "storage/src/desktop/storage_desktop.h" #include "storage/src/include/firebase/storage.h" @@ -698,6 +700,42 @@ ReferenceCountedFutureImpl* StorageReferenceInternal::future() { return storage_->future_manager().GetFutureApi(this); } +Future StorageReferenceInternal::List(int32_t max_results) { + ReferenceCountedFutureImpl* future_api = future(); + SafeFutureHandle handle = + future_api->SafeAlloc(kStorageReferenceFnList); + future_api->CompleteWithResult(handle, kErrorUnimplemented, + "List operation is not supported on desktop.", + ListResult(nullptr)); + return ListLastResult(); +} + +Future StorageReferenceInternal::List(int32_t max_results, + const char* page_token) { + ReferenceCountedFutureImpl* future_api = future(); + SafeFutureHandle handle = + future_api->SafeAlloc(kStorageReferenceFnList); + future_api->CompleteWithResult(handle, kErrorUnimplemented, + "List operation is not supported on desktop.", + ListResult(nullptr)); + return ListLastResult(); +} + +Future StorageReferenceInternal::ListAll() { + ReferenceCountedFutureImpl* future_api = future(); + SafeFutureHandle handle = + future_api->SafeAlloc(kStorageReferenceFnList); + future_api->CompleteWithResult( + handle, kErrorUnimplemented, + "ListAll operation is not supported on desktop.", ListResult(nullptr)); + return ListLastResult(); +} + +Future StorageReferenceInternal::ListLastResult() { + return static_cast&>( + future()->LastResult(kStorageReferenceFnList)); +} + } // namespace internal } // namespace storage } // namespace firebase diff --git a/storage/src/desktop/storage_reference_desktop.h b/storage/src/desktop/storage_reference_desktop.h index bbda95d342..4cb9545d59 100644 --- a/storage/src/desktop/storage_reference_desktop.h +++ b/storage/src/desktop/storage_reference_desktop.h @@ -44,6 +44,7 @@ enum StorageReferenceFn { kStorageReferenceFnPutBytesInternal, kStorageReferenceFnPutFile, kStorageReferenceFnPutFileInternal, + kStorageReferenceFnList, // Added for List operations kStorageReferenceFnCount, }; @@ -145,6 +146,18 @@ class StorageReferenceInternal { // Returns the result of the most recent call to Write(); Future PutFileLastResult(); + // Asynchronously lists objects and common prefixes under this reference + // (stub). + Future List(int32_t max_results); + Future List(int32_t max_results, const char* page_token); + + // Asynchronously lists all objects and common prefixes under this reference + // (stub). + Future ListAll(); + + // Returns the result of the most recent List operation (stub). + Future ListLastResult(); + // Pointer to the StorageInternal instance we are a part of. StorageInternal* storage_internal() const { return storage_; } diff --git a/storage/src/include/firebase/storage/list_result.h b/storage/src/include/firebase/storage/list_result.h new file mode 100644 index 0000000000..e635a15993 --- /dev/null +++ b/storage/src/include/firebase/storage/list_result.h @@ -0,0 +1,83 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_LIST_RESULT_H_ +#define FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_LIST_RESULT_H_ + +#include +#include + +#include "firebase/storage/storage_reference.h" + +namespace firebase { +namespace storage { + +// Forward declaration for internal class. +namespace internal { +class ListResultInternal; +} // namespace internal + +/// @brief ListResult contains a list of items and prefixes from a list() +/// call. +/// +/// This is a result from a list() call on a StorageReference. +class ListResult { + public: + /// @brief Creates an invalid ListResult. + ListResult(); + + /// @brief Creates a ListResult from an internal ListResult object. + /// + /// This constructor is not intended for public use. + explicit ListResult(internal::ListResultInternal* internal); + + /// @brief Copy constructor. + ListResult(const ListResult& other); + + /// @brief Copy assignment operator. + ListResult& operator=(const ListResult& other); + + /// @brief Move constructor. + ListResult(ListResult&& other); + + /// @brief Move assignment operator. + ListResult& operator=(ListResult&& other); + + /// @brief Destructor. + ~ListResult(); + + /// @brief Returns the items (files) in this result. + std::vector items() const; + + /// @brief Returns the prefixes (folders) in this result. + std::vector prefixes() const; + + /// @brief If set, there are more results to retrieve. + /// + /// Pass this token to list() to retrieve the next page of results. + std::string page_token() const; + + /// @brief Returns true if this ListResult is valid, false if it is not + /// valid. An invalid ListResult indicates that the operation that was + /// to create this ListResult failed. + bool is_valid() const; + + private: + internal::ListResultInternal* internal_; +}; + +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_LIST_RESULT_H_ diff --git a/storage/src/include/firebase/storage/storage_reference.h b/storage/src/include/firebase/storage/storage_reference.h index e5c7c2f85a..81f4e8898b 100644 --- a/storage/src/include/firebase/storage/storage_reference.h +++ b/storage/src/include/firebase/storage/storage_reference.h @@ -25,9 +25,11 @@ namespace firebase { namespace storage { +// Forward declarations class Controller; class Listener; class Storage; +class ListResult; /// @cond FIREBASE_APP_INTERNAL namespace internal { @@ -339,6 +341,60 @@ class StorageReference { /// StorageReference is invalid. bool is_valid() const; + /// @brief Asynchronously lists objects and common prefixes under this + /// StorageReference. + /// + /// This method allows you to list objects and common prefixes (virtual + /// subdirectories) directly under this StorageReference. + /// + /// @param[in] max_results The maximum number of items and prefixes to return + /// in a single page. Must be greater than 0 and at most 1000. + /// + /// @return A Future that will eventually contain a ListResult. + /// If the operation is successful, the ListResult will contain the first + /// page of items and prefixes, and potentially a page_token to retrieve + /// subsequent pages. + Future List(int32_t max_results); + + /// @brief Asynchronously lists objects and common prefixes under this + /// StorageReference. + /// + /// This method allows you to list objects and common prefixes (virtual + /// subdirectories) directly under this StorageReference. + /// + /// @param[in] max_results The maximum number of items and prefixes to return + /// in a single page. Must be greater than 0 and at most 1000. + /// @param[in] page_token A page token, returned from a previous call to + /// List, to retrieve the next page of results. If nullptr or an empty + /// string, retrieves the first page. + /// + /// @return A Future that will eventually contain a ListResult. + /// If the operation is successful, the ListResult will contain the + /// requested page of items and prefixes, and potentially a page_token + /// to retrieve subsequent pages. + Future List(int32_t max_results, const char* page_token); + + /// @brief Asynchronously lists all objects and common prefixes under this + /// StorageReference. + /// + /// This method will list all items and prefixes under the current reference + /// by making multiple calls to the backend service if necessary, until all + /// results have been fetched. + /// + /// @note This can be a long-running and memory-intensive operation if there + /// are many objects under the reference. Consider using the paginated + /// List() method for very large directories. + /// + /// @return A Future that will eventually contain a ListResult. + /// If the operation is successful, the ListResult will contain all items + /// and prefixes. The page_token in the result will be empty. + Future ListAll(); + + /// @brief Returns the result of the most recent call to List() or ListAll(). + /// + /// @return The result of the most recent call to List() or ListAll(). + Future ListLastResult(); + private: /// @cond FIREBASE_APP_INTERNAL friend class Controller; diff --git a/storage/src/ios/fir_storage_list_result_pointer.h b/storage/src/ios/fir_storage_list_result_pointer.h new file mode 100644 index 0000000000..03e6dfeb01 --- /dev/null +++ b/storage/src/ios/fir_storage_list_result_pointer.h @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_IOS_FIR_STORAGE_LIST_RESULT_POINTER_H_ +#define FIREBASE_STORAGE_SRC_IOS_FIR_STORAGE_LIST_RESULT_POINTER_H_ + +#include "app/src/ios/pointer_ios.h" + +// Forward declare Obj-C types +#ifdef __OBJC__ +@class FIRStorageListResult; +#else +typedef struct objc_object FIRStorageListResult; +#endif + +namespace firebase { +namespace storage { +namespace internal { + +// Define FIRStorageListResultPointer. This is an iOS specific implementation +// detail that is not exposed in the public API. +FIREBASE_DEFINE_POINTER_WRAPPER(FIRStorageListResultPointer, + FIRStorageListResult); + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_IOS_FIR_STORAGE_LIST_RESULT_POINTER_H_ diff --git a/storage/src/ios/list_result_ios.h b/storage/src/ios/list_result_ios.h new file mode 100644 index 0000000000..8bee94b523 --- /dev/null +++ b/storage/src/ios/list_result_ios.h @@ -0,0 +1,94 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_IOS_LIST_RESULT_IOS_H_ +#define FIREBASE_STORAGE_SRC_IOS_LIST_RESULT_IOS_H_ + +#include +#include +#include + +#include "firebase/storage/storage_reference.h" +#include "storage/src/ios/storage_ios.h" +#include "storage/src/ios/fir_storage_list_result_pointer.h" + +// Forward declare Obj-C types +#ifdef __OBJC__ +@class FIRStorageListResult; +@class FIRStorageReference; +#else +typedef struct objc_object FIRStorageListResult; +typedef struct objc_object FIRStorageReference; +#endif + + +namespace firebase { +namespace storage { + +// Forward declaration for platform-specific ListResultInternal. +class ListResult; + +namespace internal { + +// Contains the iOS-specific implementation of ListResultInternal. +class ListResultInternal { + public: + // Constructor. + // Takes ownership of the impl unique_ptr. + ListResultInternal(StorageInternal* storage_internal, + std::unique_ptr impl); + + // Copy constructor. + ListResultInternal(const ListResultInternal& other); + + // Copy assignment operator. + ListResultInternal& operator=(const ListResultInternal& other); + + // Destructor (default is fine thanks to unique_ptr). + ~ListResultInternal() = default; + + // Gets the items (files) in this result. + std::vector items() const; + + // Gets the prefixes (folders) in this result. + std::vector prefixes() const; + + // Gets the page token for the next page of results. + // Returns an empty string if there are no more results. + std::string page_token() const; + + // Returns the underlying Objective-C FIRStorageListResult object. + FIRStorageListResult* impl() const { return impl_->get(); } + + // Returns the StorageInternal object associated with this ListResult. + StorageInternal* storage_internal() const { return storage_internal_; } + + private: + friend class firebase::storage::ListResult; + + // Converts an NSArray of FIRStorageReference objects to a C++ vector of + // C++ StorageReference objects. + std::vector ProcessObjectiveCReferenceArray( + NSArray* ns_array_ref) const; + + StorageInternal* storage_internal_; // Not owned. + // Pointer to the Objective-C FIRStorageListResult instance. + std::unique_ptr impl_; +}; + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_IOS_LIST_RESULT_IOS_H_ diff --git a/storage/src/ios/list_result_ios.mm b/storage/src/ios/list_result_ios.mm new file mode 100644 index 0000000000..a4fdb73b8e --- /dev/null +++ b/storage/src/ios/list_result_ios.mm @@ -0,0 +1,123 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/ios/list_result_ios.h" + +#import +#import +#import + +#include "app/src/assert.h" +#include "app/src/ios/c_string_manager.h" +#include "storage/src/ios/converter_ios.h" +#include "storage/src/ios/storage_ios.h" +#include "storage/src/ios/storage_reference_ios.h" + +namespace firebase { +namespace storage { +namespace internal { + +ListResultInternal::ListResultInternal( + StorageInternal* storage_internal, + std::unique_ptr impl) + : storage_internal_(storage_internal), impl_(std::move(impl)) { + FIREBASE_ASSERT(storage_internal_ != nullptr); + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); +} + +ListResultInternal::ListResultInternal(const ListResultInternal& other) + : storage_internal_(other.storage_internal_), impl_(nullptr) { + FIREBASE_ASSERT(storage_internal_ != nullptr); + if (other.impl_ && other.impl_->get()) { + // FIRStorageListResult does not conform to NSCopying. + // To "copy" it, we'd typically re-fetch or if it's guaranteed immutable, + // we could retain the original. However, unique_ptr implies single ownership. + // For now, this copy constructor will create a ListResultInternal that + // shares the *same* underlying Objective-C object by retaining it and + // creating a new FIRStorageListResultPointer. + // This is generally not safe if the object is mutable or if true deep copy semantics + // are expected by the C++ ListResult's copy constructor. + // Given ListResult is usually a snapshot, sharing might be acceptable. + // TODO(b/180010117): Clarify copy semantics for ListResultInternal on iOS. + // A truly safe copy would involve creating a new FIRStorageListResult with the same contents. + // For now, we are making the unique_ptr point to the same ObjC object. + // This is done by getting the raw pointer, creating a new unique_ptr that points to it, + // and relying on FIRStorageListResultPointer's constructor to retain it. + // This breaks unique_ptr's unique ownership if the original unique_ptr still exists and manages it. + // A better approach for copy would be to create a new FIRStorageListResult with the same properties. + // As a placeholder, we will make a "copy" that points to the same Obj-C object, + // which is what FIRStorageListResultPointer(other.impl_->get()) would do. + impl_ = std::make_unique(other.impl_->get()); + } +} + +ListResultInternal& ListResultInternal::operator=( + const ListResultInternal& other) { + if (&other == this) { + return *this; + } + storage_internal_ = other.storage_internal_; + FIREBASE_ASSERT(storage_internal_ != nullptr); + if (other.impl_ && other.impl_->get()) { + // See notes in copy constructor regarding shared ownership. + impl_ = std::make_unique(other.impl_->get()); + } else { + impl_.reset(); + } + return *this; +} + +std::vector +ListResultInternal::ProcessObjectiveCReferenceArray( + NSArray* ns_array_ref) const { + std::vector cpp_references; + if (ns_array_ref == nil) { + return cpp_references; + } + for (FIRStorageReference* objc_ref in ns_array_ref) { + FIREBASE_ASSERT(objc_ref != nil); + // The StorageReferenceInternal constructor takes ownership of the pointer if unique_ptr is used directly. + // Here, FIRStorageReferencePointer constructor will retain the objc_ref. + auto sfr_internal = new StorageReferenceInternal( + storage_internal_, + std::make_unique(objc_ref)); + cpp_references.push_back(StorageReference(sfr_internal)); + } + return cpp_references; +} + +std::vector ListResultInternal::items() const { + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); + FIRStorageListResult* list_result_objc = impl_->get(); + return ProcessObjectiveCReferenceArray(list_result_objc.items); +} + +std::vector ListResultInternal::prefixes() const { + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); + FIRStorageListResult* list_result_objc = impl_->get(); + return ProcessObjectiveCReferenceArray(list_result_objc.prefixes); +} + +std::string ListResultInternal::page_token() const { + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); + FIRStorageListResult* list_result_objc = impl_->get(); + if (list_result_objc.pageToken == nil) { + return ""; + } + return NSStringToStdString(list_result_objc.pageToken); +} + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/src/ios/storage_reference_ios.h b/storage/src/ios/storage_reference_ios.h index c8b39cd54c..eea4abbe3d 100644 --- a/storage/src/ios/storage_reference_ios.h +++ b/storage/src/ios/storage_reference_ios.h @@ -160,6 +160,16 @@ class StorageReferenceInternal { // Returns the result of the most recent call to PutFile(); Future PutFileLastResult(); + // Asynchronously lists objects and common prefixes under this reference. + Future List(int32_t max_results); + Future List(int32_t max_results, const char* _Nullable page_token); + + // Asynchronously lists all objects and common prefixes under this reference. + Future ListAll(); + + // Returns the result of the most recent List operation. + Future ListLastResult(); + // StorageInternal instance we are associated with. StorageInternal* _Nullable storage_internal() const { return storage_; } diff --git a/storage/src/ios/storage_reference_ios.mm b/storage/src/ios/storage_reference_ios.mm index d2260e3416..9571132050 100644 --- a/storage/src/ios/storage_reference_ios.mm +++ b/storage/src/ios/storage_reference_ios.mm @@ -32,7 +32,6 @@ namespace storage { namespace internal { -// Should reads and writes share thier futures? enum StorageReferenceFn { kStorageReferenceFnDelete = 0, kStorageReferenceFnGetBytes, @@ -42,6 +41,7 @@ kStorageReferenceFnUpdateMetadata, kStorageReferenceFnPutBytes, kStorageReferenceFnPutFile, + kStorageReferenceFnList, kStorageReferenceFnCount, }; @@ -438,6 +438,93 @@ return static_cast&>(future()->LastResult(kStorageReferenceFnPutFile)); } +Future StorageReferenceInternal::List(int32_t max_results) { + ReferenceCountedFutureImpl* future_impl = future(); + SafeFutureHandle handle = + future_impl->SafeAlloc(kStorageReferenceFnList); + StorageInternal* storage_internal = storage_; + + FIRStorageVoidListResultError completion_block = + ^(FIRStorageListResult* _Nullable list_result_objc, NSError* _Nullable error) { + Error error_code = NSErrorToErrorCode(error); + const char* error_message = GetErrorMessage(error_code); + if (list_result_objc != nil) { + auto list_internal = new ListResultInternal( + storage_internal, + std::make_unique(list_result_objc)); + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(list_internal)); + } else { + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(nullptr)); + } + }; + + [impl() listWithMaxResults:max_results completion:completion_block]; + return ListLastResult(); +} + +Future StorageReferenceInternal::List(int32_t max_results, + const char* page_token) { + ReferenceCountedFutureImpl* future_impl = future(); + SafeFutureHandle handle = + future_impl->SafeAlloc(kStorageReferenceFnList); + StorageInternal* storage_internal = storage_; + + NSString* page_token_objc = page_token ? @(page_token) : nil; + + FIRStorageVoidListResultError completion_block = + ^(FIRStorageListResult* _Nullable list_result_objc, NSError* _Nullable error) { + Error error_code = NSErrorToErrorCode(error); + const char* error_message = GetErrorMessage(error_code); + if (list_result_objc != nil) { + auto list_internal = new ListResultInternal( + storage_internal, + std::make_unique(list_result_objc)); + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(list_internal)); + } else { + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(nullptr)); + } + }; + + [impl() listWithMaxResults:max_results + pageToken:page_token_objc + completion:completion_block]; + return ListLastResult(); +} + +Future StorageReferenceInternal::ListAll() { + ReferenceCountedFutureImpl* future_impl = future(); + SafeFutureHandle handle = + future_impl->SafeAlloc(kStorageReferenceFnList); + StorageInternal* storage_internal = storage_; + + FIRStorageVoidListResultError completion_block = + ^(FIRStorageListResult* _Nullable list_result_objc, NSError* _Nullable error) { + Error error_code = NSErrorToErrorCode(error); + const char* error_message = GetErrorMessage(error_code); + if (list_result_objc != nil) { + auto list_internal = new ListResultInternal( + storage_internal, + std::make_unique(list_result_objc)); + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(list_internal)); + } else { + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(nullptr)); + } + }; + [impl() listAllWithCompletion:completion_block]; + return ListLastResult(); +} + +Future StorageReferenceInternal::ListLastResult() { + return static_cast&>( + future()->LastResult(kStorageReferenceFnList)); +} + ReferenceCountedFutureImpl* StorageReferenceInternal::future() { return storage_->future_manager().GetFutureApi(this); }