Dateien mit Gophercloud in Go zu OpenStack Swift hochladen
Authentifizieren Sie sich mit Gophercloud v2 über Keystone, laden Sie eine lokale Datei zu OpenStack Swift hoch und vergleichen Sie die heruntergeladenen Bytes mit dem Original. Der folgende Befehl unterstützt auch Dynamic Large Objects (DLOs). Jeder Upload erhält ein neues Segmentpräfix, damit eine kürzere Ersatzversion keine alten Segmente übernimmt.
Voraussetzungen
Sie benötigen:
- Go 1.26.9 oder eine neuere, weiterhin gepflegte Patch-Version von Go 1.26. Dieses Beispiel wurde mit Go 1.26.9 unter Linux getestet. Go 1.27 wird ebenfalls vom Go-Projekt unterstützt, liegt aber außerhalb des hier getesteten Bereichs. Der Go-Veröffentlichungsverlauf erläutert Patch-Korrekturen und Support.
- Bash für die folgenden Shell-Befehle
- Grundkenntnisse in der Go-Programmierung
- Eine bestehende Keystone-v3- und Swift-Umgebung mit einem Endpunkt für
object-storeim Servicekatalog Ihrer Region sowie Berechtigungen zum Erstellen von Containern und zum Lesen, Schreiben und Löschen von Objekten - Die
clouds.yamlIhres Anbieters oder Zugangsdaten mit Benutzername und Passwort sowie die Benutzer- und Projektdomain
Die Speicherbeispiele wurden mit Swift 2.38.2 und Keystone 30.0.0 in einer lokalen Umgebung mit einem einzelnen Knoten getestet. Daraus lassen sich keine Aussagen zu Kontingenten, Replikation oder der Konsistenz von Auflistungen bei einem Hostinganbieter ableiten. Verwenden Sie einen Container und einen Objektnamen, die Ihnen gehören. Während des Ersetzens oder Löschens dürfen keine parallelen Lese- oder Schreibzugriffe erfolgen. Containernamen erlauben ASCII-Buchstaben, Ziffern, Unterstriche und Bindestriche; Objektnamen zusätzlich Punkte. Lokale Dateipfade sind von Objektnamen unabhängig und dürfen Leerzeichen enthalten.
Go-Umgebung einrichten
Erstellen Sie ein neues Projekt. Jeder Shell-Block läuft in einer Subshell, sodass bei Fehlern das
Verzeichnis und die Optionen der aufrufenden Shell erhalten bleiben. GOWORK=off
verhindert, dass ein übergeordneter Go-Workspace ein anderes Modul auswählt. Ein bereits vorhandenes
Verzeichnis swift-upload-example wird abgelehnt. Wählen Sie ein anderes, neues Verzeichnis
und passen Sie die späteren Pfade an, statt ein bestehendes Projekt zu löschen.
(
command -v go >/dev/null || { printf 'Install Go first.\n' >&2; exit 1; }
export GOWORK=off
mkdir swift-upload-example &&
cd swift-upload-example &&
go mod init example.com/swift-upload
)
Gophercloud installieren
Installieren Sie Gophercloud v2.15.0, die v2-Version des Moduls
utils zum Laden von Zugangsdaten sowie den Rate Limiter für den Modus
limited. Hier werden genau diese Abhängigkeitsversionen verwendet:
(
export GOWORK=off
cd swift-upload-example &&
go get github.com/gophercloud/gophercloud/v2@v2.15.0 &&
go get github.com/gophercloud/utils/v2/openstack/clientconfig@v2.0.0-20260922174841-b4759ea0529b &&
go get golang.org/x/time/rate@v0.16.0
)
Behalten Sie für Gophercloud und utils die v2-Importpfade bei. Falls die
Installation fehlschlägt, führen Sie diesen Block erneut im übergeordneten Verzeichnis des Projekts
aus; wiederholen Sie dabei nicht die Verzeichniserstellung. Bewahren Sie go.mod
und go.sum zusammen auf, damit spätere Builds die aufgezeichnete
Abhängigkeitsauflösung verwenden.
Bei OpenStack Swift authentifizieren
Speichern Sie jeden folgenden Go-Block in der jeweils angegebenen Datei innerhalb von
swift-upload-example. Es sind separate Quelldateien in einem gemeinsamen Paket
main; behalten Sie ihre Importblöcke wie dargestellt bei.
Speichern Sie die Zugangsdaten-Datei Ihres Anbieters als swift-upload-example/clouds.yaml. Für die
passwortbasierte Authentifizierung mit Keystone v3 hat ihr Eintrag folgende Form. Ersetzen Sie
jeden Platzhalter durch die Werte Ihres Anbieters:
clouds:
openstack:
auth_type: v3password
auth:
auth_url: https://identity.example.com/v3
username: YOUR_USERNAME
password: YOUR_PASSWORD
project_name: YOUR_PROJECT
user_domain_name: YOUR_USER_DOMAIN
project_domain_name: YOUR_PROJECT_DOMAIN
Nehmen Sie diese Datei nicht in die Versionskontrolle auf und machen Sie sie nur für sich selbst
lesbar. Die Befehle wählen den Eintrag openstack mit
OS_CLOUD und den Swift-Katalogendpunkt mit OS_REGION_NAME aus;
ersetzen Sie RegionOne durch Ihre Region. Setzen Sie für eine private CA
SSL_CERT_FILE für den Prozess auf deren PEM-Bundle. Lassen Sie die Prüfung der
TLS-Zertifikate aktiviert.
Speichern Sie dies als auth.go. Der Standardpfad verwendet
clouds.yaml; die Alternative -auth env liest
OS_AUTH_URL, OS_USERNAME, OS_PASSWORD,
OS_PROJECT_NAME und OS_DOMAIN_NAME aus Ihrer Umgebung. Diese kompakte
Alternative setzt voraus, dass Benutzer und Projekt dieselbe Domain verwenden. Verwenden Sie YAML,
wenn sich die Domains unterscheiden.
package main
import (
"context"
"fmt"
"os"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack"
"github.com/gophercloud/utils/v2/openstack/clientconfig"
)
func authenticateWithCredentials(ctx context.Context) (*gophercloud.ServiceClient, error) {
opts := gophercloud.AuthOptions{
IdentityEndpoint: os.Getenv("OS_AUTH_URL"),
Username: os.Getenv("OS_USERNAME"),
Password: os.Getenv("OS_PASSWORD"),
TenantName: os.Getenv("OS_PROJECT_NAME"),
DomainName: os.Getenv("OS_DOMAIN_NAME"),
}
provider, err := openstack.AuthenticatedClient(ctx, opts)
if err != nil {
return nil, fmt.Errorf("error creating OpenStack provider client: %w", err)
}
client, err := openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
return nil, fmt.Errorf("error creating Swift service client: %w", err)
}
return client, nil
}
func authenticateWithCloudsYAML(ctx context.Context) (*gophercloud.ServiceClient, error) {
opts := &clientconfig.ClientOpts{
Cloud: os.Getenv("OS_CLOUD"),
}
provider, err := clientconfig.AuthenticatedClient(ctx, opts)
if err != nil {
return nil, fmt.Errorf("error creating provider client: %w", err)
}
client, err := openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
return nil, fmt.Errorf("error creating Swift service client: %w", err)
}
return client, nil
}
Dateien in Swift-Container hochladen
Sicherstellen, dass der Container existiert
Speichern Sie dies als container.go. Erstellen Sie einen fehlenden Container erst
nach einem 404; eine abgelehnte Anfrage wird an den Aufrufer zurückgegeben. Die Funktion
ResponseCodeIs von Gophercloud prüft den HTTP-Status auch bei gekapselten Fehlern:
package main
import (
"context"
"fmt"
"net/http"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers"
)
func ensureContainer(ctx context.Context, client *gophercloud.ServiceClient, containerName string) error {
result := containers.Get(ctx, client, containerName, nil)
if result.Err == nil {
return nil
}
if !gophercloud.ResponseCodeIs(result.Err, http.StatusNotFound) {
return fmt.Errorf("error checking container %s: %w", containerName, result.Err)
}
_, err := containers.Create(ctx, client, containerName, containers.CreateOpts{}).Extract()
if err != nil {
return fmt.Errorf("error creating container %s: %w", containerName, err)
}
return nil
}
Eine Datei hochladen
Speichern Sie dies als upload.go. Die Eingabe muss eine reguläre Datei sein und
während des gesamten Uploads und der Überprüfung unverändert bleiben. Das Beispiel speichert sie
als application/octet-stream.
package main
import (
"context"
"fmt"
"os"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/objects"
)
func uploadFile(ctx context.Context, client *gophercloud.ServiceClient, containerName, objectName, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("error getting file info: %w", err)
}
if !stat.Mode().IsRegular() {
return fmt.Errorf("input must be a regular file")
}
createOpts := objects.CreateOpts{
Content: file,
ContentLength: stat.Size(),
ContentType: "application/octet-stream",
}
result := objects.Create(ctx, client, containerName, objectName, createOpts)
if err := result.Err; err != nil {
return fmt.Errorf("error uploading file: %w", err)
}
return nil
}
Standardmäßig berechnet Gophercloud vor dem Upload einen MD5-ETag.
Bei einer *os.File, deren Leseposition sich versetzen lässt, berechnet es den
Hash und setzt die Position auf den Anfang zurück. So liest es die Datei zweimal, ohne sie komplett
in den Arbeitsspeicher zu laden. Ein Reader ohne diese Möglichkeit wird stattdessen gepuffert.
Der spätere Prüfbefehl vergleicht die SHA-256-Hashes der vollständigen lokalen Datei und des
heruntergeladenen Objekts, auch bei leeren Dateien.
Wiederholungslogik für Uploads
Speichern Sie dies als retry.go. Wiederholen Sie Anfragen bei den Antworten
429, 502, 503 und 504 mit insgesamt bis zu drei Versuchen und Wartezeiten von 1 und 2 Sekunden
dazwischen. Andere Fehler werden sofort zurückgegeben. Jeder Versuch öffnet die Eingabedatei neu.
package main
import (
"context"
"fmt"
"time"
"github.com/gophercloud/gophercloud/v2"
)
func uploadWithRetry(ctx context.Context, client *gophercloud.ServiceClient, containerName, objectName, filePath string) error {
backoff := []time.Duration{time.Second, 2 * time.Second, 5 * time.Second}
var err error
for i, wait := range backoff {
if err = uploadFile(ctx, client, containerName, objectName, filePath); err == nil {
return nil
}
if !retryable(err) {
return err
}
if i == len(backoff)-1 {
break
}
// Waiting on the context means a cancelled upload stops retrying immediately.
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(wait):
}
}
return fmt.Errorf("failed after %d attempts: %w", len(backoff), err)
}
func retryable(err error) bool {
return gophercloud.ResponseCodeIs(err, 429) ||
gophercloud.ResponseCodeIs(err, 502) ||
gophercloud.ResponseCodeIs(err, 503) ||
gophercloud.ResponseCodeIs(err, 504)
}
Ein erneuter Versuch verwendet denselben Objektnamen und ersetzt ein dort bereits vorhandenes Objekt. Geht eine Antwort verloren, kann der Schreibstatus unklar bleiben. Prüfen Sie daher das Objekt, bevor Sie annehmen, dass ein Exit-Status ungleich null bedeutet, dass nichts gespeichert wurde.
Ein Objekt mit Wiederholungslogik hochladen (mit createopts)
Speichern Sie dies als object-retry.go. Segmente verwenden bei jedem Versuch einen
neuen Byte-Reader. Wird ein Reader erneut verwendet, den eine frühere Anfrage bereits vollständig
ausgelesen hat, kann beim erneuten Versuch ein leerer Anfragekörper gesendet werden.
package main
import (
"bytes"
"context"
"fmt"
"time"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/objects"
)
func uploadObjectWithRetry(ctx context.Context, client *gophercloud.ServiceClient, containerName, objectName string, opts objects.CreateOpts, content []byte) error {
backoff := []time.Duration{time.Second, 2 * time.Second, 5 * time.Second}
var err error
for i, wait := range backoff {
attemptOpts := opts
attemptOpts.Content = bytes.NewReader(content)
attemptOpts.ContentLength = int64(len(content))
if err = objects.Create(ctx, client, containerName, objectName, attemptOpts).Err; err == nil {
return nil
}
if !retryable(err) {
return err
}
if i == len(backoff)-1 {
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(wait):
}
}
return fmt.Errorf("failed after %d attempts: %w", len(backoff), err)
}
Große Dateien mit segmentierten Uploads verarbeiten
Speichern Sie dies als large.go. Ein DLO-Manifest ist ein Objekt mit null Bytes,
dessen Header X-Object-Manifest einen Segmentcontainer und ein Präfix angibt. Swift fügt
alle passenden Segmente in lexikografischer Reihenfolge ihrer Namen zusammen. Deshalb verwenden
die folgenden Namen Zahlen mit führenden Nullen. Setzen Sie den Header mit
CreateOpts.ObjectManifest, nicht mit benutzerdefinierten Metadaten. Weitere Informationen finden
Sie in der DLO-Dokumentation von Swift.
Ein neues zufälliges Präfix isoliert jede Version. Bei erneuter Verwendung von
<objectName>/ würden bei einer kürzeren Ersatzversion überzählige Segmente erhalten
bleiben. Außerdem würde sich der Inhalt des alten Manifests bereits vor der Veröffentlichung
ändern. Das Manifest wird nach den Segmenten geschrieben. Das Zusammenfügen eines DLO hängt von
Containerauflistungen ab, die in einer verteilten Swift-Installation erst nach einiger Zeit
konsistent sind. Die Veröffentlichung eines Manifests allein belegt daher nicht, dass ein Leser
bereits jedes Byte abrufen kann. Führen Sie die Byte-Prüfung aus, bevor Sie den Vorgang als
abgeschlossen betrachten.
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"os"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/objects"
)
// A random version keeps replacements separate from a live manifest's segments.
func newSegmentPrefix(objectName string) (string, error) {
version := make([]byte, 16)
if _, err := rand.Read(version); err != nil {
return "", fmt.Errorf("error generating segment prefix: %w", err)
}
return fmt.Sprintf("%s/%s/", objectName, hex.EncodeToString(version)), nil
}
func uploadLargeFile(ctx context.Context, client *gophercloud.ServiceClient, containerName, objectName, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("error getting file info: %w", err)
}
if !stat.Mode().IsRegular() {
return fmt.Errorf("input must be a regular file")
}
// Discover destination failures before sending segments.
if err := ensureContainer(ctx, client, containerName); err != nil {
return fmt.Errorf("error ensuring destination container: %w", err)
}
segmentsContainerName := containerName + "_segments"
if err := ensureContainer(ctx, client, segmentsContainerName); err != nil {
return fmt.Errorf("error ensuring segments container: %w", err)
}
segmentPrefix, err := newSegmentPrefix(objectName)
if err != nil {
return err
}
buffer := make([]byte, 5*1024*1024) // 5 MiB segments
segmentNum := 0
for {
// ReadFull fills each segment except the final one despite short filesystem reads.
n, readErr := io.ReadFull(file, buffer)
if n > 0 {
segmentName := fmt.Sprintf("%s%08d", segmentPrefix, segmentNum)
segmentOpts := objects.CreateOpts{ContentType: "application/octet-stream"}
if err := uploadObjectWithRetry(ctx, client, segmentsContainerName, segmentName, segmentOpts, buffer[:n]); err != nil {
return fmt.Errorf("error uploading segment %d: %w", segmentNum, err)
}
segmentNum++
}
if readErr == io.EOF || readErr == io.ErrUnexpectedEOF {
break
}
if readErr != nil {
return fmt.Errorf("error reading file: %w", readErr)
}
}
// A replacement continues to reference its previous prefix until publication.
manifestOpts := objects.CreateOpts{
ContentType: "application/octet-stream",
ObjectManifest: segmentsContainerName + "/" + segmentPrefix,
}
if err := uploadObjectWithRetry(ctx, client, containerName, objectName, manifestOpts, nil); err != nil {
return fmt.Errorf("error creating manifest for large file: %w", err)
}
return nil
}
Fehlgeschlagene Uploads und ersetzte Versionen hinterlassen Segmente. Bereinigen Sie
<objectName>/ nicht pauschal: Dort liegen auch aktive Versionen und andere
Durchläufe. Eine unklare Antwort auf das Schreiben des Manifests ist besonders gefährlich, da der
Server es trotz des Fehlers beim Aufrufer veröffentlicht haben kann. Eine allgemeine Bereinigung
ungenutzter Segmente erfordert nachgewiesene Eigentümerschaft, aktuelle Referenzprüfungen und eine
Schonfrist, die länger dauert als aktive Uploads und Lesezugriffe. Dieses Beispiel implementiert
keine solche Bereinigung.
Ein gewöhnliches DELETE entfernt ein DLO-Manifest, behält aber dessen Segmente bei. Die
SLO-Operation ?multipart-manifest=delete dient nicht zur DLO-Bereinigung: Der
SLO-Handler von Swift 2.38.2 gibt als äußeren HTTP-Status 200 zurück,
während der Antwortkörper zur Massenlöschung 400 meldet. Das DLO bleibt dabei erhalten. Der folgende
Befehl -mode delete entfernt nur das aktuelle Manifest und dessen exaktes
Versionspräfix. Er setzt alleinige Eigentümerschaft und gestoppte Lese- und Schreibzugriffe voraus.
Ältere oder fehlgeschlagene Versionen bleiben zur separaten Prüfung erhalten.
Fehlerbehandlung und bewährte Verfahren
Begrenzung der Anfragerate
Speichern Sie diesen Wrapper als rate.go. Bei wiederholter Verwendung durch
einen länger laufenden Aufrufer begrenzt er die Zulassung von Uploads ganzer Dateien. Er begrenzt
nicht jeden erneuten Versuch oder jede segmentierte Anfrage. Ein einzelner CLI-Aufruf belegt keine
dauerhaft eingehaltene Anfragerate.
package main
import (
"context"
"fmt"
"github.com/gophercloud/gophercloud/v2"
"golang.org/x/time/rate"
)
type RateLimitedClient struct {
client *gophercloud.ServiceClient
limiter *rate.Limiter
}
func NewRateLimitedClient(client *gophercloud.ServiceClient, rps float64) *RateLimitedClient {
return &RateLimitedClient{
client: client,
limiter: rate.NewLimiter(rate.Limit(rps), 1),
}
}
func (r *RateLimitedClient) UploadFile(ctx context.Context, containerName, objectName, filePath string) error {
if err := r.limiter.Wait(ctx); err != nil {
return fmt.Errorf("rate limit wait error: %w", err)
}
return uploadWithRetry(ctx, r.client, containerName, objectName, filePath)
}
Ein hochgeladenes Objekt prüfen und löschen
Speichern Sie dies als verify.go. Berechnen Sie die Hashes der lokalen Datei und
des vollständig heruntergeladenen Antwortkörpers getrennt. Der ETag eines DLO ist nicht der MD5-Hash
der Originaldatei. Ein Vergleich dieser beiden ETags wäre daher die falsche Prüfung.
package main
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/objects"
)
var errContentMismatch = errors.New("downloaded bytes differ from the local file")
func verifyFile(ctx context.Context, client *gophercloud.ServiceClient, containerName, objectName, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
localHash := sha256.New()
localSize, err := io.Copy(localHash, file)
if err != nil {
return err
}
result := objects.Download(ctx, client, containerName, objectName, nil)
if result.Err != nil {
return result.Err
}
defer result.Body.Close()
remoteHash := sha256.New()
remoteSize, err := io.Copy(remoteHash, result.Body)
if err != nil {
return err
}
if localSize != remoteSize || !bytes.Equal(localHash.Sum(nil), remoteHash.Sum(nil)) {
return errContentMismatch
}
fmt.Printf("Verified %s/%s: %d bytes, sha256 %x\n", containerName, objectName, remoteSize, remoteHash.Sum(nil))
return nil
}
Speichern Sie dies als cleanup.go. Prüfen Sie, ob ein DLO auf den Segmentcontainer
und Objektnamen dieses Befehls sowie auf eine einzelne zufällige Version verweist. Listen Sie genau
dieses Präfix auf, bevor Sie das Manifest entfernen, und löschen Sie anschließend nur diese
Segmente. Das Präfix wird zuerst ausgegeben, damit Sie verbleibende Segmente prüfen können, falls
das Löschen nach dem Entfernen des Manifests fehlschlägt. Die Container selbst bleiben erhalten.
package main
import (
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/objects"
)
func deleteUpload(ctx context.Context, client *gophercloud.ServiceClient, containerName, objectName string) error {
header, err := objects.Get(ctx, client, containerName, objectName, nil).Extract()
if err != nil {
return err
}
var names []string
segmentsContainer := containerName + "_segments"
if header.ObjectManifest != "" {
parts := strings.Split(header.ObjectManifest, "/")
if len(parts) != 4 || parts[0] != segmentsContainer || parts[1] != objectName ||
len(parts[2]) != 32 || parts[3] != "" {
return fmt.Errorf("refusing a manifest outside this upload's namespace")
}
if _, err := hex.DecodeString(parts[2]); err != nil {
return fmt.Errorf("refusing an invalid version prefix")
}
prefix := objectName + "/" + parts[2] + "/"
pages, err := objects.List(client, segmentsContainer, objects.ListOpts{Prefix: prefix}).AllPages(ctx)
if err != nil {
return err
}
names, err = objects.ExtractNames(pages)
if err != nil {
return err
}
fmt.Printf("Segment prefix: %s\n", header.ObjectManifest)
}
if err := objects.Delete(ctx, client, containerName, objectName, nil).Err; err != nil {
return err
}
for _, name := range names {
if err := objects.Delete(ctx, client, segmentsContainer, name, nil).Err; err != nil {
return err
}
}
return nil
}
Den vollständigen Befehl ausführen
Speichern Sie dies als main.go. Der Standardmodus lädt eine Datei hoch;
large verwendet DLO-Segmente, limited ruft den Wrapper
mit begrenzter Anfragerate auf, verify lädt die Daten herunter und vergleicht
die Bytes, und delete führt die oben beschriebene, gezielte Bereinigung aus.
Ctrl+C fordert einen Abbruch an. Ein Fehler gibt einen Status ungleich null zurück, ohne
Zugangsdaten oder unverarbeitete API-Antworten auszugeben.
package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"regexp"
"syscall"
"github.com/gophercloud/gophercloud/v2"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, failureMessage(err))
os.Exit(1)
}
}
func run() error {
mode := flag.String("mode", "file", "file, large, limited, verify, or delete")
auth := flag.String("auth", "yaml", "yaml or env")
flag.Parse()
args := flag.Args()
if *mode != "file" && *mode != "large" && *mode != "limited" && *mode != "verify" && *mode != "delete" {
return fmt.Errorf("unknown mode")
}
if *auth != "yaml" && *auth != "env" {
return fmt.Errorf("unknown authentication mode")
}
if (*mode == "delete" && len(args) != 2) || (*mode != "delete" && len(args) != 3) {
return fmt.Errorf("usage: swift-upload [-mode MODE] [-auth yaml|env] CONTAINER OBJECT [FILE]")
}
containerName, objectName := args[0], args[1]
if !regexp.MustCompile(`^[A-Za-z0-9_-]+$`).MatchString(containerName) ||
!regexp.MustCompile(`^[A-Za-z0-9_.-]+$`).MatchString(objectName) {
return fmt.Errorf("use plain ASCII container and object names")
}
if os.Getenv("OS_REGION_NAME") == "" || (*auth == "yaml" && os.Getenv("OS_CLOUD") == "") {
return fmt.Errorf("set OS_REGION_NAME and, for YAML authentication, OS_CLOUD")
}
if *mode != "delete" {
stat, err := os.Stat(args[2])
if err != nil {
return fmt.Errorf("cannot inspect input: %w", err)
}
if !stat.Mode().IsRegular() {
return fmt.Errorf("input must be a regular file")
}
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
var client *gophercloud.ServiceClient
var err error
if *auth == "env" {
client, err = authenticateWithCredentials(ctx)
} else {
client, err = authenticateWithCloudsYAML(ctx)
}
if err != nil {
return fmt.Errorf("authentication or catalog lookup failed: %w", err)
}
if *mode == "verify" {
return verifyFile(ctx, client, containerName, objectName, args[2])
}
if *mode == "delete" {
if err := deleteUpload(ctx, client, containerName, objectName); err != nil {
return fmt.Errorf("deletion failed: %w", err)
}
fmt.Printf("Deleted %s/%s\n", containerName, objectName)
return nil
}
if err := ensureContainer(ctx, client, containerName); err != nil {
return err
}
switch *mode {
case "large":
err = uploadLargeFile(ctx, client, containerName, objectName, args[2])
case "limited":
err = NewRateLimitedClient(client, 1).UploadFile(ctx, containerName, objectName, args[2])
default:
err = uploadWithRetry(ctx, client, containerName, objectName, args[2])
}
if err != nil {
return err
}
fmt.Printf("Uploaded %s/%s\n", containerName, objectName)
return nil
}
func failureMessage(err error) string {
if errors.Is(err, errContentMismatch) {
return "Verification failed: downloaded bytes differ from the local file."
}
if errors.Is(err, context.Canceled) {
return "Operation canceled; inspect stored objects before retrying."
}
for _, status := range []int{401, 403, 404, 429, 502, 503, 504} {
if gophercloud.ResponseCodeIs(err, status) {
return fmt.Sprintf("Operation failed with HTTP %d; check credentials, access, and the target object.", status)
}
}
return "Operation failed; check arguments, cloud configuration, and the local input file."
}
Erstellen Sie ein kleines binäres Fixture, ohne eine bestehende Datei zu überschreiben:
(
set -C
cd swift-upload-example &&
printf '\000\377Swift\n' > example.bin
)
Erstellen Sie den Build, laden Sie die Datei hoch und prüfen Sie sie. Ersetzen Sie in beiden
Befehlen exports durch Ihren eigenen Container und
RegionOne durch Ihre Katalogregion. Der Build ersetzt die ausführbare Datei
swift-upload dieses Projekts. Ein fehlgeschlagener Build stoppt den Block, bevor
eine frühere ausführbare Datei gestartet wird.
(
export GOWORK=off
cd swift-upload-example &&
go build -o swift-upload . &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload exports example.bin ./example.bin &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload -mode verify exports example.bin ./example.bin
)
Erwartete Ausgabe für dieses Fixture mit acht Bytes:
Uploaded exports/example.bin
Verified exports/example.bin: 8 bytes, sha256 2949a2f975f056f2652117015349d0768d9eaffcd9eacd14f81147844c47f158
Verwenden Sie für Ihre eigene größere Datei dieselbe ausführbare Datei mit
-mode large und prüfen Sie anschließend das zusammengefügte Objekt. Ein Upload
unter demselben Objektnamen ersetzt dessen Inhalt; beim Ersetzen eines DLO bleiben ältere
Segmentversionen erhalten. Falls die Prüfung fehlschlägt, weil die DLO-Auflistung noch nicht
konsistent ist, wiederholen Sie die Prüfung, ohne erneut hochzuladen. Abweichende Hashes können
auch auf fehlende oder veränderte Segmente hinweisen. Prüfen Sie daher das exakte Präfix, bevor Sie
etwas löschen.
(
cd swift-upload-example &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload -mode large exports backup.bin ./backup.bin &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload -mode verify exports backup.bin ./backup.bin
)
Stoppen Sie die Lese- und Schreibzugriffe und entfernen Sie anschließend die aktuelle Version, die Ihnen gehört:
(
cd swift-upload-example &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload -mode delete exports backup.bin
)
Lesen Sie bei einer fehlgeschlagenen Bereinigung das ausgegebene Präfix und prüfen Sie nur diese eigene Version. Ein zweiter Löschversuch nach dem Entfernen des Manifests gibt 404 zurück; er kann verbliebene Segmente nicht mehr anhand dieses Headers ermitteln. Löschen Sie niemals eine benachbarte Version, nur weil ihr Name mit demselben Objektnamen beginnt.
