Téléverser des fichiers sur OpenStack Swift en Go (Gophercloud)
Utilisez Gophercloud v2 pour vous authentifier via Keystone, téléverser un fichier local vers OpenStack Swift et comparer les octets téléchargés à ceux du fichier d’origine. La commande ci-dessous prend également en charge les Dynamic Large Objects (DLO), avec un nouveau préfixe de segments à chaque téléversement pour qu’une version de remplacement plus courte ne puisse pas hériter d’anciens segments.
Prérequis
Vous avez besoin des éléments suivants :
- Go 1.26.9 ou une version corrective ultérieure de Go 1.26 encore maintenue. Cet exemple a été testé avec Go 1.26.9 sous Linux. Go 1.27 est également pris en charge en amont, mais n’entre pas dans le périmètre testé de cet exemple. L’historique des versions de Go détaille les correctifs et la prise en charge.
- Bash pour les commandes shell ci-dessous
- Des connaissances de base en programmation Go
- Un environnement Keystone v3 et Swift existant, avec un point de terminaison
object-storedans le catalogue de services de votre région et l’autorisation de créer des conteneurs ainsi que de lire, d’écrire et de supprimer des objets - Le
clouds.yamlde votre fournisseur, ou des informations d’authentification par nom d’utilisateur et mot de passe ainsi que le domaine de l’utilisateur et celui du projet
Les exemples de stockage ont été testés avec Swift 2.38.2 et Keystone 30.0.0 dans un environnement local à un seul nœud. Ils ne permettent pas d’établir les quotas, la réplication ni la cohérence des listes chez un fournisseur d’hébergement. Utilisez un conteneur et un nom d’objet qui vous appartiennent, sans lecture ni écriture concurrente pendant le remplacement ou la suppression. Les noms de conteneurs acceptent les lettres ASCII, les chiffres, les traits de soulignement et les traits d’union ; les noms d’objets acceptent aussi les points. Les chemins des fichiers locaux sont distincts des noms d’objets et peuvent contenir des espaces.
Configurer l’environnement Go
Créez un nouveau projet. Chaque bloc shell s’exécute dans un sous-shell : en cas d’échec, le
répertoire et les options du shell appelant sont donc préservés. GOWORK=off
empêche un espace de travail Go englobant de sélectionner un autre module. Le bloc refuse un
répertoire swift-upload-example existant ; choisissez un autre répertoire neuf et adaptez
les chemins suivants au lieu de supprimer un projet existant.
(
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
)
Installer Gophercloud
Installez Gophercloud v2.15.0,
le module utils en version v2 pour charger les informations
d’authentification, ainsi que le limiteur de débit utilisé par le mode
limited. Voici les versions exactes des dépendances utilisées ici :
(
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
)
Conservez les chemins d’importation v2 pour Gophercloud comme pour
utils. Si l’installation échoue, relancez ce bloc depuis le répertoire
parent du projet ; ne relancez pas la création du répertoire par-dessus. Conservez ses fichiers
go.mod et go.sum ensemble pour que les compilations
suivantes utilisent la résolution de dépendances enregistrée.
S’authentifier auprès d’OpenStack Swift
Enregistrez chaque bloc Go ci-dessous dans le fichier indiqué, au sein de
swift-upload-example. Il s’agit de fichiers sources distincts dans un même paquet
main ; conservez leurs blocs d’importation tels qu’ils sont présentés.
Enregistrez le fichier d’informations d’authentification de votre fournisseur sous
swift-upload-example/clouds.yaml. Pour l’authentification Keystone v3 par mot de passe, son entrée
prend la forme suivante ; remplacez chaque valeur fictive par celle de votre fournisseur :
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
Ne versionnez pas ce fichier et veillez à être la seule personne à pouvoir le lire. Les commandes
sélectionnent son entrée openstack avec OS_CLOUD et le
point de terminaison du catalogue Swift avec OS_REGION_NAME ; remplacez
RegionOne par votre région. Pour une autorité de certification privée,
configurez SSL_CERT_FILE pour le processus afin de désigner son ensemble de
certificats PEM. Laissez la vérification des certificats TLS activée.
Enregistrez ce code sous auth.go. La méthode par défaut utilise
clouds.yaml ; l’alternative -auth env lit
OS_AUTH_URL, OS_USERNAME, OS_PASSWORD,
OS_PROJECT_NAME et OS_DOMAIN_NAME dans votre environnement. Cette
alternative succincte suppose que l’utilisateur et le projet partagent un domaine ; utilisez YAML
si leurs domaines diffèrent.
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
}
Téléverser des fichiers dans des conteneurs Swift
S’assurer que le conteneur existe
Enregistrez ce code sous container.go. Ne créez un conteneur manquant qu’après une
réponse 404 ; le refus d’une requête est renvoyé à l’appelant. La fonction
ResponseCodeIs de Gophercloud vérifie le statut HTTP même lorsque les erreurs sont
encapsulées :
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
}
Téléverser un fichier
Enregistrez ce code sous upload.go. L’entrée doit être un fichier ordinaire
et rester inchangée pendant toute la durée du téléversement et de la vérification. L’exemple la
stocke sous la forme 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
}
Par défaut, Gophercloud calcule un ETag MD5 avant le téléversement.
Pour un *os.File permettant de repositionner la lecture, il calcule
l’empreinte du fichier puis revient au début, le lisant ainsi deux fois sans le charger
intégralement en mémoire. Si le lecteur ne permet pas de repositionner la lecture, ses données
sont plutôt mises en mémoire tampon. La commande de vérification présentée plus loin compare les
empreintes SHA-256 du fichier local complet et de l’objet téléchargé, y compris pour les fichiers
vides.
Logique de nouvelle tentative pour les téléversements
Enregistrez ce code sous retry.go. Pour les réponses 429, 502, 503 et 504,
réessayez jusqu’à trois tentatives au total, en attendant 1 puis 2 secondes entre elles. Les autres
échecs sont renvoyés immédiatement. Chaque tentative rouvre le fichier d’entrée.
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)
}
Les nouvelles tentatives utilisent le même nom d’objet et remplacent tout objet déjà présent. Une réponse perdue peut laisser planer un doute sur la réussite de l’écriture ; inspectez donc l’objet avant de conclure qu’un code de sortie non nul signifie qu’aucune donnée n’a été stockée.
Téléverser un objet avec nouvelles tentatives (avec createopts)
Enregistrez ce code sous object-retry.go. Les segments utilisent un nouveau lecteur
d’octets à chaque tentative ; réutiliser un lecteur épuisé par une requête précédente peut envoyer
un corps vide lors d’une nouvelle tentative.
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)
}
Gérer les fichiers volumineux par téléversement segmenté
Enregistrez ce code sous large.go. Un manifeste DLO est un objet de zéro
octet dont l’en-tête X-Object-Manifest indique un conteneur de segments et un préfixe.
Swift concatène tous les segments correspondants dans l’ordre lexicographique de leurs noms ;
les noms ci-dessous utilisent donc des nombres complétés à largeur fixe. Utilisez
CreateOpts.ObjectManifest, et non des métadonnées personnalisées, pour définir l’en-tête.
Consultez la documentation de Swift sur les DLO.
Un nouveau préfixe aléatoire isole chaque version. Réutiliser <objectName>/
laisserait des segments à la fin si la version de remplacement était plus courte et modifierait
le contenu de l’ancien manifeste avant la publication de la nouvelle version. Le manifeste est
écrit après les segments. L’assemblage des DLO dépend des listes de conteneurs, dont la cohérence
est à terme dans un déploiement distribué de Swift ; la seule publication d’un manifeste ne prouve
pas qu’un lecteur peut déjà récupérer tous les octets. Exécutez la vérification des octets avant
de considérer l’objet comme complet.
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
}
Les téléversements ayant échoué et les versions remplacées laissent des segments. Ne supprimez
pas en bloc <objectName>/ : il contient aussi des versions actives et d’autres
exécutions. Une réponse incertaine lors de l’écriture du manifeste est particulièrement
dangereuse, car le serveur peut l’avoir publié malgré l’échec côté appelant. Un nettoyage général
requiert une preuve de propriété, la vérification des références actuelles et un délai de grâce
plus long que la durée des téléversements et des lectures en cours ; cet exemple n’implémente pas
ce mécanisme de nettoyage.
Une requête DELETE ordinaire supprime un manifeste DLO tout en conservant ses segments. Dans le
cas des SLO, ?multipart-manifest=delete n’est pas une opération de nettoyage des DLO ;
le gestionnaire SLO de Swift 2.38.2 renvoie un statut HTTP 200 pour
la réponse globale, dont le corps de suppression en masse indique 400, laissant le DLO intact.
La commande -mode delete ci-dessous supprime uniquement le manifeste actuel et
son préfixe de version exact. Elle suppose une propriété exclusive et l’arrêt des lectures et
écritures, et laisse les versions antérieures ou celles ayant échoué pour une inspection séparée.
Gestion des erreurs et bonnes pratiques
Limitation du débit
Enregistrez cette fonction d’encapsulation sous rate.go. Lorsqu’elle est
réutilisée par un appelant qui s’exécute plus longtemps, elle limite l’admission des téléversements
de fichiers entiers. Elle ne limite pas chaque nouvelle tentative ni chaque requête segmentée ;
une seule invocation en ligne de commande ne démontre pas un débit de requêtes soutenu.
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)
}
Vérifier et supprimer un objet téléversé
Enregistrez ce code sous verify.go. Calculez séparément l’empreinte du fichier
local et celle du corps téléchargé dans son intégralité. L’ETag d’un DLO n’est pas l’empreinte MD5
du fichier d’origine : comparer ces deux ETag ne serait donc pas une vérification appropriée.
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
}
Enregistrez ce code sous cleanup.go. Vérifiez qu’un DLO fait référence au
conteneur de segments et au nom d’objet de cette commande, ainsi qu’à une unique version
aléatoire. Listez les objets correspondant à ce préfixe exact avant de supprimer le manifeste,
puis supprimez uniquement ces segments. Le préfixe est affiché en premier pour vous permettre
d’inspecter les segments restants si la suppression échoue après la disparition du manifeste.
Les conteneurs eux-mêmes sont conservés.
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
}
Exécuter la commande complète
Enregistrez ce code sous main.go. Le mode par défaut téléverse un fichier ;
large utilise des segments DLO, limited appelle la
fonction d’encapsulation avec limitation du débit, verify télécharge et
compare les octets, et delete effectue le nettoyage à périmètre limité
décrit ci-dessus. Ctrl+C demande l’annulation. En cas d’erreur, la commande renvoie un statut non
nul sans afficher les informations d’authentification ni les réponses brutes de l’API.
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."
}
Créez un petit fichier de test binaire sans écraser de fichier existant :
(
set -C
cd swift-upload-example &&
printf '\000\377Swift\n' > example.bin
)
Compilez le programme, téléversez ce fichier et vérifiez-le. Remplacez
exports par votre propre conteneur et RegionOne par la
région de votre catalogue dans les deux commandes. La compilation remplace l’exécutable
swift-upload de ce projet ; si elle échoue, le bloc s’arrête avant qu’un
exécutable précédent ne soit lancé.
(
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
)
Sortie attendue pour ce fichier de test de huit octets :
Uploaded exports/example.bin
Verified exports/example.bin: 8 bytes, sha256 2949a2f975f056f2652117015349d0768d9eaffcd9eacd14f81147844c47f158
Pour votre propre fichier plus volumineux, utilisez le même exécutable avec
-mode large, puis vérifiez l’objet assemblé. Téléverser sous le même nom d’objet
remplace son contenu ; les remplacements de DLO conservent les anciennes versions de segments.
Si la vérification échoue parce que la liste des segments du DLO n’a pas convergé, relancez la
vérification sans téléverser à nouveau. Une différence d’empreinte peut aussi signaler des
segments manquants ou modifiés ; inspectez donc le préfixe exact avant de supprimer quoi que ce
soit.
(
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
)
Après avoir arrêté les lectures et les écritures, supprimez la version actuelle qui vous appartient :
(
cd swift-upload-example &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload -mode delete exports backup.bin
)
Lisez le préfixe affiché si le nettoyage échoue et inspectez uniquement cette version qui vous appartient. Une seconde suppression après la disparition du manifeste renvoie 404 ; elle ne peut pas retrouver les segments restants à partir de cet en-tête. Ne supprimez jamais une version voisine au seul motif que son nom commence par le même nom d’objet.
