Skip to content

Commit 88a564c

Browse files
[Storage] Implement List API across Android, iOS, and Desktop
Added the paginated `List` API to the Firebase Storage C++ SDK. - Android: Interoperates via JNI with `com/google/firebase/storage/ListResult`. - iOS: Connects to `FIRStorageListOptions` and `FIRStorageListResult` using Objective-C. - Desktop: Uses the Firebase Storage REST API `o` endpoint to retrieve folder prefixes and files. Included integration tests to verify successful nested directory creation, listing results, and pagination via `page_token`. Co-authored-by: AustinBenoit <22805659+AustinBenoit@users.noreply.github.com>
1 parent 5f348b8 commit 88a564c

15 files changed

Lines changed: 642 additions & 1 deletion

storage/CMakeLists.txt

Lines changed: 1 addition & 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

storage/integration_test/src/integration_test.cc

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,4 +1644,84 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) {
16441644
InitializeAppAndAuth();
16451645
}
16461646

1647+
TEST_F(FirebaseStorageTest, TestList) {
1648+
SignIn();
1649+
1650+
firebase::storage::StorageReference root = CreateFolder();
1651+
1652+
// Create some files and folders
1653+
std::vector<std::string> file_paths = {
1654+
"folderA/file1.txt",
1655+
"folderA/file2.txt",
1656+
"folderB/file3.txt",
1657+
"folderC/file4.txt",
1658+
"rootFile.txt"
1659+
};
1660+
1661+
for (const std::string& path : file_paths) {
1662+
firebase::storage::StorageReference ref = root.Child(path);
1663+
WaitForCompletion(RunWithRetry([&]() {
1664+
return ref.PutBytes(&kSimpleTestFile[0], kSimpleTestFile.size());
1665+
}), "PutBytes List Test");
1666+
}
1667+
1668+
// Allow some time for index to catch up
1669+
ProcessEvents(1000);
1670+
1671+
// List all at the root of the test folder
1672+
firebase::Future<firebase::storage::StorageListResult> list_future;
1673+
WaitForCompletion(RunWithRetry([&]() {
1674+
list_future = root.List(1000);
1675+
return list_future;
1676+
}), "ListRoot");
1677+
1678+
EXPECT_EQ(list_future.error(), firebase::storage::kErrorNone);
1679+
1680+
auto list_result = list_future.result();
1681+
ASSERT_NE(list_result, nullptr);
1682+
1683+
// Check prefixes (folders)
1684+
auto prefixes = list_result->prefixes();
1685+
EXPECT_EQ(prefixes.size(), 3);
1686+
int found_prefixes = 0;
1687+
for (auto& prefix : prefixes) {
1688+
std::string name = prefix.name();
1689+
if (name == "folderA" || name == "folderB" || name == "folderC") {
1690+
found_prefixes++;
1691+
}
1692+
}
1693+
EXPECT_EQ(found_prefixes, 3);
1694+
1695+
// Check items (files)
1696+
auto items = list_result->items();
1697+
EXPECT_EQ(items.size(), 1);
1698+
EXPECT_EQ(items[0].name(), "rootFile.txt");
1699+
1700+
EXPECT_EQ(list_result->next_page_token(), nullptr);
1701+
1702+
// Test pagination
1703+
firebase::storage::StorageReference folderA = root.Child("folderA");
1704+
WaitForCompletion(RunWithRetry([&]() {
1705+
list_future = folderA.List(1);
1706+
return list_future;
1707+
}), "ListFolderA_Page1");
1708+
1709+
EXPECT_EQ(list_future.error(), firebase::storage::kErrorNone);
1710+
list_result = list_future.result();
1711+
EXPECT_EQ(list_result->items().size(), 1);
1712+
EXPECT_NE(list_result->next_page_token(), nullptr);
1713+
1714+
std::string next_token = list_result->next_page_token();
1715+
1716+
WaitForCompletion(RunWithRetry([&]() {
1717+
list_future = folderA.List(1, next_token.c_str());
1718+
return list_future;
1719+
}), "ListFolderA_Page2");
1720+
1721+
EXPECT_EQ(list_future.error(), firebase::storage::kErrorNone);
1722+
list_result = list_future.result();
1723+
EXPECT_EQ(list_result->items().size(), 1);
1724+
EXPECT_EQ(list_result->next_page_token(), nullptr);
1725+
}
1726+
16471727
} // namespace firebase_testapp_automated

storage/src/android/storage_reference_android.cc

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ namespace firebase {
3333
namespace storage {
3434
namespace internal {
3535

36+
// clang-format off
37+
#define LIST_RESULT_METHODS(X) \
38+
X(GetPrefixes, "getPrefixes", "()Ljava/util/List;"), \
39+
X(GetItems, "getItems", "()Ljava/util/List;"), \
40+
X(GetPageToken, "getPageToken", "()Ljava/lang/String;")
41+
METHOD_LOOKUP_DECLARATION(list_result, LIST_RESULT_METHODS)
42+
METHOD_LOOKUP_DEFINITION(list_result,
43+
PROGUARD_KEEP_CLASS
44+
"com/google/firebase/storage/ListResult",
45+
LIST_RESULT_METHODS)
46+
// clang-format on
47+
3648
// clang-format off
3749
#define STORAGE_REFERENCE_METHODS(X) \
3850
X(Child, "child", \
@@ -61,6 +73,8 @@ namespace internal {
6173
X(PutFileWithMetadata, "putFile", \
6274
"(Landroid/net/Uri;Lcom/google/firebase/storage/StorageMetadata;)" \
6375
"Lcom/google/firebase/storage/UploadTask;"), \
76+
X(List, "list", \
77+
"(ILjava/lang/String;)Lcom/google/android/gms/tasks/Task;"), \
6478
X(PutFileWithMetadataAndUri, "putFile", \
6579
"(Landroid/net/Uri;Lcom/google/firebase/storage/StorageMetadata;" \
6680
"Landroid/net/Uri;)Lcom/google/firebase/storage/UploadTask;"), \
@@ -105,17 +119,20 @@ enum StorageReferenceFn {
105119
kStorageReferenceFnUpdateMetadata,
106120
kStorageReferenceFnPutBytes,
107121
kStorageReferenceFnPutFile,
122+
kStorageReferenceFnList,
108123
kStorageReferenceFnCount,
109124
};
110125

111126
bool StorageReferenceInternal::Initialize(App* app) {
112127
JNIEnv* env = app->GetJNIEnv();
113128
jobject activity = app->activity();
114-
return storage_reference::CacheMethodIds(env, activity);
129+
return list_result::CacheMethodIds(env, activity) &&
130+
storage_reference::CacheMethodIds(env, activity);
115131
}
116132

117133
void StorageReferenceInternal::Terminate(App* app) {
118134
JNIEnv* env = app->GetJNIEnv();
135+
list_result::ReleaseClass(env);
119136
storage_reference::ReleaseClass(env);
120137
util::CheckAndClearJniExceptions(env);
121138
}
@@ -249,9 +266,59 @@ void StorageReferenceInternal::FutureCallback(JNIEnv* env, jobject result,
249266
// is returned.
250267
data->impl->CompleteWithResult(data->handle, code, message.c_str(),
251268
Metadata(nullptr));
269+
} else if (data->func == kStorageReferenceFnList) {
270+
data->impl->CompleteWithResult(data->handle, code, message.c_str(),
271+
StorageListResult(nullptr));
252272
} else {
253273
data->impl->Complete(data->handle, code, message.c_str());
254274
}
275+
} else if (result && env->IsInstanceOf(result, list_result::GetClass())) {
276+
// result is a ListResult.
277+
SafeFutureHandle<StorageListResult> handle(data->handle);
278+
jobject prefixes_list = env->CallObjectMethod(
279+
result, list_result::GetMethodId(list_result::kGetPrefixes));
280+
jobject items_list = env->CallObjectMethod(
281+
result, list_result::GetMethodId(list_result::kGetItems));
282+
jstring page_token_jstring = static_cast<jstring>(env->CallObjectMethod(
283+
result, list_result::GetMethodId(list_result::kGetPageToken)));
284+
285+
std::vector<StorageReference> prefixes;
286+
if (prefixes_list) {
287+
jint size = env->CallIntMethod(
288+
prefixes_list, list::GetMethodId(list::kSize));
289+
for (jint i = 0; i < size; ++i) {
290+
jobject prefix_obj = env->CallObjectMethod(
291+
prefixes_list, list::GetMethodId(list::kGet), i);
292+
prefixes.push_back(
293+
StorageReference(new StorageReferenceInternal(data->storage, prefix_obj)));
294+
env->DeleteLocalRef(prefix_obj);
295+
}
296+
env->DeleteLocalRef(prefixes_list);
297+
}
298+
299+
std::vector<StorageReference> items;
300+
if (items_list) {
301+
jint size = env->CallIntMethod(
302+
items_list, list::GetMethodId(list::kSize));
303+
for (jint i = 0; i < size; ++i) {
304+
jobject item_obj = env->CallObjectMethod(
305+
items_list, list::GetMethodId(list::kGet), i);
306+
items.push_back(
307+
StorageReference(new StorageReferenceInternal(data->storage, item_obj)));
308+
env->DeleteLocalRef(item_obj);
309+
}
310+
env->DeleteLocalRef(items_list);
311+
}
312+
313+
std::string page_token;
314+
if (page_token_jstring) {
315+
page_token = util::JniStringToString(env, page_token_jstring);
316+
env->DeleteLocalRef(page_token_jstring);
317+
}
318+
319+
StorageListResultInternal* list_internal =
320+
new StorageListResultInternal(prefixes, items, page_token);
321+
data->impl->CompleteWithResult(handle, kErrorNone, StorageListResult(list_internal));
255322
} else if (result && env->IsInstanceOf(result, util::string::GetClass())) {
256323
LogDebug("FutureCallback: Completing a Future from a String.");
257324
// Complete a Future<std::string> from a Java String object.
@@ -687,6 +754,38 @@ Future<Metadata> StorageReferenceInternal::PutFileLastResult() {
687754
future()->LastResult(kStorageReferenceFnPutFile));
688755
}
689756

757+
Future<StorageListResult> StorageReferenceInternal::List(int max_results_per_page,
758+
const char *page_token) {
759+
JNIEnv* env = storage_->app()->GetJNIEnv();
760+
ReferenceCountedFutureImpl* future_impl = future();
761+
FutureHandle handle =
762+
future_impl->Alloc<StorageListResult>(kStorageReferenceFnList);
763+
764+
jstring java_page_token = page_token ? env->NewStringUTF(page_token) : nullptr;
765+
jobject task = env->CallObjectMethod(
766+
obj_, storage_reference::GetMethodId(storage_reference::kList),
767+
max_results_per_page, java_page_token);
768+
769+
if (java_page_token) {
770+
env->DeleteLocalRef(java_page_token);
771+
}
772+
773+
util::RegisterCallbackOnTask(
774+
env, task, FutureCallback,
775+
// FutureCallback will delete the newed FutureCallbackData.
776+
reinterpret_cast<void*>(new FutureCallbackData(handle, future(), storage_,
777+
kStorageReferenceFnList)),
778+
storage_->jni_task_id());
779+
util::CheckAndClearJniExceptions(env);
780+
env->DeleteLocalRef(task);
781+
return ListLastResult();
782+
}
783+
784+
Future<StorageListResult> StorageReferenceInternal::ListLastResult() {
785+
return static_cast<const Future<StorageListResult>&>(
786+
future()->LastResult(kStorageReferenceFnList));
787+
}
788+
690789
ReferenceCountedFutureImpl* StorageReferenceInternal::future() {
691790
return storage_->future_manager().GetFutureApi(this);
692791
}

storage/src/android/storage_reference_android.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ class StorageReferenceInternal {
129129
// Returns the result of the most recent call to PutFile();
130130
Future<Metadata> PutFileLastResult();
131131

132+
// List items (files) and prefixes (folders) under this StorageReference.
133+
Future<StorageListResult> List(int max_results_per_page,
134+
const char *page_token);
135+
136+
// Returns the result of the most recent call to List();
137+
Future<StorageListResult> ListLastResult();
138+
132139
// Initialize JNI bindings for this class.
133140
static bool Initialize(App* app);
134141
static void Terminate(App* app);

storage/src/common/list_result.cc

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "storage/src/include/firebase/storage/list_result.h"
16+
#include "storage/src/common/list_result_internal.h"
17+
18+
namespace firebase {
19+
namespace storage {
20+
21+
StorageListResult::StorageListResult() : internal_(nullptr) {}
22+
23+
StorageListResult::~StorageListResult() {
24+
delete internal_;
25+
internal_ = nullptr;
26+
}
27+
28+
StorageListResult::StorageListResult(const StorageListResult& other)
29+
: internal_(other.internal_ ? new internal::StorageListResultInternal(*other.internal_) : nullptr) {}
30+
31+
StorageListResult& StorageListResult::operator=(const StorageListResult& other) {
32+
if (this == &other) {
33+
return *this;
34+
}
35+
delete internal_;
36+
internal_ = other.internal_ ? new internal::StorageListResultInternal(*other.internal_) : nullptr;
37+
return *this;
38+
}
39+
40+
#if defined(FIREBASE_USE_MOVE_OPERATORS) || defined(DOXYGEN)
41+
StorageListResult::StorageListResult(StorageListResult&& other) {
42+
internal_ = other.internal_;
43+
other.internal_ = nullptr;
44+
}
45+
46+
StorageListResult& StorageListResult::operator=(StorageListResult&& other) {
47+
if (this == &other) {
48+
return *this;
49+
}
50+
delete internal_;
51+
internal_ = other.internal_;
52+
other.internal_ = nullptr;
53+
return *this;
54+
}
55+
#endif // defined(FIREBASE_USE_MOVE_OPERATORS) || defined(DOXYGEN)
56+
57+
const std::vector<StorageReference>& StorageListResult::prefixes() const {
58+
static const std::vector<StorageReference> empty_prefixes;
59+
return internal_ ? internal_->prefixes() : empty_prefixes;
60+
}
61+
62+
const std::vector<StorageReference>& StorageListResult::items() const {
63+
static const std::vector<StorageReference> empty_items;
64+
return internal_ ? internal_->items() : empty_items;
65+
}
66+
67+
const char* StorageListResult::next_page_token() const {
68+
return internal_ ? internal_->next_page_token() : nullptr;
69+
}
70+
71+
StorageListResult::StorageListResult(internal::StorageListResultInternal* internal)
72+
: internal_(internal) {}
73+
74+
} // namespace storage
75+
} // namespace firebase
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef FIREBASE_STORAGE_SRC_COMMON_LIST_RESULT_INTERNAL_H_
16+
#define FIREBASE_STORAGE_SRC_COMMON_LIST_RESULT_INTERNAL_H_
17+
18+
#include <string>
19+
#include <vector>
20+
21+
#include "storage/src/include/firebase/storage/storage_reference.h"
22+
23+
namespace firebase {
24+
namespace storage {
25+
namespace internal {
26+
27+
/// @brief Internal representation of a StorageListResult.
28+
class StorageListResultInternal {
29+
public:
30+
StorageListResultInternal() {}
31+
StorageListResultInternal(const std::vector<StorageReference>& prefixes,
32+
const std::vector<StorageReference>& items,
33+
const std::string& next_page_token)
34+
: prefixes_(prefixes), items_(items), next_page_token_(next_page_token) {}
35+
36+
StorageListResultInternal(const StorageListResultInternal& other)
37+
: prefixes_(other.prefixes_),
38+
items_(other.items_),
39+
next_page_token_(other.next_page_token_) {}
40+
41+
const std::vector<StorageReference>& prefixes() const { return prefixes_; }
42+
const std::vector<StorageReference>& items() const { return items_; }
43+
const char* next_page_token() const {
44+
return next_page_token_.empty() ? nullptr : next_page_token_.c_str();
45+
}
46+
47+
private:
48+
std::vector<StorageReference> prefixes_;
49+
std::vector<StorageReference> items_;
50+
std::string next_page_token_;
51+
};
52+
53+
} // namespace internal
54+
} // namespace storage
55+
} // namespace firebase
56+
57+
#endif // FIREBASE_STORAGE_SRC_COMMON_LIST_RESULT_INTERNAL_H_

0 commit comments

Comments
 (0)