Einfacher Dateiexport nach OpenStack Swift mit .NET C#
Laden Sie Dateien über die HTTP-API von Swift mit dem in .NET integrierten
HttpClient hoch. Dieses Beispiel erstellt tatsächliche Segmente und
veröffentlicht erst dann ein statisches Large-Object-Manifest, wenn jedes Segment bestätigt wurde.
Einführung in OpenStack Swift
Swift speichert Objekte in Containern. Die API für statische große Objekte führt eine geordnete Liste hochgeladener Segmente zu einem herunterladbaren Objekt zusammen. Für dieses Beispiel muss die SLO-Middleware in Ihrer Bereitstellung aktiviert sein.
Umgebung einrichten
Voraussetzungen
Installieren Sie das .NET 10 SDK. Beschaffen Sie über Ihren OpenStack-Authentifizierungsablauf ein gültiges Token mit festgelegtem Berechtigungsumfang und die Konto-URL für den Objektspeicher. Ihr Konto benötigt die Berechtigung, Container und Objekte zu erstellen. Das Beispiel verwendet Segmente mit 5 MiB und höchstens 1.000 Segmente (etwa 4,88 GiB). Prüfen Sie, ob die SLO-, Objektgrößen- und Kontingentgrenzen Ihrer Bereitstellung diese Werte zulassen.
dotnet new console --framework net10.0 --name SwiftExport
cd SwiftExport
Konfiguration sicher verwalten
Setzen Sie SWIFT_STORAGE_URL auf den vollständigen HTTPS-Kontoendpunkt aus dem
Dienstkatalog, einschließlich seines Pfads /v1/AUTH_…. Setzen Sie
SWIFT_TOKEN auf das Token mit festgelegtem Berechtigungsumfang. Stellen Sie
beide Werte über Ihre Shell oder Ihren Secret-Manager bereit. Beschaffung und Erneuerung des
Tokens gehören zum Keystone-Ablauf Ihrer Bereitstellung. Bei einem abgelaufenen Token beendet
sich das Programm, statt vorzugeben, ein erneuter Versuch könne es erneuern.
Speicheroperationen für Swift implementieren
Ersetzen Sie Program.cs durch dieses vollständige Programm. Die Verantwortung
für den injizierten HttpClient liegt beim Aufrufer; der Exporter gibt jede
Anfrage und Antwort frei. Weiterleitungen sind deaktiviert, damit das Token nicht an ein
anderes Ziel weitergegeben wird.
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");
}
}
Große Dateien verarbeiten
Jede nichtleere Datei erhält ein neues Segmentpräfix in <container>_segments.
Sequenzielle Uploads halten den Nutzdatenpuffer bei 5 MiB. Das Manifest enthält Pfad, ETag und
Größe jedes Segments. Swift validiert diese Angaben, bevor es das Objekt veröffentlicht.
Eine leere Datei verwendet einen regulären leeren PUT, da ein SLO-Manifest ein Segment enthalten
muss, dem ein Objekt zugrunde liegt.
Der entfernte Objektschlüssel darf Leerzeichen und Unicode enthalten. Die Zeichencodierung behält Pfadtrennzeichen bei und weist leere Segmente sowie Segmente aus einem oder zwei Punkten zurück, um eine URI-Normalisierung zu vermeiden. Lassen Sie die lokale Datei während des gesamten Uploads unverändert; Größenprüfungen erkennen nicht jede gleichzeitige Änderung, bei der die Größe unverändert bleibt.
Robuste Fehlerbehandlung implementieren
Beim Erstellen von Containern wird HTTP 201 oder 202 akzeptiert; Uploads von Objekten und Manifesten erfordern 201. Fehler bei Kontingenten, Authentifizierung, Prüfsummen oder Verbindungen sowie Abbrüche führen zu einem Exitstatus ungleich null. Ctrl+C und die Frist von fünf Minuten wirken auch auf Dateilesezugriffe und HTTP-Operationen. Dieses Beispiel führt keine Retries aus.
Ein fehlgeschlagener Versuch kann Segmente zurücklassen. Ein neues Präfix verhindert, dass ein späterer Versuch alte und neue Segmente vermischt. Erfassen und entfernen Sie im Rahmen Ihrer Speicherwartung ausschließlich Upload-Präfixe, auf die nicht mehr verwiesen wird. Löschen Sie Segmente nicht ungeprüft, nachdem eine Manifest-Anfrage fehlgeschlagen ist: Die Antwort könnte verloren gegangen sein, obwohl die Veröffentlichung erfolgreich war. Auch beim Ersetzen eines Objekts bleiben dessen alte Segmente für diesen Wartungsablauf erhalten.
Leistung optimieren
Verwenden Sie den Client für mehrere Exporte wieder. Das Verbindungslimit wird in
SocketsHttpHandler festgelegt. Wenn Sie gleichzeitige Uploads hinzufügen, begrenzen
Sie deren Anzahl und planen Sie pro Operation einen Segmentpuffer ein.
Praxisbeispiel: vollständige Implementierung
Wenn beide Umgebungsvariablen konfiguriert sind, führen Sie Folgendes im Projektverzeichnis aus:
printf 'example export\n' > report.txt
dotnet run -- exports 'reports/report #1.txt' report.txt
Der Befehl erstellt bei Bedarf exports und
exports_segments, lädt die Segmente hoch und veröffentlicht abschließend
reports/report #1.txt. Sein Exitstatus ist erst nach der letzten erfolgreichen Antwort null.
Fazit
Die Segmentierung in Swift ist ein expliziter Ablauf aus Upload und Manifest-Veröffentlichung. Mit der dokumentierten HTTP-Schnittstellenvereinbarung bleiben Antwortverarbeitung, Zuständigkeit für Ressourcen und Abbruch in einer kleinen .NET-Anwendung nachvollziehbar.
