Dateien mühelos zu OpenStack Swift mit .NET C# exportieren
Laden Sie Dateien mit dem integrierten HttpClient von .NET über die HTTP-API
von Swift hoch. Dieses Beispiel erstellt tatsächliche Segmente und veröffentlicht ein statisches
Large-Object-Manifest erst, nachdem jedes Segment bestätigt wurde.
Einführung in OpenStack Swift
Swift speichert Objekte in Containern. Seine 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 ein gepflegtes .NET 10 SDK. Das Beispiel wurde in Bash unter Linux mit SDK 10.0.111, Runtime 10.0.11 und Swift 2.38.2 getestet. Beschaffen Sie ein projektgebundenes Token und die URL des Objektspeicherkontos über den OpenStack-Authentifizierungsablauf Ihrer Bereitstellung. Das Token benötigt Berechtigungen zum Erstellen von Containern, Hochladen von Objekten, Lesen von Segmentmetadaten zur SLO-Validierung und Löschen von Segmenten fehlgeschlagener Uploads.
Das Beispiel verwendet Segmente mit 5 MiB und höchstens 1.000 Segmente (5.242.880.000 Bytes,
einschließlich dieser Obergrenze). Vergewissern Sie sich, dass die SLO-, Objektgrößen- und
Kontingentgrenzen Ihrer Bereitstellung diese Werte zulassen.
Für den gesamten Upload gilt eine Frist von fünf Minuten. Eine langsamere Verbindung kann daher
auch unterhalb der Größenbeschränkung zu einem Fehler führen.
Verwenden Sie lokale Pfade und Projektverzeichnisnamen ausschließlich mit ASCII-Zeichen.
Das Programm erzwingt diese Einschränkung vor dem Öffnen der Datei, damit fehlerhafte Bytes im
Dateinamen nach der Argumentdecodierung keine andere Datei auswählen können.
Entfernte Objektschlüssel dürfen Unicode enthalten.
Beginnen Sie in einem beschreibbaren Verzeichnis ohne Eintrag SwiftExport.
Übergeordnete Konfigurationen für .NET SDK, NuGet und MSBuild gelten weiterhin.
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
Nach erfolgreicher Einrichtung befindet sich Ihre Shell im Projektverzeichnis. Schlägt die
Wiederherstellung fehl, bleibt das neue Projekt erhalten und Ihre Shell im übergeordneten
Verzeichnis. Beheben Sie das gemeldete SDK- oder NuGet-Problem und führen Sie dort anschließend
dotnet restore SwiftExport/SwiftExport.csproj && cd SwiftExport aus, um die Einrichtung abzuschließen.
Zusätzliche NuGet-Pakete sind nicht erforderlich.
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 projektgebundene Token. Stellen Sie beide Werte über Ihre
Shell oder einen Secret-Manager bereit. Beschaffung und Erneuerung des Tokens gehören zum
Keystone-Ablauf Ihrer Bereitstellung.
Ein abgelaufenes Token führt zu einem Exitcode ungleich null. Beschaffen Sie vor der erneuten
Ausführung ein neues projektgebundenes Token.
Swift-Speicheroperationen implementieren
Ersetzen Sie Program.cs durch dieses vollständige Programm. Die injizierte
Instanz von HttpClient gehört dem Aufrufer; der Exporter gibt jede Anfrage
und Antwort frei. Weiterleitungen sind deaktiviert, damit das Token nicht an ein anderes Ziel
gesendet 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.");
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; }
}
}
}
Große Dateien verarbeiten
Jede nicht leere Datei erhält ein neues Segmentpräfix in <container>_segments.
Sequenzielle Uploads begrenzen den Nutzdatenpuffer auf 5 MiB. Das Manifest enthält Pfad, ETag und
Größe jedes Segments. Swift validiert diese Angaben, bevor es das Objekt veröffentlicht.
Für eine leere Datei wird ein regulärer leerer PUT verwendet. So ist keine Unterstützung für
leere SLO-Manifeste erforderlich, die je nach Bereitstellung unterschiedlich ausfallen kann.
Der entfernte Objektschlüssel darf Leerzeichen und Unicode enthalten. Die URL-Kodierung erhält Pfadtrennzeichen und weist leere Segmente sowie die Segmente „.“ und „..“ zurück, um eine URI-Normalisierung zu vermeiden. Verändern Sie die lokale Datei während des gesamten Uploads nicht; Größenprüfungen können nicht jede gleichzeitige Änderung bei unveränderter Größe erkennen.
Robuste Fehlerbehandlung implementieren
Beim Erstellen von Containern werden HTTP 201 oder 202 akzeptiert; Objekt- und Manifest-Uploads erfordern 201. Die Segmentprüfsumme wird mit dem unveränderten ETag-Header von Swift verglichen, auch bei Bereitstellungen, die Anführungszeichen weglassen. Fehler bei Kontingenten, Authentifizierung, Prüfsummen, Verbindungen und Abbrüchen führen zu einem Exitcode ungleich null. Strg+C und die Frist von fünf Minuten wirken sich auch auf Dateilesezugriffe und HTTP-Operationen aus. Dieses Beispiel führt keine erneuten Versuche durch.
Vor dem Absenden des Manifests versucht ein fehlgeschlagener Upload, ausschließlich seine eigenen, exakten Segmentnamen zu löschen. Für die Bereinigung stehen separat 15 Sekunden zur Verfügung, sodass sie bei einem Abbruch nicht übersprungen wird. Fehlgeschlagene Löschvorgänge können dennoch verwaiste Segmente hinterlassen. Das bestehende Objekt bleibt verfügbar, bis sein Ersatz veröffentlicht wurde.
Sobald das Absenden des Manifests beginnt, behält das Programm die neuen Segmente bei einem Fehler bei. Eine verlorene Antwort kann eine erfolgreiche Veröffentlichung verbergen. Das Löschen dieser Segmente könnte daher das Objekt beschädigen. Prüfen Sie das aktuelle Manifest, bevor Sie den Upload erneut versuchen oder nicht referenzierte Versionen bereinigen. Beim Ersetzen werden ein neues Präfix und eine explizite Segmentliste verwendet. So übernimmt eine kürzere Datei keine Segmente vom Ende einer älteren Datei. Alte Segmente bleiben nach dem Ersetzen erhalten. Bereinigen Sie nur Versionen, die Ihnen gehören, auf die keine Referenzen mehr verweisen und auf die niemand mehr aktiv lesend zugreift. Dieses Beispiel implementiert diesen Wartungsablauf nicht.
Leistung optimieren
Verwenden Sie den Client für eine Reihe von Exporten wieder. Die Verbindungsgrenze wird durch
SocketsHttpHandler festgelegt.
Wenn Sie parallele Uploads ergänzen, begrenzen Sie deren Anzahl und planen Sie pro Operation einen
Segmentpuffer ein.
Praxisbeispiel: vollständige Implementierung
Wenn beide Umgebungsvariablen konfiguriert sind, legen Sie die zu exportierende Datei unter
report.bin im Projektverzeichnis ab. Dieser Befehl erstellt oder ersetzt das
entfernte Objekt unter dem angegebenen Schlüssel:
dotnet run -- exports 'reports/report #1.bin' report.bin
Der Befehl erstellt bei Bedarf exports und
exports_segments, lädt die Segmente hoch und veröffentlicht abschließend
reports/report #1.bin. Eine leere Eingabe erstellt stattdessen ein reguläres leeres Objekt.
Der Befehl gibt Uploaded. aus und endet erst
nach der letzten erfolgreichen Antwort mit Exitcode null. Laden Sie das Objekt über die reguläre
authentifizierte GET-API von Swift herunter, um seine Bytes mit report.bin zu
vergleichen. Das SLO-ETag eines nicht leeren Objekts ist nicht die MD5-Prüfsumme der gesamten Datei.
