Exporting files to Backblaze B2 in .NET using open-source tools
Export a local file to an existing B2 bucket with the open-source AWS SDK for .NET. This complete console example uses B2’s S3-compatible API, accepts files up to 64 MiB, and reads the stored object back before reporting success. Empty files are valid inputs.
Introduction to Backblaze B2 cloud storage
B2 supports a native API and an S3-compatible API. Here we use the latter, with S3 bucket names and the AWS SDK’s request signing. No AWS account is needed.
Setting up a Backblaze B2 account
Use an existing private bucket and a dedicated application key restricted to that bucket and, if
appropriate, an export prefix. This example needs writeFiles for PUT and readFiles for its GET
verification. Record the bucket’s S3 endpoint and region. Use the application key ID as the
access key and the application key
as the secret; an account ID is not a substitute for the key ID. Use a standard application key,
not the master key, for the S3-compatible API. Backblaze explains these restrictions in its
application key guide.
Installing required .NET libraries
Install a maintained .NET 10 SDK. The commands below use Bash on Linux and were tested with SDK 10.0.111. Keep your SDK patched; .NET 10 is an LTS release. The package versions pin this example’s tested AWS SDK v4 combination.
Start in a writable directory outside an existing .NET project tree. This block refuses inherited
project configuration and an existing B2Export directory, and leaves your shell in its original
directory even if installation fails:
(
command -v dotnet >/dev/null || { printf 'Install the .NET 10 SDK first.\n' >&2; exit 1; }
parent="$PWD"
while :; do
for config in "$parent"/global.json "$parent"/[Dd]irectory.[Bb]uild.* \
"$parent"/[Dd]irectory.[Pp]ackages.props "$parent"/[Nn]u[Gg]et.[Cc]onfig; do
if [ -e "$config" ]; then
printf 'Choose a directory outside an existing .NET project tree.\n' >&2
exit 1
fi
done
[ "$parent" = / ] && break
parent="${parent%/*}"
[ -n "$parent" ] || parent=/
done
mkdir B2Export && cd B2Export &&
dotnet new console --framework net10.0 --name B2Export --output . &&
dotnet add package AWSSDK.Core --version 4.0.102.11 &&
dotnet add package AWSSDK.S3 --version 4.0.104.4
)
Authenticating with Backblaze B2 API in .NET
Supply these variables in the program’s process environment through your secret manager or local launch configuration. Keep application keys out of source files and command arguments.
| Variable | Value |
|---|---|
B2_ENDPOINT | Your regional endpoint, such as https://s3.us-west-004.backblazeb2.com |
B2_REGION | The matching region, such as us-west-004 |
B2_BUCKET | Existing bucket name |
B2_KEY_ID | Application key ID |
B2_APPLICATION_KEY | Application key |
B2_OBJECT_KEY | Destination key for the command below, such as reports/report #1.txt |
B2_TIMEOUT_SECONDS | Optional whole-operation deadline, 1–300 seconds; defaults to 300 |
The explicit credentials below keep the SDK from searching an AWS profile or instance metadata. Backblaze documents the key mapping and SDK configuration.
Uploading files to Backblaze B2 using C#
Replace B2Export/Program.cs with this complete program. The object key is supplied independently
of the local filename; pass it unescaped so the SDK can encode spaces, Unicode, and reserved
characters. Local paths must be valid UTF-8 and must not contain the replacement character �;
the guard prevents a lossy filename conversion from selecting a different file on Linux.
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
public static class B2Exporter
{
public static async Task<int> Main(string[] args)
{
using var deadline = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; deadline.Cancel(); };
try
{
if (args.Length != 2) throw new ArgumentException("Pass a local file and an object key.");
if (args[0].Contains('\uFFFD')) throw new ArgumentException("Unsupported local filename.");
var seconds = Environment.GetEnvironmentVariable("B2_TIMEOUT_SECONDS") ?? "300";
if (!int.TryParse(seconds, out var timeout) || timeout < 1 || timeout > 300)
throw new ArgumentException("B2_TIMEOUT_SECONDS must be between 1 and 300.");
deadline.CancelAfter(TimeSpan.FromSeconds(timeout));
var endpoint = new Uri(Required("B2_ENDPOINT"));
if (endpoint.Scheme != "https" || endpoint.UserInfo.Length != 0 ||
endpoint.AbsolutePath != "/" || endpoint.Query.Length != 0 || endpoint.Fragment.Length != 0)
throw new ArgumentException("Use the HTTPS regional endpoint without a bucket or path.");
var config = new AmazonS3Config
{
ServiceURL = endpoint.AbsoluteUri,
AuthenticationRegion = Required("B2_REGION"),
ForcePathStyle = true,
RetryMode = RequestRetryMode.Standard,
MaxErrorRetry = 2,
RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED,
ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED
};
using var client = new AmazonS3Client(
Required("B2_KEY_ID"), Required("B2_APPLICATION_KEY"), config);
var bucket = Required("B2_BUCKET");
var version = await UploadAsync(client, bucket, args[0], args[1], deadline.Token);
await VerifyAsync(client, bucket, args[0], args[1], version, deadline.Token);
Console.WriteLine("Uploaded and verified.");
return 0;
}
catch (OperationCanceledException) { Console.Error.WriteLine("Upload canceled or timed out."); }
catch (AmazonS3Exception error) { Console.Error.WriteLine($"Storage returned HTTP {(int)error.StatusCode}."); }
catch (Exception) { Console.Error.WriteLine("Upload failed. Check the file and storage configuration."); }
return 1;
}
private static string Required(string name) =>
Environment.GetEnvironmentVariable(name) is { Length: > 0 } value
? value : throw new ArgumentException($"Missing {name}.");
public static async Task<string?> UploadAsync(IAmazonS3 client, string bucket, string path,
string key, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("An object key is required.");
await using var input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
if (input.Length > 64L * 1024 * 1024) throw new IOException("This example accepts at most 64 MiB.");
var uploaded = await client.PutObjectAsync(new PutObjectRequest
{
BucketName = bucket,
Key = key,
InputStream = input,
AutoCloseStream = false,
ContentType = "application/octet-stream",
UseChunkEncoding = false
}, cancellationToken);
return uploaded.VersionId;
}
private static async Task VerifyAsync(IAmazonS3 client, string bucket, string path,
string key, string? version, CancellationToken cancellationToken)
{
using var stored = await client.GetObjectAsync(new GetObjectRequest
{
BucketName = bucket,
Key = key,
VersionId = version
}, cancellationToken);
await using var input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
if (stored.ContentLength != input.Length) throw new IOException("Stored length differs.");
var local = new byte[64 * 1024];
var remote = new byte[64 * 1024];
int count;
while ((count = await input.ReadAsync(local, cancellationToken)) != 0)
{
await stored.ResponseStream.ReadExactlyAsync(remote.AsMemory(0, count), cancellationToken);
if (!local.AsSpan(0, count).SequenceEqual(remote.AsSpan(0, count)))
throw new IOException("Stored bytes differ.");
}
if (await stored.ResponseStream.ReadAsync(remote.AsMemory(0, 1), cancellationToken) != 0)
throw new IOException("Stored object has extra bytes.");
}
}
Integrating Backblaze uploads into your application
Run this from the same directory where you created B2Export. The first command creates a small
input and refuses to overwrite an existing report.txt:
(set -C; printf 'example export\n' > report.txt) &&
dotnet run --disable-build-servers --project B2Export -- ./report.txt "${B2_OBJECT_KEY:-}"
An exit status of zero and Uploaded and verified. mean that PUT completed and the downloaded bytes matched the local file. The GET uses the returned version ID when available. Verification reads in 64 KiB chunks rather than holding the whole file in memory; this does not impose a numeric bound on the SDK’s own buffers. Leave the input unchanged until the process exits.
UploadAsync accepts an IAmazonS3 instance and cancellation token, so an application can reuse
its client and propagate request cancellation. The caller owns the client; the method closes its
file stream on success and failure. UploadAsync alone does not perform the subsequent verification.
Troubleshooting and best practices
The configured standard retry policy allows up to two retries per SDK request. The deadline covers upload, retries, and readback together, so a valid file can still fail on a slow connection. Ctrl+C, timeout, a storage refusal, or a byte mismatch returns status one. See the SDK’s retry and cancellation documentation.
For an HTTP refusal, check the endpoint, matching region, key ID, key secret, bucket, prefix, and read/write capabilities. Diagnostics omit provider response bodies and secrets. A failed GET can leave a successful upload behind. A canceled PUT can also have an uncertain remote outcome: inspect the bucket before retrying rather than assuming nothing was stored.
This example uses streamed PUT requests rather than multipart uploads, including for empty files. For larger exports, use the SDK’s multipart facilities and plan for abandoned multipart-upload cleanup. The checksum settings request only checksums the SDK considers required; the explicit readback checks the bytes independently of optional checksum headers. HTTPS and request signing remain enabled.
Uploading to the same key again creates another B2 file version. Use a fresh key for each export if replacement is unintended. To remove test data permanently, delete every version and hide marker for its keys; hiding the current file alone leaves its history. This program retains the exported object and does not manage that cleanup.
