Listing Objects
Enumerate objects in R2 buckets with automatic pagination handling.
Overview
public class BrowserService(IR2Client r2)
{
public async Task ListFilesAsync(string prefix)
{
var result = await r2.ListObjectsAsync("my-bucket", prefix);
foreach (var obj in result.Data)
{
Console.WriteLine($"{obj.Key}: {obj.Size} bytes");
}
Console.WriteLine($"Class A operations: {result.Metrics.ClassAOperations}");
}
}
List All Objects
List all objects in a bucket:
var result = await r2.ListObjectsAsync(
bucketName: "my-bucket",
prefix: null); // null for all objects
foreach (var obj in result.Data)
{
Console.WriteLine($"Key: {obj.Key}");
Console.WriteLine($"Size: {obj.Size} bytes");
Console.WriteLine($"Modified: {obj.LastModified}");
Console.WriteLine($"ETag: {obj.ETag}");
Console.WriteLine();
}
List by Prefix
Filter objects by key prefix:
// List all objects under "documents/"
var result = await r2.ListObjectsAsync("my-bucket", "documents/");
// List all objects under "images/2024/"
var images = await r2.ListObjectsAsync("my-bucket", "images/2024/");
S3Object Properties
The returned S3Object contains:
| Property | Type | Description |
|---|---|---|
Key |
string |
Object key (path) |
Size |
long |
Object size in bytes |
LastModified |
DateTime |
Last modification timestamp |
ETag |
string |
Entity tag (content hash) |
StorageClass |
string |
Storage class (STANDARD, INFREQUENT_ACCESS) |
Owner |
Owner |
Object owner information |
R2Result
Listing returns R2Result<IReadOnlyList<S3Object>>:
var result = await r2.ListObjectsAsync("bucket", "prefix/");
// Access the data
var objects = result.Data;
Console.WriteLine($"Found {objects.Count} objects");
// Access the metrics
var metrics = result.Metrics;
Console.WriteLine($"Class A operations: {metrics.ClassAOperations}");
Pagination
ListObjectsAsync handles pagination for you, fetching every page before it returns:
// This fetches ALL matching objects, regardless of how many pages
var result = await r2.ListObjectsAsync("my-bucket", null);
// Could be thousands of objects
Console.WriteLine($"Total objects: {result.Data.Count}");
Console.WriteLine($"List operations: {result.Metrics.ClassAOperations}"); // Multiple if paginated
One Page at a Time
ListObjectsPageAsync performs a single list request and hands back the continuation token, so your
own code decides whether to ask for the next page. Use it when a bucket holds more keys than you want
to hold in memory at once, or when you are feeding a paged user interface:
var page = await r2.ListObjectsPageAsync(
bucketName: "my-bucket",
prefix: "documents/",
maxKeys: 500, // clamped to the S3 ceiling of 1000
continuationToken: null); // null starts a fresh walk
foreach (var obj in page.Data.Objects)
{
Console.WriteLine($"{obj.Key}: {obj.Size} bytes");
}
if (page.Data.IsTruncated)
{
// Pass this token back on the next call to continue where this page stopped.
var next = await r2.ListObjectsPageAsync(
"my-bucket", "documents/", 500, page.Data.NextContinuationToken);
}
The returned R2ObjectPage carries three members:
| Property | Type | Description |
|---|---|---|
Objects |
IReadOnlyList<S3Object> |
The keys in this page. |
NextContinuationToken |
string? |
The token to pass on the next call. null when this is the last page. |
IsTruncated |
bool |
true when more pages follow. |
Walking a whole prefix by hand looks like this:
public async Task ProcessEveryObjectAsync(string bucket, string? prefix)
{
string? token = null;
do
{
var page = await r2.ListObjectsPageAsync(bucket, prefix, 1000, token);
foreach (var obj in page.Data.Objects)
{
await ProcessAsync(obj);
}
token = page.Data.NextContinuationToken;
}
while (token is not null);
}
maxKeys is clamped to the range 1 to 1000. A value of zero or below requests the maximum, matching
the S3 default, and a value above 1000 is reduced to 1000 because R2 will never return more keys than
that in one response.
Error Handling
try
{
var result = await r2.ListObjectsAsync("bucket", "prefix/");
}
catch (CloudflareR2ListException<S3Object> ex)
{
// Listing failed mid-pagination
Console.WriteLine($"Listing failed: {ex.Message}");
Console.WriteLine($"Partial data retrieved: {ex.PartialData.Count} objects");
Console.WriteLine($"Partial metrics: {ex.PartialMetrics}");
// You can still use the partial data
foreach (var obj in ex.PartialData)
{
Console.WriteLine($" {obj.Key}");
}
}
catch (CloudflareR2OperationException ex)
{
Console.WriteLine($"List operation failed: {ex.Message}");
}
Common Patterns
Get Total Size
public async Task<long> GetTotalSizeAsync(string bucket, string? prefix = null)
{
var result = await r2.ListObjectsAsync(bucket, prefix);
return result.Data.Sum(o => o.Size);
}
Find Files by Extension
public async Task<IReadOnlyList<S3Object>> FindByExtensionAsync(
string bucket, string extension)
{
var result = await r2.ListObjectsAsync(bucket, null);
return result.Data
.Where(o => o.Key.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
.ToList();
}
Get Folder Structure
public async Task<IReadOnlyList<string>> GetFoldersAsync(
string bucket, string? prefix = null)
{
var result = await r2.ListObjectsAsync(bucket, prefix);
// Extract unique folder prefixes
var folders = result.Data
.Select(o => {
var relativePath = prefix != null
? o.Key.Substring(prefix.Length)
: o.Key;
var slashIndex = relativePath.IndexOf('/');
return slashIndex >= 0 ? relativePath.Substring(0, slashIndex) : null;
})
.Where(f => f != null)
.Distinct()
.OrderBy(f => f)
.ToList();
return folders!;
}
Find Large Files
public async Task<IReadOnlyList<S3Object>> FindLargeFilesAsync(
string bucket, long minSizeBytes)
{
var result = await r2.ListObjectsAsync(bucket, null);
return result.Data
.Where(o => o.Size >= minSizeBytes)
.OrderByDescending(o => o.Size)
.ToList();
}
Find Recently Modified
public async Task<IReadOnlyList<S3Object>> FindRecentAsync(
string bucket, TimeSpan maxAge)
{
var cutoff = DateTime.UtcNow - maxAge;
var result = await r2.ListObjectsAsync(bucket, null);
return result.Data
.Where(o => o.LastModified >= cutoff)
.OrderByDescending(o => o.LastModified)
.ToList();
}
Stream Processing for Large Buckets
For very large buckets, process objects as you go:
public async Task ProcessAllObjectsAsync(
string bucket, Func<S3Object, Task> processor)
{
var result = await r2.ListObjectsAsync(bucket, null);
foreach (var obj in result.Data)
{
await processor(obj);
}
}
Export Object List to CSV
public async Task ExportToCsvAsync(string bucket, string csvPath)
{
var result = await r2.ListObjectsAsync(bucket, null);
using var writer = new StreamWriter(csvPath);
await writer.WriteLineAsync("Key,Size,LastModified,ETag");
foreach (var obj in result.Data)
{
await writer.WriteLineAsync(
$"\"{obj.Key}\",{obj.Size},{obj.LastModified:O},{obj.ETag}");
}
}
List Multipart Upload Parts
List parts of an in-progress multipart upload:
var result = await r2.ListPartsAsync(
bucketName: "my-bucket",
objectKey: "large-file.bin",
uploadId: "upload-id-here");
foreach (var part in result.Data)
{
Console.WriteLine($"Part {part.PartNumber}: {part.Size} bytes, ETag: {part.ETag}");
}
R2 Pricing Note
List operations are Class A operations ($4.50 per million). Each page of results counts as one operation.
Related
- Deleting Objects - Delete listed objects
- Downloading Objects - Download found objects
- Multipart Uploads - List multipart parts