Table of Contents

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.