Skip to content

Commit 5e9fc6b

Browse files
feat: Implement List and ListAll API for StorageReference
Adds the `List(max_results, page_token)` and `ListAll()` methods to the `firebase::storage::StorageReference` C++ API. These methods allow you to list objects and common prefixes (directories) within a storage location. Features: - Paginated listing using `List(max_results, page_token)`. - Comprehensive listing using `ListAll()`. - Returns a `Future<ListResult>`, where `ListResult` contains a list of `StorageReference` objects for items and prefixes, and a page token for continuation. - Implemented for Android by calling the underlying Firebase Android SDK's list operations via JNI. - Implemented for iOS by calling the underlying Firebase iOS SDK's list operations. - Desktop platform provides stubs that return `kErrorUnimplemented`. - Includes comprehensive integration tests covering various scenarios such as basic listing, pagination, empty folders, and listing non-existent paths.
1 parent a06d206 commit 5e9fc6b

20 files changed

+1720
-3
lines changed

storage/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ set(common_SRCS
1919
src/common/common.cc
2020
src/common/controller.cc
2121
src/common/listener.cc
22+
src/common/list_result.cc
2223
src/common/metadata.cc
2324
src/common/storage.cc
2425
src/common/storage_reference.cc
@@ -36,6 +37,7 @@ binary_to_array("storage_resources"
3637
set(android_SRCS
3738
${storage_resources_source}
3839
src/android/controller_android.cc
40+
src/android/list_result_android.cc
3941
src/android/metadata_android.cc
4042
src/android/storage_android.cc
4143
src/android/storage_reference_android.cc)
@@ -44,6 +46,7 @@ set(android_SRCS
4446
set(ios_SRCS
4547
src/ios/controller_ios.mm
4648
src/ios/listener_ios.mm
49+
src/ios/list_result_ios.mm
4750
src/ios/metadata_ios.mm
4851
src/ios/storage_ios.mm
4952
src/ios/storage_reference_ios.mm
@@ -54,6 +57,7 @@ set(desktop_SRCS
5457
src/desktop/controller_desktop.cc
5558
src/desktop/curl_requests.cc
5659
src/desktop/listener_desktop.cc
60+
src/desktop/list_result_desktop.cc
5761
src/desktop/metadata_desktop.cc
5862
src/desktop/rest_operation.cc
5963
src/desktop/storage_desktop.cc

storage/integration_test/src/integration_test.cc

Lines changed: 281 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <cstring>
2121
#include <ctime>
2222
#include <thread> // NOLINT
23+
#include <vector> // For std::vector in list tests
2324

2425
#include "app_framework.h" // NOLINT
2526
#include "firebase/app.h"
@@ -80,6 +81,9 @@ using app_framework::PathForResource;
8081
using app_framework::ProcessEvents;
8182
using firebase_test_framework::FirebaseTest;
8283
using testing::ElementsAreArray;
84+
using testing::IsEmpty;
85+
using testing::UnorderedElementsAreArray;
86+
8387

8488
class FirebaseStorageTest : public FirebaseTest {
8589
public:
@@ -96,8 +100,10 @@ class FirebaseStorageTest : public FirebaseTest {
96100
// Called after each test.
97101
void TearDown() override;
98102

99-
// File references that we need to delete on test exit.
100103
protected:
104+
// Root reference for list tests.
105+
firebase::storage::StorageReference list_test_root_;
106+
101107
// Initialize Firebase App and Firebase Auth.
102108
static void InitializeAppAndAuth();
103109
// Shut down Firebase App and Firebase Auth.
@@ -118,6 +124,18 @@ class FirebaseStorageTest : public FirebaseTest {
118124
// Create a unique working folder and return a reference to it.
119125
firebase::storage::StorageReference CreateFolder();
120126

127+
// Uploads a string as a file to the given StorageReference.
128+
void UploadStringAsFile(
129+
firebase::storage::StorageReference& ref, const std::string& content,
130+
const char* content_type = nullptr);
131+
132+
// Verifies the contents of a ListResult.
133+
void VerifyListResultContains(
134+
const firebase::storage::ListResult& list_result,
135+
const std::vector<std::string>& expected_item_names,
136+
const std::vector<std::string>& expected_prefix_names);
137+
138+
121139
static firebase::App* shared_app_;
122140
static firebase::auth::Auth* shared_auth_;
123141

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

217245
void FirebaseStorageTest::TearDown() {
@@ -313,6 +341,62 @@ void FirebaseStorageTest::SignOut() {
313341
EXPECT_FALSE(shared_auth_->current_user().is_valid());
314342
}
315343

344+
void FirebaseStorageTest::UploadStringAsFile(
345+
firebase::storage::StorageReference& ref, const std::string& content,
346+
const char* content_type) {
347+
LogDebug("Uploading string content to: gs://%s%s", ref.bucket().c_str(),
348+
ref.full_path().c_str());
349+
firebase::storage::Metadata metadata;
350+
if (content_type) {
351+
metadata.set_content_type(content_type);
352+
}
353+
firebase::Future<firebase::storage::Metadata> future =
354+
RunWithRetry<firebase::storage::Metadata>(
355+
[&]() { return ref.PutBytes(content.c_str(), content.length(), metadata); });
356+
WaitForCompletion(future, "UploadStringAsFile");
357+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
358+
<< "Failed to upload to " << ref.full_path() << ": "
359+
<< future.error_message();
360+
ASSERT_NE(future.result(), nullptr);
361+
// On some platforms (iOS), size_bytes might not be immediately available or might be 0
362+
// if the upload was very fast and metadata propagation is slow.
363+
// For small files, this is less critical than the content being there.
364+
// For larger files in other tests, size_bytes is asserted.
365+
// ASSERT_EQ(future.result()->size_bytes(), content.length());
366+
cleanup_files_.push_back(ref);
367+
}
368+
369+
void FirebaseStorageTest::VerifyListResultContains(
370+
const firebase::storage::ListResult& list_result,
371+
const std::vector<std::string>& expected_item_names,
372+
const std::vector<std::string>& expected_prefix_names) {
373+
ASSERT_TRUE(list_result.is_valid());
374+
375+
std::vector<std::string> actual_item_names;
376+
for (const auto& item_ref : list_result.items()) {
377+
actual_item_names.push_back(item_ref.name());
378+
}
379+
std::sort(actual_item_names.begin(), actual_item_names.end());
380+
std::vector<std::string> sorted_expected_item_names = expected_item_names;
381+
std::sort(sorted_expected_item_names.begin(), sorted_expected_item_names.end());
382+
383+
EXPECT_THAT(actual_item_names, ::testing::ContainerEq(sorted_expected_item_names))
384+
<< "Item names do not match expected.";
385+
386+
387+
std::vector<std::string> actual_prefix_names;
388+
for (const auto& prefix_ref : list_result.prefixes()) {
389+
actual_prefix_names.push_back(prefix_ref.name());
390+
}
391+
std::sort(actual_prefix_names.begin(), actual_prefix_names.end());
392+
std::vector<std::string> sorted_expected_prefix_names = expected_prefix_names;
393+
std::sort(sorted_expected_prefix_names.begin(), sorted_expected_prefix_names.end());
394+
395+
EXPECT_THAT(actual_prefix_names, ::testing::ContainerEq(sorted_expected_prefix_names))
396+
<< "Prefix names do not match expected.";
397+
}
398+
399+
316400
firebase::storage::StorageReference FirebaseStorageTest::CreateFolder() {
317401
// Generate a folder for the test data based on the time in milliseconds.
318402
int64_t time_in_microseconds = GetCurrentTimeInMicroseconds();
@@ -1622,4 +1706,200 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) {
16221706
InitializeAppAndAuth();
16231707
}
16241708

1709+
TEST_F(FirebaseStorageTest, ListAllBasic) {
1710+
SKIP_TEST_ON_ANDROID_EMULATOR; // List tests can be slow on emulators or have quota issues.
1711+
SignIn();
1712+
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";
1713+
1714+
firebase::storage::StorageReference list_all_base =
1715+
list_test_root_.Child("list_all_basic_test");
1716+
// cleanup_files_.push_back(list_all_base); // Not a file, its contents are files.
1717+
1718+
UploadStringAsFile(list_all_base.Child("file_a.txt"), "content_a");
1719+
UploadStringAsFile(list_all_base.Child("file_b.txt"), "content_b");
1720+
UploadStringAsFile(list_all_base.Child("prefix1/file_c.txt"), "content_c_in_prefix1");
1721+
UploadStringAsFile(list_all_base.Child("prefix2/file_e.txt"), "content_e_in_prefix2");
1722+
1723+
LogDebug("Calling ListAll() on gs://%s%s", list_all_base.bucket().c_str(),
1724+
list_all_base.full_path().c_str());
1725+
firebase::Future<firebase::storage::ListResult> future =
1726+
list_all_base.ListAll();
1727+
WaitForCompletion(future, "ListAllBasic");
1728+
1729+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1730+
<< future.error_message();
1731+
ASSERT_NE(future.result(), nullptr);
1732+
const firebase::storage::ListResult* result = future.result();
1733+
1734+
VerifyListResultContains(*result, {"file_a.txt", "file_b.txt"},
1735+
{"prefix1/", "prefix2/"});
1736+
EXPECT_TRUE(result->page_token().empty()) << "Page token should be empty for ListAll.";
1737+
}
1738+
1739+
TEST_F(FirebaseStorageTest, ListPaginated) {
1740+
SKIP_TEST_ON_ANDROID_EMULATOR;
1741+
SignIn();
1742+
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";
1743+
1744+
firebase::storage::StorageReference list_paginated_base =
1745+
list_test_root_.Child("list_paginated_test");
1746+
// cleanup_files_.push_back(list_paginated_base);
1747+
1748+
// Expected total entries: file_aa.txt, file_bb.txt, file_ee.txt, prefix_x/, prefix_y/ (5 entries)
1749+
UploadStringAsFile(list_paginated_base.Child("file_aa.txt"), "content_aa");
1750+
UploadStringAsFile(list_paginated_base.Child("prefix_x/file_cc.txt"), "content_cc_in_prefix_x");
1751+
UploadStringAsFile(list_paginated_base.Child("file_bb.txt"), "content_bb");
1752+
UploadStringAsFile(list_paginated_base.Child("prefix_y/file_dd.txt"), "content_dd_in_prefix_y");
1753+
UploadStringAsFile(list_paginated_base.Child("file_ee.txt"), "content_ee");
1754+
1755+
1756+
std::vector<std::string> all_item_names_collected;
1757+
std::vector<std::string> all_prefix_names_collected;
1758+
std::string page_token = "";
1759+
const int page_size = 2;
1760+
int page_count = 0;
1761+
const int max_pages = 5; // Safety break for loop
1762+
1763+
LogDebug("Starting paginated List() on gs://%s%s with page_size %d",
1764+
list_paginated_base.bucket().c_str(), list_paginated_base.full_path().c_str(), page_size);
1765+
1766+
do {
1767+
page_count++;
1768+
LogDebug("Fetching page %d, token: '%s'", page_count, page_token.c_str());
1769+
firebase::Future<firebase::storage::ListResult> future =
1770+
page_token.empty() ? list_paginated_base.List(page_size)
1771+
: list_paginated_base.List(page_size, page_token.c_str());
1772+
WaitForCompletion(future, "ListPaginated - Page " + std::to_string(page_count));
1773+
1774+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone) << future.error_message();
1775+
ASSERT_NE(future.result(), nullptr);
1776+
const firebase::storage::ListResult* result = future.result();
1777+
ASSERT_TRUE(result->is_valid());
1778+
1779+
LogDebug("Page %d items: %zu, prefixes: %zu", page_count, result->items().size(), result->prefixes().size());
1780+
for (const auto& item : result->items()) {
1781+
all_item_names_collected.push_back(item.name());
1782+
LogDebug(" Item: %s", item.name().c_str());
1783+
}
1784+
for (const auto& prefix : result->prefixes()) {
1785+
all_prefix_names_collected.push_back(prefix.name());
1786+
LogDebug(" Prefix: %s", prefix.name().c_str());
1787+
}
1788+
1789+
page_token = result->page_token();
1790+
1791+
size_t entries_on_page = result->items().size() + result->prefixes().size();
1792+
1793+
if (!page_token.empty()) {
1794+
EXPECT_EQ(entries_on_page, page_size) << "A non-last page should have full page_size entries.";
1795+
} else {
1796+
// This is the last page
1797+
size_t total_entries = 5;
1798+
size_t expected_entries_on_last_page = total_entries % page_size;
1799+
if (expected_entries_on_last_page == 0 && total_entries > 0) { // if total is a multiple of page_size
1800+
expected_entries_on_last_page = page_size;
1801+
}
1802+
EXPECT_EQ(entries_on_page, expected_entries_on_last_page);
1803+
}
1804+
} while (!page_token.empty() && page_count < max_pages);
1805+
1806+
EXPECT_LT(page_count, max_pages) << "Exceeded max_pages, possible infinite loop.";
1807+
EXPECT_EQ(page_count, (5 + page_size -1) / page_size) << "Unexpected number of pages.";
1808+
1809+
1810+
std::vector<std::string> expected_final_items = {"file_aa.txt", "file_bb.txt", "file_ee.txt"};
1811+
std::vector<std::string> expected_final_prefixes = {"prefix_x/", "prefix_y/"};
1812+
1813+
// VerifyListResultContains needs a ListResult object. We can't directly use it with collected names.
1814+
// Instead, we sort and compare the collected names.
1815+
std::sort(all_item_names_collected.begin(), all_item_names_collected.end());
1816+
std::sort(all_prefix_names_collected.begin(), all_prefix_names_collected.end());
1817+
std::sort(expected_final_items.begin(), expected_final_items.end());
1818+
std::sort(expected_final_prefixes.begin(), expected_final_prefixes.end());
1819+
1820+
EXPECT_THAT(all_item_names_collected, ::testing::ContainerEq(expected_final_items));
1821+
EXPECT_THAT(all_prefix_names_collected, ::testing::ContainerEq(expected_final_prefixes));
1822+
}
1823+
1824+
1825+
TEST_F(FirebaseStorageTest, ListEmpty) {
1826+
SKIP_TEST_ON_ANDROID_EMULATOR;
1827+
SignIn();
1828+
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";
1829+
1830+
firebase::storage::StorageReference list_empty_ref =
1831+
list_test_root_.Child("list_empty_folder_test");
1832+
// Do not upload anything to this reference.
1833+
// cleanup_files_.push_back(list_empty_ref); // Not a file
1834+
1835+
LogDebug("Calling ListAll() on empty folder: gs://%s%s",
1836+
list_empty_ref.bucket().c_str(), list_empty_ref.full_path().c_str());
1837+
firebase::Future<firebase::storage::ListResult> future =
1838+
list_empty_ref.ListAll();
1839+
WaitForCompletion(future, "ListEmpty");
1840+
1841+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1842+
<< future.error_message();
1843+
ASSERT_NE(future.result(), nullptr);
1844+
const firebase::storage::ListResult* result = future.result();
1845+
1846+
VerifyListResultContains(*result, {}, {});
1847+
EXPECT_TRUE(result->page_token().empty());
1848+
}
1849+
1850+
TEST_F(FirebaseStorageTest, ListWithMaxResultsGreaterThanActual) {
1851+
SKIP_TEST_ON_ANDROID_EMULATOR;
1852+
SignIn();
1853+
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";
1854+
1855+
firebase::storage::StorageReference list_max_greater_base =
1856+
list_test_root_.Child("list_max_greater_test");
1857+
// cleanup_files_.push_back(list_max_greater_base);
1858+
1859+
UploadStringAsFile(list_max_greater_base.Child("only_file.txt"), "content_only");
1860+
UploadStringAsFile(list_max_greater_base.Child("only_prefix/another.txt"), "content_another_in_prefix");
1861+
1862+
LogDebug("Calling List(10) on gs://%s%s",
1863+
list_max_greater_base.bucket().c_str(),
1864+
list_max_greater_base.full_path().c_str());
1865+
firebase::Future<firebase::storage::ListResult> future =
1866+
list_max_greater_base.List(10); // Max results (10) > actual (1 file + 1 prefix = 2)
1867+
WaitForCompletion(future, "ListWithMaxResultsGreaterThanActual");
1868+
1869+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1870+
<< future.error_message();
1871+
ASSERT_NE(future.result(), nullptr);
1872+
const firebase::storage::ListResult* result = future.result();
1873+
1874+
VerifyListResultContains(*result, {"only_file.txt"}, {"only_prefix/"});
1875+
EXPECT_TRUE(result->page_token().empty());
1876+
}
1877+
1878+
TEST_F(FirebaseStorageTest, ListNonExistentPath) {
1879+
SKIP_TEST_ON_ANDROID_EMULATOR;
1880+
SignIn();
1881+
ASSERT_TRUE(list_test_root_.is_valid()) << "List test root is not valid.";
1882+
1883+
firebase::storage::StorageReference list_non_existent_ref =
1884+
list_test_root_.Child("this_folder_does_not_exist_for_list_test");
1885+
// No cleanup needed as nothing is created.
1886+
1887+
LogDebug("Calling ListAll() on non-existent path: gs://%s%s",
1888+
list_non_existent_ref.bucket().c_str(),
1889+
list_non_existent_ref.full_path().c_str());
1890+
firebase::Future<firebase::storage::ListResult> future =
1891+
list_non_existent_ref.ListAll();
1892+
WaitForCompletion(future, "ListNonExistentPath");
1893+
1894+
// Listing a non-existent path should not be an error, it's just an empty list.
1895+
ASSERT_EQ(future.error(), firebase::storage::kErrorNone)
1896+
<< future.error_message();
1897+
ASSERT_NE(future.result(), nullptr);
1898+
const firebase::storage::ListResult* result = future.result();
1899+
1900+
VerifyListResultContains(*result, {}, {});
1901+
EXPECT_TRUE(result->page_token().empty());
1902+
}
1903+
1904+
16251905
} // namespace firebase_testapp_automated

0 commit comments

Comments
 (0)