Exportación sin esfuerzo de archivos a OpenStack Swift en .NET C#
Sube archivos mediante la API HTTP de Swift con HttpClient, integrado en .NET.
Este ejemplo crea segmentos reales y publica un manifiesto estático de objetos grandes solo después
de recibir la confirmación de cada segmento.
Introducción a OpenStack Swift
Swift almacena objetos en contenedores. Su API de objetos grandes estáticos combina una lista ordenada de segmentos subidos en un único objeto descargable. Tu despliegue debe tener habilitado el middleware SLO para este ejemplo.
Configuración del entorno
Requisitos previos
Instala el SDK de .NET 10. Obtén un token válido con ámbito definido y la URL de la cuenta de almacenamiento de objetos mediante tu flujo de autenticación de OpenStack. Tu cuenta necesita permisos para crear contenedores y objetos. El ejemplo utiliza segmentos de 5 MiB y un máximo de 1.000 segmentos (unos 4,88 GiB); confirma que los límites de SLO, tamaño de objetos y cuota de tu despliegue permitan estos valores.
dotnet new console --framework net10.0 --name SwiftExport
cd SwiftExport
Gestión segura de la configuración
Asigna a SWIFT_STORAGE_URL el endpoint HTTPS completo de la cuenta que figura en el
catálogo de servicios, incluida su ruta /v1/AUTH_…. Asigna a
SWIFT_TOKEN el token con ámbito definido. Proporciona ambos valores mediante tu
shell o gestor de secretos. La obtención y renovación del token corresponden al flujo de Keystone
de tu despliegue; el programa termina si el token ha caducado, en lugar de asumir que un reintento
puede renovarlo.
Implementación de operaciones de almacenamiento en Swift
Reemplaza Program.cs por este programa completo. El código que llama al exportador
es responsable del HttpClient inyectado; el exportador libera los recursos de cada
solicitud y respuesta. Las redirecciones están deshabilitadas para que el token no se reenvíe a un
destino diferente.
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.");
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);
return response.Headers.ETag?.Tag.Trim('"');
}
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 buffer = new byte[SegmentSize];
long remaining = length;
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}";
using var content = new ByteArrayContent(buffer, 0, count);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
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");
await PutAsync(container + "/" + key, manifestContent, false, ct,
query: "?multipart-manifest=put");
}
}
Gestión de archivos grandes
Cada archivo no vacío recibe un nuevo prefijo de segmentos en <container>_segments.
Las subidas secuenciales mantienen el búfer de datos en 5 MiB. El manifiesto contiene la ruta, el
ETag y el tamaño de cada segmento, que Swift valida antes de publicar el objeto. Un archivo vacío
utiliza un PUT vacío normal porque un manifiesto SLO debe contener un segmento respaldado por un
objeto.
La clave del objeto remoto puede contener espacios y Unicode. La codificación conserva los separadores de ruta y rechaza los segmentos vacíos, los formados por un único punto y los formados por dos puntos consecutivos, para evitar la normalización de la URI. Mantén el archivo local sin cambios durante toda la subida; las comprobaciones de tamaño no pueden detectar todas las modificaciones simultáneas que conservan el mismo tamaño.
Implementación de una gestión robusta de errores
La creación de contenedores acepta HTTP 201 o 202; las subidas de objetos y manifiestos requieren 201. Los errores de cuota, autenticación, suma de verificación, conexión y cancelación provocan la terminación con un código distinto de cero. Ctrl+C y el límite de cinco minutos se propagan a las lecturas de archivos y las operaciones HTTP. Este ejemplo no realiza reintentos.
Un intento fallido puede dejar segmentos almacenados. Un prefijo nuevo impide que un intento posterior mezcle segmentos antiguos y nuevos. Registra y elimina únicamente los prefijos de subida sin referencias mediante tu flujo de mantenimiento del almacenamiento. No elimines segmentos sin comprobar su estado después de que falle una solicitud de manifiesto: su respuesta puede haberse perdido aunque la publicación haya tenido éxito. Al reemplazar un objeto, sus segmentos antiguos también quedan pendientes de ese flujo de mantenimiento.
Optimización del rendimiento
Reutiliza el cliente para un lote de exportaciones. El límite de conexiones corresponde a
SocketsHttpHandler. Si añades subidas simultáneas, limita la concurrencia y reserva
un búfer de segmento por operación.
Ejemplo práctico: implementación completa
Con ambas variables de entorno configuradas, ejecuta lo siguiente desde el directorio del proyecto:
printf 'example export\n' > report.txt
dotnet run -- exports 'reports/report #1.txt' report.txt
El comando crea exports y exports_segments si es necesario,
sube los segmentos y, por último, publica reports/report #1.txt. Su código de salida es
cero solo después de recibir la respuesta final satisfactoria.
Conclusión
La segmentación de Swift es un flujo explícito de subida y creación de manifiestos. Utilizar el contrato HTTP documentado permite ver con claridad la gestión de respuestas, la responsabilidad sobre los recursos y la cancelación en una pequeña aplicación .NET.
