Fazer upload de arquivos no OpenStack Swift em Go com Gophercloud
Use o Gophercloud v2 para autenticar via Keystone, fazer upload de um arquivo local para o OpenStack Swift e comparar os bytes baixados com o original. O comando abaixo também oferece suporte a Dynamic Large Objects (DLOs), usando um novo prefixo de segmentos a cada upload para que uma substituição menor não possa herdar segmentos antigos.
Pré-requisitos
Você precisa de:
- Go 1.26.9 ou uma versão de correção posterior do Go 1.26 que ainda receba manutenção. Este exemplo foi testado com Go 1.26.9 no Linux. O Go 1.27 também recebe suporte do projeto Go, mas está fora das versões testadas neste exemplo. O histórico de versões do Go explica as correções e o suporte.
- Bash para os comandos de shell abaixo
- Conhecimento básico de programação em Go
- Um ambiente existente com Keystone v3 e Swift, incluindo um endpoint
object-storeno catálogo de serviços da sua região e permissão para criar contêineres e ler, gravar e excluir objetos - O
clouds.yamldo seu provedor, ou credenciais de usuário e senha e o domínio do usuário/projeto
Os exemplos de armazenamento foram testados com Swift 2.38.2 e Keystone 30.0.0 em um ambiente local com um único nó. Eles não estabelecem as cotas, a replicação nem a consistência das listagens de um provedor hospedado. Use um contêiner e um nome de objeto que pertençam a você, sem leitores ou gravadores concorrentes durante a substituição ou exclusão. Nomes de contêineres aceitam letras ASCII, dígitos, sublinhados e hífens; nomes de objetos também aceitam pontos. Caminhos de arquivos locais são separados dos nomes de objetos e podem conter espaços.
Configurar o ambiente Go
Crie um projeto novo. Cada bloco de comandos é executado em um subshell, de modo que falhas preservam
o diretório e as opções do shell que o invocou. GOWORK=off impede que uma área de
trabalho do Go que contenha o projeto selecione outro módulo. Um diretório
swift-upload-example já existente é rejeitado; escolha outro diretório novo e ajuste os
caminhos usados mais adiante, em vez de excluir um projeto 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
)
Instalar o Gophercloud
Instale o Gophercloud v2.15.0,
o módulo utils v2 para carregar credenciais e o limitador de taxa usado pelo
modo limited. Estas são as versões exatas das dependências usadas aqui:
(
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
)
Mantenha tanto o Gophercloud quanto utils nos respectivos caminhos de
importação v2. Se a instalação falhar, execute este bloco novamente a partir do diretório pai do
projeto; não tente recriar o diretório existente. Mantenha os arquivos
go.mod e go.sum juntos para que as compilações seguintes
usem a resolução registrada.
Autenticar no OpenStack Swift
Salve cada bloco Go abaixo no arquivo indicado dentro de swift-upload-example. São arquivos
de código-fonte separados em um único pacote main; mantenha seus blocos de
importação exatamente como apresentados.
Salve o arquivo de credenciais do seu provedor como swift-upload-example/clouds.yaml. Para autenticação
Keystone v3 baseada em senha, a entrada tem esta estrutura; substitua todos os valores de exemplo
pelos valores do seu provedor:
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
Mantenha este arquivo fora do controle de versão e permita que apenas você possa lê-lo. Os comandos
selecionam a entrada openstack com OS_CLOUD e selecionam o
endpoint do catálogo Swift com OS_REGION_NAME; substitua
RegionOne pela sua região. Para uma CA privada, configure
SSL_CERT_FILE para usar o pacote PEM dessa CA no processo. Mantenha a verificação de
certificados TLS habilitada.
Salve isto como auth.go. Por padrão, é usado
clouds.yaml; a alternativa -auth env lê
OS_AUTH_URL, OS_USERNAME, OS_PASSWORD,
OS_PROJECT_NAME e OS_DOMAIN_NAME do seu ambiente. Essa alternativa
simplificada pressupõe que o usuário e o projeto compartilham um domínio; use YAML quando eles
forem diferentes.
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
}
Fazer upload de arquivos para contêineres do Swift
Garantir que o contêiner exista
Salve isto como container.go. Crie um contêiner ausente somente após um 404; uma
requisição negada é retornada ao chamador. A função ResponseCodeIs do Gophercloud
verifica o status HTTP mesmo em erros encapsulados:
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
}
Fazer upload de um arquivo
Salve isto como upload.go. A entrada deve ser um arquivo regular e permanecer
inalterada durante todo o upload e a verificação. O exemplo armazena esse arquivo 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
}
Por padrão, o Gophercloud calcula um ETag MD5 antes do upload.
Para um *os.File com suporte a reposicionamento, ele calcula o hash e volta ao
início do arquivo, lendo-o duas vezes sem carregar o arquivo inteiro na memória. Já os dados de um
leitor sem suporte a reposicionamento são armazenados em buffer. O comando de verificação
apresentado mais adiante compara os hashes SHA-256 do arquivo local completo e do objeto baixado,
incluindo arquivos vazios.
Lógica de novas tentativas de upload
Salve isto como retry.go. Para respostas 429, 502, 503 e 504, faça até três
tentativas, aguardando 1 e 2 segundos entre elas. Outras falhas são retornadas imediatamente. Cada
tentativa reabre o arquivo 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)
}
As novas tentativas usam o mesmo nome de objeto e substituem qualquer objeto que já esteja lá. Uma resposta perdida pode deixar incerto o resultado da gravação, então inspecione o objeto antes de presumir que um status de término diferente de zero significa que nada foi armazenado.
Fazer upload de um objeto com novas tentativas (usando createopts)
Salve isto como object-retry.go. Os segmentos usam um novo leitor de bytes a cada
tentativa; reutilizar um leitor já consumido por uma requisição anterior pode enviar um corpo vazio
na nova tentativa.
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)
}
Lidar com arquivos grandes usando uploads segmentados
Salve isto como large.go. Um manifesto DLO é um objeto de zero bytes cujo
cabeçalho X-Object-Manifest especifica um contêiner de segmentos e um prefixo. O Swift
concatena todos os segmentos correspondentes na ordem lexicográfica dos nomes, por isso os nomes
abaixo usam números com preenchimento. Use CreateOpts.ObjectManifest, e não metadados
personalizados, para definir o cabeçalho. Consulte a
documentação de DLO do Swift.
Um novo prefixo aleatório isola cada versão. Reutilizar <objectName>/ deixaria
segmentos excedentes no final quando a substituição fosse menor e alteraria o conteúdo do manifesto
antigo antes da publicação. O manifesto é gravado após os segmentos. A montagem do DLO depende das
listagens de contêineres, que têm consistência eventual em uma implantação distribuída do Swift;
publicar um manifesto, por si só, não comprova que um leitor já consegue recuperar todos os bytes.
Execute o verificador de bytes antes de considerar o processo concluído.
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
}
Uploads que falharam e versões substituídas deixam segmentos para trás. Não limpe
<objectName>/: esse prefixo também contém versões ativas e dados de outras execuções.
Uma resposta que deixa incerta a gravação do manifesto é especialmente perigosa, pois o servidor
pode tê-lo publicado apesar da falha do chamador. Uma coleta de lixo genérica exige propriedade
comprovada, verificações das referências atuais e um período de carência maior que o tempo de
atividade dos uploads e dos leitores; este exemplo não implementa esse coletor.
Um DELETE comum remove um manifesto DLO, mas mantém seus segmentos. A operação
?multipart-manifest=delete do SLO não é uma operação de limpeza de DLO; o
manipulador de SLO do Swift 2.38.2 retorna HTTP 200 na resposta externa,
com um corpo de exclusão em lote que informa 400, deixando o DLO intacto. O comando
-mode delete abaixo remove apenas o manifesto atual e o prefixo exato da sua versão.
Ele pressupõe propriedade exclusiva e leitores e gravadores interrompidos, e deixa versões antigas
ou que falharam para uma inspeção separada.
Tratamento de erros e boas práticas
Limitação de taxa
Salve esta implementação de um tipo encapsulador de cliente como rate.go.
Ela limita a admissão de uploads de arquivos inteiros quando reutilizada por código chamador de
execução mais longa. Ela não limita cada nova tentativa ou requisição segmentada; uma única
invocação da CLI não comprova uma taxa de requisições sustentada.
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)
}
Verificar e excluir um objeto enviado por upload
Salve isto como verify.go. Calcule separadamente os hashes do arquivo local e do
corpo completo baixado. O ETag de um DLO não é o MD5 do arquivo original, portanto comparar esses
dois ETags seria uma verificação incorreta.
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
}
Salve isto como cleanup.go. Verifique se um DLO faz referência ao contêiner de
segmentos e ao nome de objeto usados por este comando e a uma única versão aleatória. Liste esse
prefixo exato antes de remover o manifesto e, em seguida, exclua apenas esses segmentos. O prefixo
é impresso primeiro para que você possa inspecionar os segmentos restantes se a exclusão falhar
após a remoção do manifesto. Os próprios contêineres são mantidos.
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
}
Executar o comando completo
Salve isto como main.go. O modo padrão faz upload de um arquivo;
large usa segmentos DLO, limited chama o encapsulador
de cliente com limitação de taxa, verify baixa e compara os bytes, e
delete realiza a limpeza com escopo delimitado descrita acima. Ctrl+C solicita
o cancelamento. Um erro retorna um status diferente de zero sem imprimir credenciais nem respostas
brutas da 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."
}
Crie um pequeno arquivo binário de teste sem sobrescrever um arquivo existente:
(
set -C
cd swift-upload-example &&
printf '\000\377Swift\n' > example.bin
)
Compile o programa, faça upload do arquivo e verifique-o. Altere exports para
um contêiner que pertença a você e RegionOne para a região do seu catálogo nos
dois comandos. A compilação substitui o executável swift-upload deste projeto; uma
falha na compilação interrompe o bloco antes que qualquer executável anterior seja executado.
(
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
)
Saída esperada para esse arquivo de teste de oito bytes:
Uploaded exports/example.bin
Verified exports/example.bin: 8 bytes, sha256 2949a2f975f056f2652117015349d0768d9eaffcd9eacd14f81147844c47f158
Para um arquivo seu de tamanho maior, use o mesmo executável com -mode large e,
em seguida, verifique o objeto montado. Fazer upload com o mesmo nome de objeto substitui seu
conteúdo; substituições de DLO mantêm as versões antigas dos segmentos. Se a verificação falhar
porque a listagem do DLO ainda não convergiu, repita a verificação sem fazer outro upload. Uma
divergência entre hashes também pode indicar segmentos ausentes ou alterados, então inspecione o
prefixo exato antes de excluir qualquer coisa.
(
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
)
Depois de interromper os leitores e os gravadores, remova a versão atual que pertence a você:
(
cd swift-upload-example &&
OS_CLOUD=openstack OS_REGION_NAME=RegionOne ./swift-upload -mode delete exports backup.bin
)
Leia o prefixo impresso se a limpeza falhar e inspecione apenas essa versão que pertence a você. Uma segunda exclusão após a remoção do manifesto retorna 404; ela não consegue descobrir segmentos restantes a partir daquele cabeçalho. Nunca exclua uma versão vizinha apenas porque seu nome começa com o mesmo nome de objeto.
