Skip to content

Firebase Storage: Implement List and ListAll API for StorageReference #1726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions storage/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
282 changes: 281 additions & 1 deletion storage/integration_test/src/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <cstring>
#include <ctime>
#include <thread> // NOLINT
#include <vector> // For std::vector in list tests

#include "app_framework.h" // NOLINT
#include "firebase/app.h"
Expand Down Expand Up @@ -80,6 +81,9 @@ 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:
Expand All @@ -96,8 +100,10 @@ class FirebaseStorageTest : public FirebaseTest {
// Called after each test.
void TearDown() override;

// File references that we need to delete on test exit.
protected:
// Root reference for list tests.
firebase::storage::StorageReference list_test_root_;

// Initialize Firebase App and Firebase Auth.
static void InitializeAppAndAuth();
// Shut down Firebase App and Firebase Auth.
Expand All @@ -118,6 +124,18 @@ 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<std::string>& expected_item_names,
const std::vector<std::string>& expected_prefix_names);


static firebase::App* shared_app_;
static firebase::auth::Auth* shared_auth_;

Expand Down Expand Up @@ -212,6 +230,16 @@ void FirebaseStorageTest::TerminateAppAndAuth() {
void FirebaseStorageTest::SetUp() {
FirebaseTest::SetUp();
InitializeStorage();
if (storage_ != nullptr && storage_->GetReference().is_valid()) {
list_test_root_ = CreateFolder().Child("list_tests_root");
// list_test_root_ itself doesn't need to be in cleanup_files_ if its parent from CreateFolder() is.
// However, specific files/folders created under list_test_root_ for each test *will* be added
// via UploadStringAsFile or by explicitly adding the parent of a set of files for that test.
} else {
// Handle cases where storage might not be initialized (e.g. if InitializeStorage fails)
// by providing a default, invalid reference.
list_test_root_ = firebase::storage::StorageReference();
}
}

void FirebaseStorageTest::TearDown() {
Expand Down Expand Up @@ -313,6 +341,62 @@ 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<firebase::storage::Metadata> future =
RunWithRetry<firebase::storage::Metadata>(
[&]() { 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<std::string>& expected_item_names,
const std::vector<std::string>& expected_prefix_names) {
ASSERT_TRUE(list_result.is_valid());

std::vector<std::string> 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<std::string> 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<std::string> 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<std::string> 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();
Expand Down Expand Up @@ -1622,4 +1706,200 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) {
InitializeAppAndAuth();
}

TEST_F(FirebaseStorageTest, ListAllBasic) {
SKIP_TEST_ON_ANDROID_EMULATOR; // List tests can be slow on emulators or have quota issues.
SignIn();
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";

firebase::storage::StorageReference list_all_base =
list_test_root_.Child("list_all_basic_test");
// cleanup_files_.push_back(list_all_base); // Not a file, its contents are files.

UploadStringAsFile(list_all_base.Child("file_a.txt"), "content_a");
UploadStringAsFile(list_all_base.Child("file_b.txt"), "content_b");
UploadStringAsFile(list_all_base.Child("prefix1/file_c.txt"), "content_c_in_prefix1");
UploadStringAsFile(list_all_base.Child("prefix2/file_e.txt"), "content_e_in_prefix2");

LogDebug("Calling ListAll() on gs://%s%s", list_all_base.bucket().c_str(),
list_all_base.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future =
list_all_base.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;
SignIn();
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";

firebase::storage::StorageReference list_paginated_base =
list_test_root_.Child("list_paginated_test");
// cleanup_files_.push_back(list_paginated_base);

// Expected total entries: file_aa.txt, file_bb.txt, file_ee.txt, prefix_x/, prefix_y/ (5 entries)
UploadStringAsFile(list_paginated_base.Child("file_aa.txt"), "content_aa");
UploadStringAsFile(list_paginated_base.Child("prefix_x/file_cc.txt"), "content_cc_in_prefix_x");
UploadStringAsFile(list_paginated_base.Child("file_bb.txt"), "content_bb");
UploadStringAsFile(list_paginated_base.Child("prefix_y/file_dd.txt"), "content_dd_in_prefix_y");
UploadStringAsFile(list_paginated_base.Child("file_ee.txt"), "content_ee");


std::vector<std::string> all_item_names_collected;
std::vector<std::string> 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",
list_paginated_base.bucket().c_str(), list_paginated_base.full_path().c_str(), page_size);

do {
page_count++;
LogDebug("Fetching page %d, token: '%s'", page_count, page_token.c_str());
firebase::Future<firebase::storage::ListResult> future =
page_token.empty() ? list_paginated_base.List(page_size)
: list_paginated_base.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<std::string> expected_final_items = {"file_aa.txt", "file_bb.txt", "file_ee.txt"};
std::vector<std::string> 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;
SignIn();
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";

firebase::storage::StorageReference list_empty_ref =
list_test_root_.Child("list_empty_folder_test");
// Do not upload anything to this reference.
// cleanup_files_.push_back(list_empty_ref); // Not a file

LogDebug("Calling ListAll() on empty folder: gs://%s%s",
list_empty_ref.bucket().c_str(), list_empty_ref.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future =
list_empty_ref.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;
SignIn();
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";

firebase::storage::StorageReference list_max_greater_base =
list_test_root_.Child("list_max_greater_test");
// cleanup_files_.push_back(list_max_greater_base);

UploadStringAsFile(list_max_greater_base.Child("only_file.txt"), "content_only");
UploadStringAsFile(list_max_greater_base.Child("only_prefix/another.txt"), "content_another_in_prefix");

LogDebug("Calling List(10) on gs://%s%s",
list_max_greater_base.bucket().c_str(),
list_max_greater_base.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future =
list_max_greater_base.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;
SignIn();
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";

firebase::storage::StorageReference list_non_existent_ref =
list_test_root_.Child("this_folder_does_not_exist_for_list_test");
// No cleanup needed as nothing is created.

LogDebug("Calling ListAll() on non-existent path: gs://%s%s",
list_non_existent_ref.bucket().c_str(),
list_non_existent_ref.full_path().c_str());
firebase::Future<firebase::storage::ListResult> future =
list_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
Loading
Loading