Exportar archivos a Backblaze B2 en .NET con código abierto
Exporta un archivo local a un bucket B2 existente con el SDK de AWS para .NET, de código abierto. Este ejemplo completo de consola usa la API compatible con S3 de B2, acepta archivos de hasta 64 MiB y vuelve a leer el objeto almacenado antes de indicar que la operación se completó correctamente. Los archivos vacíos son entradas válidas.
Introducción al almacenamiento en la nube de Backblaze B2
B2 admite una API nativa y una API compatible con S3. Aquí usamos la segunda, con nombres de bucket de S3 y la firma de solicitudes del SDK de AWS. No se necesita una cuenta de AWS.
Configura una cuenta de Backblaze B2
Usa un bucket privado existente y una clave de aplicación dedicada, restringida a ese bucket y,
si corresponde, a un prefijo de exportación. Este ejemplo necesita writeFiles para PUT y readFiles para su verificación mediante GET.
Anota el endpoint de S3 del bucket y su región. Usa el ID de clave de la aplicación como
clave de acceso y la clave de aplicación como secreto; un ID de cuenta no sustituye al ID de
clave. Usa una clave de aplicación estándar, en lugar de la clave maestra, para la API compatible
con S3. Backblaze explica estas restricciones en su
guía de claves de aplicación.
Instala las bibliotecas .NET necesarias
Instala un SDK de .NET 10 que siga recibiendo mantenimiento. Los siguientes comandos usan Bash en Linux y se probaron con el SDK 10.0.111. Mantén tu SDK actualizado con los parches disponibles; .NET 10 es una versión LTS. Las versiones fijadas de los paquetes corresponden a la combinación del SDK de AWS v4 probada para este ejemplo.
Comienza en un directorio con permisos de escritura, fuera del árbol de un proyecto .NET existente.
Este bloque rechaza la configuración de proyecto heredada y un directorio
B2Export existente, y deja tu shell en su directorio original aunque la instalación falle:
(
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
)
Autentícate con la API de Backblaze B2 en .NET
Proporciona estas variables en el entorno del proceso del programa mediante tu gestor de secretos o la configuración de inicio local. Mantén las claves de aplicación fuera de los archivos de código fuente y de los argumentos de los comandos.
| Variable | Valor |
|---|---|
B2_ENDPOINT | Tu endpoint regional, como https://s3.us-west-004.backblazeb2.com |
B2_REGION | La región correspondiente, como us-west-004 |
B2_BUCKET | Nombre del bucket existente |
B2_KEY_ID | ID de clave de aplicación |
B2_APPLICATION_KEY | Clave de aplicación |
B2_OBJECT_KEY | Clave de destino para el siguiente comando, como reports/report #1.txt |
B2_TIMEOUT_SECONDS | Plazo máximo opcional para toda la operación, de 1–300 segundos; el valor predeterminado es 300 |
Las credenciales explícitas que se muestran a continuación evitan que el SDK busque en un perfil de AWS o en los metadatos de la instancia. Backblaze documenta la correspondencia de claves y la configuración del SDK.
Sube archivos a Backblaze B2 con C#
Reemplaza B2Export/Program.cs por este programa completo. La clave del objeto se proporciona
independientemente del nombre del archivo local; pásala sin escapar para que el SDK pueda codificar
los espacios, Unicode y los caracteres reservados. Las rutas locales deben ser UTF-8 válido y no
deben contener el carácter de sustitución �;
esta validación evita que una conversión con pérdida del nombre seleccione un archivo diferente
en 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.");
}
}
Integra las subidas a Backblaze en tu aplicación
Ejecuta esto desde el mismo directorio donde creaste B2Export. El primer comando crea
un archivo de entrada pequeño y se niega a sobrescribir un report.txt existente:
(set -C; printf 'example export\n' > report.txt) &&
dotnet run --disable-build-servers --project B2Export -- ./report.txt "${B2_OBJECT_KEY:-}"
Un código de salida de cero y Uploaded and verified. indican que PUT se completó y que los bytes descargados coincidieron con el archivo local. GET usa el ID de versión devuelto cuando está disponible. La verificación lee bloques de 64 KiB en lugar de mantener todo el archivo en memoria; esto no impone un límite numérico a los búferes del propio SDK. Mantén el archivo de entrada sin cambios hasta que el proceso termine.
UploadAsync acepta una instancia de IAmazonS3 y un token de cancelación,
por lo que una aplicación puede reutilizar su cliente y propagar la cancelación de solicitudes.
El código que realiza la llamada es responsable del cliente; el método cierra su flujo de archivo
tanto si la operación tiene éxito como si falla. UploadAsync por sí solo no realiza
la verificación posterior.
Solución de problemas y buenas prácticas
La política estándar de reintentos configurada permite hasta dos reintentos por solicitud del SDK. El plazo máximo abarca conjuntamente la subida, los reintentos y la lectura de verificación, por lo que un archivo válido puede fallar si la conexión es lenta. Ctrl+C, el agotamiento del tiempo de espera, un rechazo del servicio de almacenamiento o una discrepancia de bytes devuelven el código uno. Consulta la documentación sobre reintentos y cancelación del SDK.
Ante un rechazo HTTP, revisa el endpoint, la región correspondiente, el ID de clave, el secreto de la clave, el bucket, el prefijo y los permisos de lectura y escritura. Los diagnósticos omiten los cuerpos de respuesta del proveedor y los secretos. Si GET falla, puede quedar almacenado el archivo que se subió correctamente. Un PUT cancelado también puede tener un resultado remoto incierto: inspecciona el bucket antes de reintentarlo, sin dar por hecho que no se almacenó nada.
Este ejemplo usa solicitudes PUT en streaming en lugar de subidas multiparte, incluso para archivos vacíos. Para exportaciones más grandes, usa las funciones de subida multiparte del SDK y planifica la limpieza de las subidas multiparte abandonadas. La configuración de sumas de verificación solicita únicamente las que el SDK considera obligatorias; la lectura de verificación explícita comprueba los bytes independientemente de los encabezados opcionales de sumas de verificación. HTTPS y la firma de solicitudes permanecen habilitados.
Volver a subir un archivo a la misma clave crea otra versión de archivo de B2. Usa una clave nueva para cada exportación si no deseas reemplazar el archivo. Para eliminar los datos de prueba de forma permanente, elimina todas las versiones y todos los marcadores de ocultación asociados a sus claves; ocultar solo el archivo actual conserva su historial. Este programa conserva el objeto exportado y no gestiona esa limpieza.
