Exportação fácil de arquivos para OpenStack Swift em .NET C#
Faça upload de arquivos pela API HTTP do Swift com o HttpClient integrado ao .NET.
Este exemplo cria segmentos reais e publica um manifesto de objeto grande estático somente após a
confirmação de todos os segmentos.
Introdução ao OpenStack Swift
O Swift armazena objetos em contêineres. Sua API de objetos grandes estáticos combina uma lista ordenada de segmentos enviados em um único objeto disponível para download. A implantação precisa ter o middleware SLO habilitado para este exemplo.
Configurar seu ambiente
Pré-requisitos
Instale uma versão com manutenção ativa do SDK do .NET 10. O exemplo foi testado no Bash em Linux com o SDK 10.0.111, o runtime 10.0.11 e o Swift 2.38.2. Obtenha um token com escopo de projeto e a URL da conta de armazenamento de objetos pelo fluxo de trabalho de autenticação do OpenStack da sua implantação. O token precisa de acesso para criar contêineres, fazer upload de objetos, ler metadados dos segmentos para validação de SLO e excluir segmentos de uploads que falharam.
O exemplo usa segmentos de 5 MiB e no máximo 1.000 segmentos (5.242.880.000 bytes, incluindo esse
limite). Confirme se os limites de SLO, tamanho de objeto e cota da sua implantação permitem esses
valores. O upload inteiro tem um prazo de cinco minutos, por isso, em uma conexão mais lenta, ele
pode falhar mesmo abaixo do limite de tamanho.
Use caminhos locais e nomes de diretórios de projeto em ASCII. O programa impõe essa restrição antes
de abrir o arquivo para que bytes inválidos no nome do arquivo não levem à seleção de outro arquivo
após a decodificação dos argumentos.
As chaves de objetos remotos podem conter Unicode.
Comece em um diretório com permissão de escrita e sem uma entrada SwiftExport.
As configurações do SDK do .NET, do NuGet e do MSBuild definidas nos diretórios superiores continuam
sendo aplicadas.
if ! command -v dotnet >/dev/null 2>&1; then
printf '%s\n' 'Install the .NET 10 SDK first.' >&2
false
elif [ -e SwiftExport ] || [ -L SwiftExport ]; then
printf '%s\n' 'SwiftExport already exists; choose another parent directory.' >&2
false
else
dotnet new console --framework net10.0 --name SwiftExport --no-restore &&
dotnet restore SwiftExport/SwiftExport.csproj &&
cd SwiftExport
fi
Se a configuração for bem-sucedida, o shell entra no diretório do projeto. Se a restauração falhar,
o novo projeto permanece no local e o shell continua no diretório pai. Corrija o problema informado
do SDK ou do NuGet e execute dotnet restore SwiftExport/SwiftExport.csproj && cd SwiftExport nesse diretório para concluir a
configuração.
Não são necessários pacotes NuGet adicionais.
Gerenciamento seguro de configurações
Defina SWIFT_STORAGE_URL com o endpoint HTTPS completo da conta informado no catálogo de
serviços, incluindo seu caminho /v1/AUTH_…. Defina SWIFT_TOKEN
com o token com escopo de projeto. Forneça esses valores pelo shell ou pelo gerenciador de segredos.
A obtenção e a renovação do token fazem parte do fluxo de trabalho do Keystone da sua implantação.
Um token expirado faz o programa terminar com código de saída diferente de zero; obtenha um novo
token com escopo de projeto antes de executar novamente.
Implementar operações de armazenamento no Swift
Substitua Program.cs por este programa completo.
O HttpClient injetado pertence ao código chamador; o exportador libera os
recursos de cada requisição e resposta. Os redirecionamentos estão desabilitados para que o token
não seja encaminhado a outro destino.
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text.Json;
public static class Program
{
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 != 3) throw new ArgumentException("Pass container, object key, and local file.");
if (args[2].Any(character => character > 127) ||
Directory.GetCurrentDirectory().Any(character => character > 127))
throw new ArgumentException("Use ASCII local paths and working directory names.");
var endpoint = new Uri(Required("SWIFT_STORAGE_URL"));
if (endpoint.Scheme != "https" || endpoint.UserInfo.Length != 0 ||
endpoint.Query.Length != 0 || endpoint.Fragment.Length != 0)
throw new ArgumentException("Use the HTTPS account endpoint from the service catalog.");
using var handler = new SocketsHttpHandler { AllowAutoRedirect = false, MaxConnectionsPerServer = 4 };
using var http = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
var exporter = new SwiftExporter(http, endpoint, Required("SWIFT_TOKEN"));
await exporter.UploadAsync(args[0], args[1], args[2], deadline.Token);
Console.WriteLine("Uploaded.");
return 0;
}
catch (OperationCanceledException) { Console.Error.WriteLine("Upload canceled or timed out."); }
catch (HttpRequestException error) { Console.Error.WriteLine($"Swift request failed (HTTP {error.StatusCode})."); }
catch (Exception) { Console.Error.WriteLine("Upload failed. Check the file and Swift configuration."); }
return 1;
}
private static string Required(string name) =>
Environment.GetEnvironmentVariable(name) is { Length: > 0 } value
? value : throw new ArgumentException($"Missing {name}.");
}
public sealed class SwiftExporter(HttpClient http, Uri endpoint, string token)
{
private const int SegmentSize = 5 * 1024 * 1024;
private const int MaxSegments = 1000;
private static string Encode(string value)
{
var parts = value.Split('/');
if (parts.Any(part => part.Length == 0 || part == "." || part == ".."))
throw new ArgumentException("Use nonempty path segments other than dot or dot-dot.");
return string.Join("/", parts.Select(Uri.EscapeDataString));
}
private async Task<string?> PutAsync(string path, HttpContent content,
bool container, CancellationToken ct, string? etag = null, string query = "")
{
content.Headers.ContentType ??= new MediaTypeHeaderValue("application/octet-stream");
using var request = new HttpRequestMessage(HttpMethod.Put,
endpoint.AbsoluteUri.TrimEnd('/') + "/" + Encode(path) + query) { Content = content };
request.Headers.Add("X-Auth-Token", token);
if (etag != null) request.Headers.Add("ETag", etag);
using var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
var status = (int)response.StatusCode;
if (status != 201 && !(container && status == 202))
throw new HttpRequestException("Unexpected Swift response.", null, response.StatusCode);
// Swift can return an unquoted MD5 that the typed ETag accessor cannot parse.
return response.Headers.TryGetValues("ETag", out var values)
? values.Single().Trim('"') : null;
}
public async Task UploadAsync(string container, string key, string file, CancellationToken ct)
{
if (container.Contains('/')) throw new ArgumentException("A container cannot contain slashes.");
Encode(container);
Encode(key);
await using var input = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
long length = input.Length;
if (length > (long)SegmentSize * MaxSegments) throw new IOException("File exceeds the example limit.");
await PutAsync(container, new ByteArrayContent([]), true, ct);
if (length == 0)
{
await PutAsync(container + "/" + key, new ByteArrayContent([]), false, ct);
return;
}
string segments = container + "_segments";
await PutAsync(segments, new ByteArrayContent([]), true, ct);
string version = Guid.NewGuid().ToString("N");
var manifest = new List<object>();
var attempted = new List<string>();
bool publicationStarted = false;
var buffer = new byte[SegmentSize];
long remaining = length;
try
{
for (int index = 0; remaining > 0; index++)
{
int count = (int)Math.Min(remaining, SegmentSize);
await input.ReadExactlyAsync(buffer.AsMemory(0, count), ct);
string hash = Convert.ToHexString(MD5.HashData(buffer.AsSpan(0, count))).ToLowerInvariant();
string name = $"{segments}/{version}/{index:D8}";
attempted.Add(name);
using var content = new ByteArrayContent(buffer, 0, count);
string? returned = await PutAsync(name, content, false, ct, hash);
if (!string.Equals(hash, returned, StringComparison.OrdinalIgnoreCase))
throw new IOException("Swift did not confirm the segment checksum.");
manifest.Add(new { path = "/" + name, etag = hash, size_bytes = count });
remaining -= count;
}
if (input.Length != length || input.ReadByte() != -1) throw new IOException("The input file changed.");
using var manifestContent = new StringContent(JsonSerializer.Serialize(manifest));
manifestContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
// After dispatch, a lost response cannot tell us whether these segments are live.
publicationStarted = true;
await PutAsync(container + "/" + key, manifestContent, false, ct,
query: "?multipart-manifest=put");
}
catch
{
if (!publicationStarted) await RemoveAttemptedAsync(attempted);
throw;
}
}
private async Task RemoveAttemptedAsync(List<string> paths)
{
using var cleanup = new CancellationTokenSource(TimeSpan.FromSeconds(15));
foreach (string path in paths)
{
try
{
using var request = new HttpRequestMessage(HttpMethod.Delete,
endpoint.AbsoluteUri.TrimEnd('/') + "/" + Encode(path));
request.Headers.Add("X-Auth-Token", token);
using var response = await http.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead, cleanup.Token);
if ((int)response.StatusCode != 204 && (int)response.StatusCode != 404)
Console.Error.WriteLine("An unreferenced segment could not be removed.");
}
catch (HttpRequestException) { Console.Error.WriteLine("Segment cleanup lost its connection."); }
catch (OperationCanceledException) { Console.Error.WriteLine("Segment cleanup timed out."); break; }
}
}
}
Lidar com arquivos grandes
Cada arquivo não vazio recebe um novo prefixo de segmentos em <container>_segments.
Os uploads sequenciais mantêm o buffer de dados em 5 MiB. O manifesto contém o caminho, o ETag e o
tamanho de cada segmento, que o Swift valida antes de publicar o objeto. Para um arquivo vazio, o
programa usa um PUT vazio comum, sem depender do suporte a manifestos SLO vazios, que varia conforme
a implantação.
A chave do objeto remoto pode conter espaços e Unicode. A codificação preserva os separadores de caminho e rejeita segmentos vazios e os segmentos “.” e “..” para evitar a normalização de URI. Mantenha o arquivo local inalterado durante todo o upload; as verificações de tamanho não conseguem detectar todas as alterações simultâneas que mantêm o mesmo tamanho.
Implementar um tratamento robusto de erros
A criação de contêineres aceita HTTP 201 ou 202; os uploads de objetos e manifestos exigem 201. O checksum do segmento é comparado com o cabeçalho ETag bruto do Swift, inclusive em implantações que omitem as aspas. Falhas de cota, autenticação, checksum, conexão e cancelamento encerram o programa com código de saída diferente de zero. Os cancelamentos por Ctrl+C e pelo prazo de cinco minutos são propagados para as leituras de arquivos e as operações HTTP. Este exemplo não faz novas tentativas.
Antes do envio do manifesto, um upload que falha tenta excluir somente seus próprios segmentos, usando os nomes exatos. A limpeza tem um prazo separado de 15 segundos, portanto o cancelamento não a impede; exclusões que falham ainda podem deixar segmentos órfãos. O objeto existente continua disponível até que seu substituto seja publicado.
Após o início do envio do manifesto, o programa mantém os novos segmentos em caso de falha. Uma resposta perdida pode ocultar uma publicação bem-sucedida, portanto excluir esses segmentos poderia danificar o objeto. Inspecione o manifesto atual antes de tentar novamente ou remover versões sem referências. Uma substituição usa um novo prefixo e uma lista explícita de segmentos, impedindo que um arquivo menor incorpore uma parte final antiga. Os segmentos antigos permanecem após a substituição; remova somente versões que pertençam a você e que não tenham mais referências nem leitores ativos. Este exemplo não implementa esse fluxo de trabalho de manutenção.
Otimização de desempenho
Reutilize o cliente para um lote de exportações. O limite de conexões é controlado por
SocketsHttpHandler.
Se adicionar uploads simultâneos, limite a concorrência e reserve um buffer de segmento por operação.
Exemplo prático: implementação completa
Com as duas variáveis de ambiente configuradas, coloque o arquivo que deseja exportar em
report.bin no diretório do projeto. Este comando cria ou substitui o objeto
remoto na chave indicada:
dotnet run -- exports 'reports/report #1.bin' report.bin
O comando cria exports e exports_segments se necessário, faz
upload dos segmentos e, por fim, publica reports/report #1.bin. Uma entrada vazia cria um
objeto vazio comum em vez disso.
O comando imprime Uploaded. e termina com
código de saída zero somente após a resposta final bem-sucedida. Faça o download do objeto pela
API GET autenticada comum do Swift para comparar seus bytes com report.bin;
o ETag SLO de um objeto não vazio não é o MD5 do arquivo completo.
