Subida de archivos a OpenStack Swift en Go con Gophercloud
Usa Gophercloud v2 para autenticarte mediante Keystone, subir un archivo local a OpenStack Swift y comparar los bytes descargados con el original. El comando siguiente también admite Dynamic Large Objects (DLO), con un prefijo de segmento nuevo para cada subida, de modo que un reemplazo más corto no pueda heredar segmentos antiguos.
Requisitos previos
Necesitas:
- Go 1.26.9 o una versión de parche posterior de Go 1.26 que siga recibiendo mantenimiento. Este ejemplo se probó con Go 1.26.9 en Linux. Go 1.27 también cuenta con soporte por parte del proyecto, pero queda fuera del rango probado para este ejemplo. El historial de versiones de Go explica las correcciones de los parches y el soporte.
- Bash para los comandos de shell siguientes
- Conocimientos básicos de programación en Go
- Un entorno existente de Keystone v3 y Swift, con un endpoint
object-storeen el catálogo de servicios de tu región y permisos para crear contenedores y leer, escribir y eliminar objetos - El
clouds.yamlde tu proveedor, o credenciales de usuario y contraseña y el dominio del usuario y del proyecto
Los ejemplos de almacenamiento se probaron con Swift 2.38.2 y Keystone 30.0.0 en un entorno local de un solo nodo. No permiten establecer las cuotas, la replicación ni la consistencia de los listados de un proveedor de servicios alojados. Usa un contenedor y un nombre de objeto que te pertenezcan, sin lectores ni escritores concurrentes durante el reemplazo o la eliminación. Los nombres de contenedores admiten letras ASCII, dígitos, guiones bajos y guiones; los nombres de objetos también admiten puntos. Las rutas de archivos locales son independientes de los nombres de objetos y pueden contener espacios.
Configura el entorno de Go
Crea un proyecto nuevo. Cada bloque de shell se ejecuta en un subshell, de modo que los fallos
conservan el directorio y las opciones del shell desde el que lo ejecutas.
GOWORK=off evita que un espacio de trabajo de Go que lo englobe seleccione otro
módulo. Si el directorio swift-upload-example ya existe, se rechaza; elige otro directorio
nuevo y ajusta las rutas posteriores en lugar de eliminar un proyecto existente.
(
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
)
Instala Gophercloud
Instala Gophercloud v2.15.0, el módulo v2
utils para cargar credenciales y el limitador de tasa que usa el modo
limited. Estas son las versiones exactas de las dependencias utilizadas aquí:
(
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
)
Mantén tanto Gophercloud como utils en sus rutas de importación v2. Si la
instalación falla, vuelve a ejecutar este bloque desde el directorio padre del proyecto; no vuelvas
a ejecutar la creación del directorio sobre el existente. Conserva juntos su
go.mod y su go.sum para que las compilaciones
posteriores usen la resolución de dependencias registrada.
Autentícate con OpenStack Swift
Guarda cada bloque de Go siguiente en el archivo indicado dentro de swift-upload-example.
Son archivos de código fuente separados en un mismo paquete main; conserva
sus bloques de importación tal como se muestran.
Guarda el archivo de credenciales de tu proveedor como swift-upload-example/clouds.yaml. Para la
autenticación de Keystone v3 mediante contraseña, su entrada tiene esta estructura; reemplaza cada
marcador de posición con los valores de tu proveedor:
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
Mantén este archivo fuera del control de versiones y permite que solo tú puedas leerlo. Los
comandos seleccionan su entrada openstack mediante
OS_CLOUD y el endpoint de Swift del catálogo mediante
OS_REGION_NAME; reemplaza RegionOne con tu región. Para una CA
privada, establece SSL_CERT_FILE para el proceso con la ruta de su paquete PEM.
Mantén habilitada la verificación de certificados TLS.
Guarda esto como auth.go. La ruta predeterminada usa
clouds.yaml; la alternativa -auth env lee
OS_AUTH_URL, OS_USERNAME, OS_PASSWORD,
OS_PROJECT_NAME y OS_DOMAIN_NAME de tu entorno. Esta alternativa mínima
supone que el usuario y el proyecto comparten un dominio; usa YAML cuando sean distintos.
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
}
Sube archivos a contenedores de Swift
Asegúrate de que el contenedor exista
Guarda esto como container.go. Crea un contenedor inexistente solo después de un
404; una solicitud denegada se devuelve al código que hizo la llamada. La función
ResponseCodeIs de Gophercloud comprueba el estado HTTP a través de errores envueltos:
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
}
Sube un archivo
Guarda esto como upload.go. La entrada debe ser un archivo regular y permanecer
sin cambios durante toda la subida y la verificación. El ejemplo lo almacena como
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
}
De forma predeterminada,
Gophercloud calcula un ETag MD5 antes de subir el archivo.
Para un *os.File que permite reposicionar la lectura, calcula el hash y vuelve
al inicio del archivo, leyéndolo dos veces sin cargarlo completo en memoria. Si el lector no permite
reposicionar la lectura, usa un búfer. El comando de verificación posterior compara los hashes
SHA-256 del archivo local completo y del objeto descargado, incluidos los archivos vacíos.
Lógica de reintentos para las subidas
Guarda esto como retry.go. Reintenta las solicitudes con respuestas 429, 502,
503 y 504 hasta un máximo de tres intentos, con esperas de 1 y 2 segundos entre ellos. Los demás
fallos se devuelven de inmediato. Cada intento vuelve a abrir el archivo de entrada.
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)
}
Los reintentos usan el mismo nombre de objeto y reemplazan cualquier objeto que ya exista allí. Una respuesta perdida puede dejar incierto el resultado de la escritura, así que inspecciona el objeto antes de suponer que un código de salida distinto de cero significa que no se almacenó nada.
Sube un objeto con reintentos (con createopts)
Guarda esto como object-retry.go. Los segmentos usan un lector de bytes nuevo en cada
intento; reutilizar un lector agotado por una solicitud anterior puede enviar un cuerpo vacío al
reintentar.
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)
}
Gestiona archivos grandes con subidas segmentadas
Guarda esto como large.go. Un manifiesto DLO es un objeto de cero bytes cuyo
encabezado X-Object-Manifest indica un contenedor de segmentos y un prefijo. Swift
concatena todos los segmentos coincidentes en orden lexicográfico por nombre, por lo que los
nombres siguientes usan números con relleno. Usa CreateOpts.ObjectManifest, en lugar de
metadatos personalizados, para establecer el encabezado. Consulta la
documentación de DLO de Swift.
Un prefijo aleatorio nuevo aísla cada versión. Reutilizar <objectName>/ dejaría
segmentos sobrantes al final cuando un reemplazo fuera más corto y cambiaría el contenido del
manifiesto anterior antes de publicar el nuevo. El manifiesto se escribe después de los segmentos.
El ensamblado de DLO depende de los listados de contenedores, que tienen consistencia eventual en un
despliegue distribuido de Swift; publicar un manifiesto por sí solo no demuestra que un lector ya
pueda recuperar todos los bytes. Ejecuta el verificador de bytes antes de dar el objeto por completo.
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
}
Las subidas fallidas y las versiones reemplazadas dejan segmentos. No elimines indiscriminadamente
el contenido de <objectName>/: también contiene versiones en uso y otras
ejecuciones. Una respuesta incierta al escribir el manifiesto es especialmente peligrosa, porque el
servidor puede haberlo publicado pese al fallo de quien hizo la llamada. La recolección general de
segmentos sin uso requiere comprobar la propiedad, verificar las referencias actuales y establecer
un período de gracia más largo que la duración de las subidas y lecturas activas; este ejemplo no
implementa ese recolector.
Una operación DELETE normal elimina un manifiesto DLO y conserva sus segmentos. La operación
?multipart-manifest=delete de SLO no sirve para limpiar un DLO; el
manejador de SLO de Swift 2.38.2 devuelve un HTTP 200 externo con un
cuerpo de eliminación masiva que informa un 400 y deja intacto el DLO. El comando
-mode delete siguiente elimina únicamente el manifiesto actual y el prefijo exacto
de su versión. Supone que tienes la propiedad exclusiva y que los lectores y escritores están
detenidos; deja las versiones anteriores o fallidas para una inspección independiente.
Manejo de errores y buenas prácticas
Limitación de tasa
Guarda esta función envolvente como rate.go. Limita la admisión de subidas de
archivos completos cuando se reutiliza desde un programa de mayor duración. No limita cada
reintento ni cada solicitud segmentada; una sola invocación de la CLI no demuestra una tasa de
solicitudes sostenida.
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)
}
Verifica y elimina un objeto subido
Guarda esto como verify.go. Calcula por separado los hashes del archivo local
y del cuerpo completo descargado. El ETag de un DLO no es el MD5 del archivo original, por lo que
comparar esos dos ETag sería una comprobación incorrecta.
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
}
Guarda esto como cleanup.go. Comprueba que un DLO haga referencia al contenedor
de segmentos y al nombre de objeto de este comando, y a una sola versión aleatoria. Lista ese
prefijo exacto antes de eliminar el manifiesto y después elimina solo esos segmentos. El prefijo se
imprime primero para que puedas inspeccionar los segmentos restantes si la eliminación falla una
vez que el manifiesto ya no existe. Los contenedores se conservan.
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
}
Ejecuta el comando completo
Guarda esto como main.go. El modo predeterminado sube un archivo;
large usa segmentos DLO, limited llama a la función
envolvente con limitación de tasa, verify descarga y compara los bytes, y
delete realiza la limpieza delimitada descrita antes. Ctrl+C solicita la
cancelación. Un error devuelve un estado distinto de cero sin imprimir credenciales ni respuestas
sin procesar de la 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."
}
Crea un pequeño archivo binario de prueba sin sobrescribir un archivo existente:
(
set -C
cd swift-upload-example &&
printf '\000\377Swift\n' > example.bin
)
Compila el programa, sube el archivo y verifícalo. Cambia exports por un
contenedor que te pertenezca y RegionOne por la región de tu catálogo en ambos
comandos. La compilación reemplaza el ejecutable swift-upload de este proyecto; si
falla, el bloque se detiene antes de ejecutar cualquier ejecutable anterior.
(
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
)
Salida esperada para ese archivo de prueba de ocho bytes:
Uploaded exports/example.bin
Verified exports/example.bin: 8 bytes, sha256 2949a2f975f056f2652117015349d0768d9eaffcd9eacd14f81147844c47f158
Para tu propio archivo de mayor tamaño, usa el mismo ejecutable con -mode large
y después verifica el objeto ensamblado. Subir un objeto con el mismo nombre reemplaza su contenido;
los reemplazos de DLO conservan las versiones anteriores de los segmentos. Si la verificación falla
porque el listado del DLO aún no ha convergido, vuelve a verificar sin subir el archivo otra vez.
Una discrepancia entre los hashes también puede indicar que faltan segmentos o que han cambiado,
así que inspecciona el prefijo exacto antes de eliminar nada.
(
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
)
Después de detener los lectores y escritores, elimina la versión actual que te pertenece:
(
cd swift-upload-example &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload -mode delete exports backup.bin
)
Lee el prefijo impreso si la limpieza falla e inspecciona únicamente esa versión que te pertenece. Una segunda eliminación después de que el manifiesto ya no exista devuelve un 404; no puede descubrir los segmentos sobrantes a partir de ese encabezado. Nunca elimines una versión vecina solo porque su nombre empieza con el mismo nombre de objeto.
