Exportar arquivos ao Backblaze B2 em .NET com ferramentas livres
Exporte um arquivo local para um bucket B2 existente com o AWS SDK para .NET, de código aberto. Este exemplo completo de console usa a API compatível com S3 do B2, aceita arquivos de até 64 MiB e lê novamente o objeto armazenado antes de informar o sucesso. Arquivos vazios são entradas válidas.
Introdução ao armazenamento em nuvem Backblaze B2
O B2 oferece uma API nativa e uma API compatível com S3. Aqui usamos a segunda opção, com nomes de buckets S3 e a assinatura de requisições do AWS SDK. Não é necessário ter uma conta AWS.
Configurar uma conta Backblaze B2
Use um bucket privado existente e uma chave de aplicação dedicada, restrita a esse bucket e, se
apropriado, a um prefixo de exportação. Este exemplo precisa de writeFiles
para PUT e de readFiles para a verificação por GET. Anote o endpoint S3 e a
região do bucket. Use o ID da chave de aplicação como chave de acesso e a chave de aplicação
como segredo; um ID de conta não substitui o ID da chave. Use uma chave de aplicação padrão,
em vez da chave mestra, para a API compatível com S3. A Backblaze explica essas restrições no
guia de chaves de aplicação.
Instalar as bibliotecas .NET necessárias
Instale um SDK do .NET 10 com manutenção ativa. Os comandos abaixo usam Bash no Linux e foram testados com o SDK 10.0.111. Mantenha seu SDK atualizado com as correções; o .NET 10 é uma versão LTS. As versões dos pacotes fixam a combinação do AWS SDK v4 testada neste exemplo.
Comece em um diretório com permissão de escrita, fora da árvore de um projeto .NET existente.
Este bloco recusa configurações de projeto herdadas e um diretório B2Export
já existente, e mantém seu shell no diretório original mesmo se a instalação falhar:
(
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
)
Autenticar com a API do Backblaze B2 em .NET
Forneça estas variáveis no ambiente do processo do programa por meio do seu gerenciador de segredos ou da configuração de execução local. Mantenha as chaves de aplicação fora dos arquivos de código-fonte e dos argumentos de comandos.
| Variável | Valor |
|---|---|
B2_ENDPOINT | Seu endpoint regional, como https://s3.us-west-004.backblazeb2.com |
B2_REGION | A região correspondente, como us-west-004 |
B2_BUCKET | Nome do bucket existente |
B2_KEY_ID | ID da chave de aplicação |
B2_APPLICATION_KEY | Chave de aplicação |
B2_OBJECT_KEY | Chave de destino para o comando abaixo, como reports/report #1.txt |
B2_TIMEOUT_SECONDS | Prazo máximo opcional para toda a operação, de 1 a 300 segundos; o padrão é 300 |
As credenciais explícitas abaixo impedem que o SDK busque credenciais em um perfil AWS ou nos metadados da instância. A Backblaze documenta o mapeamento das chaves e a configuração do SDK.
Fazer upload de arquivos para o Backblaze B2 usando C#
Substitua B2Export/Program.cs por este programa completo. A chave do objeto é fornecida
independentemente do nome do arquivo local; passe a chave sem escapes para que o SDK possa
codificar espaços, Unicode e caracteres reservados. Os caminhos locais devem ser UTF-8 válidos e
não devem conter o caractere de substituição �; essa proteção impede
que uma conversão com perda do nome do arquivo selecione um arquivo diferente no 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.");
}
}
Integrar uploads para o Backblaze à sua aplicação
Execute o bloco abaixo no mesmo diretório em que você criou B2Export.
O primeiro comando cria um pequeno arquivo de entrada e se recusa a sobrescrever um
report.txt existente:
(set -C; printf 'example export\n' > report.txt) &&
dotnet run --disable-build-servers --project B2Export -- ./report.txt "${B2_OBJECT_KEY:-}"
Um status de saída zero e Uploaded and verified. significam que o PUT foi concluído e que os bytes baixados corresponderam ao arquivo local. O GET usa o ID de versão retornado quando disponível. A verificação lê blocos de 64 KiB, em vez de manter o arquivo inteiro na memória; isso não impõe um limite numérico aos buffers do próprio SDK. Mantenha o arquivo de entrada inalterado até o processo terminar.
UploadAsync aceita uma instância de IAmazonS3 e um token de
cancelamento, permitindo que uma aplicação reutilize seu cliente e propague o cancelamento da
requisição. Quem chama o método é responsável pelo cliente; o método fecha seu fluxo de arquivo
em caso de sucesso ou falha. UploadAsync, por si só, não realiza a verificação
subsequente.
Solucionar problemas e aplicar boas práticas
O modo de novas tentativas Standard, explicitamente configurado no código, permite até duas novas tentativas por requisição do SDK. O prazo máximo abrange upload, novas tentativas e releitura em conjunto; por isso, mesmo um arquivo válido pode falhar em uma conexão lenta. Ctrl+C, tempo limite excedido, uma recusa do serviço de armazenamento ou uma divergência de bytes resultam em status de saída um. Consulte a documentação do SDK sobre novas tentativas e cancelamento.
Em caso de recusa HTTP, verifique o endpoint, a região correspondente, o ID da chave, o segredo da chave, o bucket, o prefixo e as permissões de leitura e escrita. Os diagnósticos omitem os corpos das respostas do provedor e os segredos. Uma falha no GET pode deixar armazenado um objeto cujo upload foi concluído. Um PUT cancelado também pode ter um resultado remoto incerto: inspecione o bucket antes de tentar novamente, em vez de presumir que nada foi armazenado.
Este exemplo usa requisições PUT em fluxo em vez de uploads multipartes, inclusive para arquivos vazios. Para exportações maiores, use os recursos de upload multiparte do SDK e planeje a limpeza de uploads multipartes abandonados. As configurações de checksum solicitam apenas os checksums que o SDK considera obrigatórios; a releitura explícita verifica os bytes independentemente dos cabeçalhos opcionais de checksum. HTTPS e a assinatura de requisições permanecem habilitados.
Fazer upload novamente para a mesma chave cria outra versão de arquivo no B2. Use uma chave nova para cada exportação se não quiser substituir o objeto. Para remover os dados de teste permanentemente, exclua todas as versões e todos os marcadores de ocultação das respectivas chaves; apenas ocultar o arquivo atual mantém seu histórico. Este programa mantém o objeto exportado e não gerencia essa limpeza.
