Exportar arquivos para o DigitalOcean Spaces em .NET (C#)
Use o AWS SDK for .NET para fazer upload de um arquivo local para um Space privado da DigitalOcean. Depois, faça o download e compare seu tamanho e hash SHA-256. O programa abaixo aceita arquivos vazios e arquivos de até 64 MiB e só informa sucesso após a verificação.
Entender a compatibilidade do DigitalOcean Spaces com S3
O Spaces implementa um subconjunto da API S3.
Para um Space existente, configure explicitamente seu endpoint regional, a região de assinatura e as
credenciais do Spaces. Este exemplo usa requisições com endereçamento por caminho, então o endpoint
contém a região, mas não o nome do bucket: https://fra1.digitaloceanspaces.com.
Você precisa de um Space privado existente e de uma chave do Spaces que possa gravar e ler seus objetos. Para um bucket sem uma política de bucket, a DigitalOcean permite usar uma chave restrita ao bucket com permissões Read/Write/Delete. Chaves limitadas não podem ser combinadas com políticas de bucket; consulte o gerenciamento de acesso do Spaces. Um token da API da DigitalOcean não é o par de chaves do Spaces usado por este programa. Este guia pressupõe um bucket privado sem uma política de leitura pública.
Instalar os pacotes NuGet necessários
Os comandos de shell usam Bash no Linux. Instale o SDK do .NET 10 atual, em vez de apenas o runtime. Este exemplo foi testado com o SDK 10.0.402 e o runtime 10.0.13. As versões de pacotes abaixo fixam versões do AWS SDK v4 para uma configuração reproduzível; são versões testadas, não versões mínimas exigidas pelo funcionamento do exemplo.
Comece em um diretório com permissão de escrita, fora de um projeto .NET existente. O bloco recusa
arquivos SpacesExport já existentes e configurações em diretórios ancestrais que possam
alterar a seleção do SDK, as compilações ou a instalação de pacotes. Se a configuração for concluída
com sucesso, o shell entra no novo projeto; em caso de falha, permanece no diretório original.
(
command -v dotnet >/dev/null 2>&1 || { printf 'Install the .NET 10 SDK first.\n' >&2; exit 1; }
parent=$PWD
while :; do
for config in "$parent/global.json" "$parent"/Directory.Build.* \
"$parent/Directory.Packages.props" "$parent"/[Nn][Uu][Gg][Ee][Tt].[Cc][Oo][Nn][Ff][Ii][Gg]; do
if [ -e "$config" ]; then
printf 'Use a directory outside existing .NET configuration.\n' >&2
exit 1
fi
done
[ "$parent" = / ] && break
parent=${parent%/*}
[ -n "$parent" ] || parent=/
done
[ ! -e SpacesExport ] || { printf 'SpacesExport already exists.\n' >&2; exit 1; }
dotnet new console --framework net10.0 --name SpacesExport &&
cd SpacesExport &&
dotnet add package AWSSDK.Core --version 4.0.102.10 &&
dotnet add package AWSSDK.S3 --version 4.0.104.3
) && cd SpacesExport
Se a instalação de pacotes falhar após a criação do projeto, mantenha esse projeto. Corrija o problema informado de conexão ou de fonte do NuGet e execute novamente estes comandos a partir do diretório original:
dotnet add ./SpacesExport/SpacesExport.csproj package AWSSDK.Core --version 4.0.102.10 &&
dotnet add ./SpacesExport/SpacesExport.csproj package AWSSDK.S3 --version 4.0.104.3
Após uma nova tentativa bem-sucedida, entre em SpacesExport antes de continuar.
Usar o AWS SDK for .NET com o DigitalOcean Spaces
Forneça estas variáveis de ambiente por meio do seu gerenciador de segredos ou do ambiente do shell. A chave e o segredo devem pertencer ao Spaces, não à AWS.
| Variável | Valor |
|---|---|
SPACES_ENDPOINT | Endpoint HTTPS regional, como https://fra1.digitaloceanspaces.com |
SPACES_REGION | Região que contém o Space existente, como fra1 |
SPACES_BUCKET | Nome do Space existente |
SPACES_KEY_ID | ID da chave de acesso do Spaces |
SPACES_APPLICATION_KEY | Chave secreta do Spaces |
O guia do SDK da DigitalOcean mostra como selecionar o endpoint.
Passe as chaves de objeto como strings sem codificação de URL. O SDK assina a requisição e codifica
espaços, #, ?, +,
sinais de porcentagem e caracteres Unicode.
Substitua Program.cs por este programa completo. Use arquivos locais regulares e
nomes UTF-8 válidos. O programa rejeita o caractere de substituição � nos
argumentos para impedir que um nome de arquivo nativo inválido seja resolvido silenciosamente como
outro arquivo. Mantenha o arquivo local inalterado até a conclusão da verificação e use uma chave de
destino na qual nenhum outro processo esteja gravando.
using System.Security.Cryptography;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
public static class SpacesExporter
{
private const long MaxBytes = 64L * 1024 * 1024;
public static async Task<int> Main(string[] args)
{
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(5));
Console.CancelKeyPress += (_, e) => { e.Cancel = true; deadline.Cancel(); };
try
{
if (args.Length is not (2 or 3) || (args.Length == 3 && args[2] != "--verify-only"))
throw new ArgumentException("Pass a local file, an object key, and optionally --verify-only.");
if (args.Any(value => value.Contains('\uFFFD')))
throw new ArgumentException("Use UTF-8 arguments without the replacement character.");
if (string.IsNullOrWhiteSpace(args[1]))
throw new ArgumentException("An object key is required.");
var endpoint = new Uri(Required("SPACES_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("SPACES_REGION"),
ForcePathStyle = true,
MaxErrorRetry = 2,
RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED,
ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED
};
using var client = new AmazonS3Client(
Required("SPACES_KEY_ID"), Required("SPACES_APPLICATION_KEY"), config);
var bucket = Required("SPACES_BUCKET");
await using var input = new FileStream(args[0], FileMode.Open, FileAccess.Read, FileShare.Read);
if (input.Length > MaxBytes)
{
Console.Error.WriteLine("File exceeds the 64 MiB limit.");
return 1;
}
var expectedHash = await SHA256.HashDataAsync(input, deadline.Token);
if (args.Length == 2)
await UploadAsync(client, bucket, args[0], args[1], deadline.Token);
using var stored = await client.GetObjectAsync(bucket, args[1], deadline.Token);
if (stored.ContentLength != input.Length)
throw new InvalidDataException();
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
var buffer = new byte[64 * 1024];
long received = 0;
int count;
while ((count = await stored.ResponseStream.ReadAsync(buffer, deadline.Token)) != 0)
{
received += count;
if (received > input.Length) throw new InvalidDataException();
hash.AppendData(buffer, 0, count);
}
if (received != input.Length || !hash.GetHashAndReset().SequenceEqual(expectedHash))
throw new InvalidDataException();
Console.WriteLine($"Verified {received} bytes.");
return 0;
}
catch (OperationCanceledException) { Console.Error.WriteLine("Transfer canceled or timed out."); }
catch (AmazonS3Exception error) { Console.Error.WriteLine($"Storage returned HTTP {(int)error.StatusCode}."); }
catch (InvalidDataException) { Console.Error.WriteLine("Stored bytes do not match the local file."); }
catch (Exception) { Console.Error.WriteLine("Transfer failed. Check the file, arguments, 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 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 > MaxBytes) throw new IOException("File exceeds the 64 MiB limit.");
await client.PutObjectAsync(new PutObjectRequest
{
BucketName = bucket,
Key = key,
InputStream = input,
AutoCloseStream = false,
ContentType = "application/octet-stream",
UseChunkEncoding = false
}, cancellationToken);
}
}
No diretório do projeto, compile e exporte um arquivo que você já tenha:
dotnet build && dotnet run --no-build -- './report.txt' 'reports/report #1.txt'
Substitua os dois argumentos pelo caminho local e pela chave de destino. O
-- separa as opções da CLI do .NET dos argumentos do programa, incluindo
nomes de arquivo que começam com -. Uma execução bem-sucedida imprime
Verified, seguido da quantidade de bytes baixados,
e termina com status zero. O arquivo permanece no Spaces. Reutilizar a mesma chave substitui seu
conteúdo atual; escolha uma nova chave para manter uma exportação anterior.
Adicionar lógica de novas tentativas e tratamento de erros
MaxErrorRetry = 2 permite no máximo duas novas tentativas do SDK por requisição para
falhas que admitem novas tentativas. O SDK
aceita um token de cancelamento para chamadas assíncronas.
Este programa define um único prazo de cinco minutos para o cálculo do hash local, o upload, as novas
tentativas e a verificação em conjunto. Mesmo um arquivo com menos de 64 MiB pode exceder esse prazo
em uma conexão lenta. Ctrl+C solicita o cancelamento pelo mesmo token. Nenhum dos casos imprime um
resultado verificado, e ambos encerram o programa com status diferente de zero.
Um upload pode ter chegado ao Spaces antes de uma resposta se perder ou de uma verificação posterior falhar. O programa não exclui o objeto remoto em caso de falha. Verifique o conteúdo dessa chave em relação ao arquivo local inalterado sem fazer upload novamente:
dotnet build && dotnet run --no-build -- './report.txt' 'reports/report #1.txt' --verify-only
Este modo apenas faz o download e compara; ele retorna um status diferente de zero se o conteúdo estiver ausente ou for diferente. Se a verificação for bem-sucedida, o objeto armazenado no momento corresponde ao seu arquivo. Caso contrário, resolva a falha antes de decidir se fará upload novamente. A verificação é uma observação do objeto atual, não um bloqueio contra gravações de outro processo.
Transferir arquivos de tamanho limitado em fluxo
O upload envia uma única requisição PUT a partir de um fluxo de leitura de arquivo aberto. A verificação lê a resposta por meio de um buffer de 64 KiB e verifica o tamanho completo e o hash, inclusive para arquivos vazios. Ela faz o download do arquivo uma vez, portanto inclua essa transferência adicional ao estimar o tempo e a largura de banda. O limite de 64 MiB é uma restrição da aplicação deste exemplo, não o limite de tamanho de objeto do Spaces.
A configuração desativa a codificação de transferência em blocos no upload e solicita as somas de verificação opcionais do SDK somente quando uma operação as exige. Essas são as configurações usadas neste fluxo com o Spaces; a releitura explícita fornece a verificação de conteúdo de ponta a ponta. O programa libera seus fluxos, a resposta e o cliente tanto em caso de sucesso quanto de falha.
Este exemplo não realiza uploads em múltiplas partes. Arquivos maiores precisam de um fluxo de trabalho separado para uploads em múltiplas partes, incluindo recuperação e limpeza de uploads inacabados; aumentar essa constante não fornece esse fluxo.
Gerenciar chaves de acesso e permissões com segurança
Mantenha as credenciais na configuração do lado do servidor. Não coloque o segredo em uma URL, em um arquivo de código-fonte, em um bundle para navegador nem em um log capturado do tráfego do SDK. Este programa fornece o par de chaves explicitamente e usa HTTPS; ele não torna o Space público nem altera as permissões do bucket. Limite a saída de diagnóstico ao status e às mensagens do próprio programa.
Resolver problemas comuns
Para Storage returned HTTP 403., verifique se você forneceu o par de chaves do Spaces, se ele tem acesso de leitura e escrita a esse bucket e se o endpoint e a região de assinatura correspondem à localização dele. Um PUT bem-sucedido seguido de um GET negado ainda resulta em falha na verificação.
Para File exceeds the 64 MiB limit., use um arquivo menor ou uma implementação separada de upload em múltiplas partes. Para Stored bytes do not match the local file., verifique se o arquivo local ou o destino mudou durante a operação e use o modo de somente verificação com o par pretendido. Uma conexão recusada ou uma falha HTTP transitória que persista após o esgotamento das tentativas resulta em um status de saída diferente de zero; verifique a conectividade antes de tentar novamente. Não trate uma execução com falha como evidência de que nenhum objeto foi armazenado.
