Skip to content

Commit 07a7304

Browse files
authored
Add List storage API to Unity SDK (#1434)
* Add List API to Unity SDK * Remove un needed vector refernce and add in the storage cmake change * Fix Gemini Comments * fix up testing to use aysnc pattern
1 parent 6ed687b commit 07a7304

6 files changed

Lines changed: 137 additions & 5 deletions

File tree

docs/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ Release Notes
112112
### Upcoming
113113
- Changes
114114
- Firebase AI: Add support for Grounding with Google Maps.
115+
- Storage: Added `ListAsync` API to list items and prefixes under a reference.
115116

116117
### 13.10.0
117118
- Changes

storage/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ set(firebase_storage_src_documented
3030
src/StorageException.cs
3131
src/StorageProgress.cs
3232
src/StorageReference.cs
33+
src/StorageListResult.cs
3334
src/UploadState.cs
3435
)
3536

storage/src/StorageListResult.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.Collections.Generic;
19+
using Firebase.Storage.Internal;
20+
21+
namespace Firebase.Storage {
22+
/// <summary>
23+
/// Represents the result of a List operation.
24+
/// </summary>
25+
public sealed class StorageListResult {
26+
private readonly StorageListResultInternal internalResult;
27+
private readonly List<StorageReference> prefixes;
28+
private readonly List<StorageReference> items;
29+
30+
internal StorageListResult(FirebaseStorage storage, StorageListResultInternal internalResult) {
31+
this.internalResult = internalResult;
32+
33+
var prefixesCount = internalResult.prefixes_count();
34+
prefixes = new List<StorageReference>((int)prefixesCount);
35+
for (uint i = 0; i < prefixesCount; i++) {
36+
prefixes.Add(new StorageReference(storage, internalResult.prefixes_get(i)));
37+
}
38+
39+
var itemsCount = internalResult.items_count();
40+
items = new List<StorageReference>((int)itemsCount);
41+
for (uint i = 0; i < itemsCount; i++) {
42+
items.Add(new StorageReference(storage, internalResult.items_get(i)));
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Gets the list of prefixes (folders) returned by the List operation.
48+
/// </summary>
49+
public IReadOnlyList<StorageReference> Prefixes { get { return prefixes; } }
50+
51+
/// <summary>
52+
/// Gets the list of items (files) returned by the List operation.
53+
/// </summary>
54+
public IReadOnlyList<StorageReference> Items { get { return items; } }
55+
56+
/// <summary>
57+
/// Gets a page token that can be used to resume the List operation, or an empty string if there are no more results.
58+
/// </summary>
59+
public string NextPageToken { get { return internalResult.next_page_token(); } }
60+
}
61+
}

storage/src/StorageReference.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,33 @@ public Task<StorageMetadata> GetMetadataAsync() {
480480
return result.Task;
481481
}
482482

483+
/// <summary>
484+
/// Retrieves a list of prefixes and items under this
485+
/// <see cref="StorageReference" />
486+
/// .
487+
/// </summary>
488+
/// <param name="maxResults">The maximum number of items to return.</param>
489+
/// <param name="pageToken">A page token to resume from.</param>
490+
/// <returns>
491+
/// A <see cref="Task"/>
492+
/// which can be used to monitor the operation and obtain the result.
493+
/// </returns>
494+
public Task<StorageListResult> ListAsync(int maxResults = 1000, string pageToken = null) {
495+
Task<StorageListResultInternal> internalTask;
496+
if (pageToken != null) {
497+
internalTask = Internal.ListAsync(maxResults, pageToken);
498+
} else {
499+
internalTask = Internal.ListAsync(maxResults);
500+
}
501+
502+
var tcs = new TaskCompletionSource<StorageListResult>();
503+
internalTask.ContinueWith(task => {
504+
CompleteTask(task, tcs, () => { return new StorageListResult(this.Storage, task.Result); },
505+
"ListAsync");
506+
});
507+
return tcs.Task;
508+
}
509+
483510
/// <summary>
484511
/// Retrieves a long lived download URL with a revokable token.
485512
/// </summary>

storage/src/swig/storage.i

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ static CppInstanceManager<Storage> g_storage_instances;
7171
%SWIG_FUTURE(Future_StorageMetadata, MetadataInternal, internal,
7272
firebase::storage::Metadata, FirebaseException)
7373
%SWIG_FUTURE(Future_Size, long, internal, size_t, FirebaseException)
74+
%SWIG_FUTURE(Future_StorageListResult, StorageListResultInternal, internal, firebase::storage::StorageListResult, FirebaseException)
7475

7576

7677
// TODO: Move this into App
@@ -160,10 +161,6 @@ static AttributeType& %mangle(Class) ##_## AttributeName ## _get_func(Class* sel
160161
%ignore firebase::storage::StorageReference::GetBytes;
161162
%ignore firebase::storage::StorageReference::PutBytes;
162163
%ignore firebase::storage::StorageReference::PutFile;
163-
// Ignore List methods and StorageListResult since StorageListResult is not wrapped yet.
164-
%ignore firebase::storage::StorageReference::List;
165-
%ignore firebase::storage::StorageReference::ListLastResult;
166-
%ignore firebase::storage::StorageListResult;
167164

168165
// Remove the copy operator as the proxy uses the copy constructor.
169166
%ignore firebase::storage::StorageReference::operator=(const StorageReference&);
@@ -217,6 +214,7 @@ static AttributeType& %mangle(Class) ##_## AttributeName ## _get_func(Class* sel
217214
}
218215
}
219216

217+
%rename("StorageListResultInternal") firebase::storage::StorageListResult;
220218
%rename("MetadataInternal") firebase::storage::Metadata;
221219
// Configure properties for get / set methods on the Metadata class.
222220
%safeattributestring(firebase::storage::Metadata, std::string, Bucket, bucket);
@@ -480,6 +478,17 @@ class MonitorController;
480478
%include "storage/src/include/firebase/storage/common.h"
481479
%include "storage/src/include/firebase/storage/controller.h"
482480
%include "storage/src/include/firebase/storage/listener.h"
481+
%ignore firebase::storage::StorageListResult::items;
482+
%ignore firebase::storage::StorageListResult::prefixes;
483+
484+
%extend firebase::storage::StorageListResult {
485+
size_t items_count() const { return self->items().size(); }
486+
firebase::storage::StorageReference items_get(size_t index) const { return self->items()[index]; }
487+
size_t prefixes_count() const { return self->prefixes().size(); }
488+
firebase::storage::StorageReference prefixes_get(size_t index) const { return self->prefixes()[index]; }
489+
}
490+
491+
%include "storage/src/include/firebase/storage/list_result.h"
483492
%include "storage/src/include/firebase/storage/metadata.h"
484493
%include "storage/src/include/firebase/storage/storage_reference.h"
485494
%include "storage/src/swig/monitor_controller.h"

storage/testapp/Assets/Firebase/Sample/Storage/UIHandlerAutomated.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ protected override void Start() {
140140
TestUploadSmallFileAndDownloadToFile,
141141
TestUploadLargeFileAndDownloadToFile,
142142
TestUploadLargeFileAndDownloadToFileWithCancelation,
143+
TestList,
143144
};
144145

145146
testRunner = AutomatedTestRunner.CreateTestRunner(
@@ -1092,6 +1093,38 @@ Task TestUploadLargeFileAndDownloadToFileWithCancelation() {
10921093
predownloadOperation: () => { CancelAfterDelayInSeconds(CANCELATION_DELAY_SECONDS); });
10931094
}
10941095

1095-
// TODO: Upload and attempt to partially download a file.
1096+
async Task TestList() {
1097+
string baseFolder = "test_list_" + Guid.NewGuid().ToString() + "/";
1098+
string file1Path = baseFolder + "file1.txt";
1099+
string file2Path = baseFolder + "file2.txt";
1100+
string file3Path = baseFolder + "file3.txt";
1101+
string file4Path = baseFolder + "prefix/file4.txt";
1102+
1103+
// Upload files
1104+
await UploadToPath(file1Path, SMALL_FILE_CONTENTS, MetadataTestMode.None, UploadBytesAsync, ValidateUploadSuccessfulNotFile);
1105+
await UploadToPath(file2Path, SMALL_FILE_CONTENTS, MetadataTestMode.None, UploadBytesAsync, ValidateUploadSuccessfulNotFile);
1106+
await UploadToPath(file3Path, SMALL_FILE_CONTENTS, MetadataTestMode.None, UploadBytesAsync, ValidateUploadSuccessfulNotFile);
1107+
await UploadToPath(file4Path, SMALL_FILE_CONTENTS, MetadataTestMode.None, UploadBytesAsync, ValidateUploadSuccessfulNotFile);
1108+
1109+
// List with maxResultsPerPage: 2 to test pagination
1110+
var storageRef = FirebaseStorage.DefaultInstance.GetReference(baseFolder);
1111+
var result = await storageRef.ListAsync(maxResults: 2);
1112+
Assert("result != null", result != null);
1113+
1114+
// We should get 2 items (order is not guaranteed, but we should have exactly 2)
1115+
AssertEq("result.Items.Count + result.Prefixes.Count", result.Items.Count + result.Prefixes.Count, 2);
1116+
Assert("result.NextPageToken != null", !string.IsNullOrEmpty(result.NextPageToken));
1117+
1118+
// Fetch page 2
1119+
var result2 = await storageRef.ListAsync(maxResults: 2, pageToken: result.NextPageToken);
1120+
AssertEq("result2.Items.Count + result2.Prefixes.Count", result2.Items.Count + result2.Prefixes.Count, 2); // 1 item remaining + 1 prefix
1121+
1122+
// Verify we can find the prefix
1123+
bool foundPrefix = false;
1124+
foreach (var prefix in result2.Prefixes) {
1125+
if (prefix.Name == "prefix") foundPrefix = true;
1126+
}
1127+
Assert("foundPrefix", foundPrefix);
1128+
}
10961129
}
10971130
}

0 commit comments

Comments
 (0)