Importar arquivos do Backblaze em Go: técnicas eficientes
Este exemplo importa um prefixo de um bucket existente do Backblaze B2 para um novo diretório local privado. Ele usa o cliente paginado da API nativa do Blazer e verifica o tamanho e o SHA-1 de cada arquivo inteiro antes de publicá-lo localmente. Nomes de arquivo numerados mantêm as chaves remotas fora dos caminhos do sistema de arquivos.
Entender a API do Backblaze B2
O repositório original kurin/blazer não é mais mantido. Seu sucessor é o
repositório Blazer da Backblaze. Este exemplo fixa a versão
v0.8.0, a versão atual com tag, que usa a API nativa v3. Esta é uma
integração com a API nativa para um bucket existente; os mantenedores do Blazer recomendam um
cliente S3 para integrações que não precisam dessa API.
Use uma chave de aplicação existente compatível com a v3. Chaves de aplicação para vários buckets exigem autorização v4 e não funcionam com esta versão, mesmo que selecionem apenas um bucket. A política de versões da API da Backblaze descreve essa distinção e a continuidade do suporte às versões anteriores da API.
Configurar o ambiente Go para importar arquivos do Backblaze
Os comandos abaixo usam Bash no Linux e foram testados com Go 1.27.3. Instale o patch de segurança mais recente da linha Go 1.27 na página de downloads do Go. O pacote B2 não tem dependências externas em tempo de execução.
Cole estes comandos a partir de um diretório com permissão de escrita. O subshell preserva seu
diretório atual e para em caso de falha; um diretório backblaze-import existente não é
alterado. GOWORK=off mantém um workspace Go que englobe esse diretório fora deste
projeto independente.
(
command -v go >/dev/null &&
mkdir backblaze-import &&
cd backblaze-import &&
GOWORK=off GOTOOLCHAIN=local go mod init backblaze-import &&
GOWORK=off GOTOOLCHAIN=local go get github.com/Backblaze/blazer/b2@v0.8.0
)
Usar o SDK Go para se conectar ao Backblaze B2
Defina B2_KEY_ID com o ID da sua chave de aplicação e
B2_APPLICATION_KEY com o valor secreto dela usando seu gerenciador de segredos ou o
ambiente do shell. A chave precisa de acesso ao bucket existente e das capacidades
listBuckets, listFiles e readFiles.
A consulta ao bucket passa o nome dele, por isso também funciona com uma chave restrita a um único
bucket. Se a chave restringir os nomes de arquivo, B2_PREFIX deve permanecer
dentro dessa restrição. O primeiro argumento de credencial é o ID da chave, não o ID do bucket.
Consulte os requisitos de consulta ao bucket.
Defina B2_BUCKET com o nome do bucket, B2_PREFIX com um
prefixo não vazio e LOCAL_PARENT com um diretório local existente com permissão de
escrita. Inclua / no final ao selecionar uma pasta virtual:
invoices/ exclui invoices-old/. O bucket já deve conter os
objetos que você quer importar, e esses objetos devem ter um SHA-1 do arquivo inteiro nos metadados.
Mantenha esse prefixo estável enquanto o comando estiver em execução.
Implementar a funcionalidade básica de importação de arquivos em Go
Salve este programa completo como backblaze-import/main.go. Cada download é verificado em
relação ao tamanho e ao SHA-1 dos metadados, sincronizado, fechado e renomeado dentro do mesmo
diretório privado. Aqui, o SHA-1 verifica a integridade da transferência; ele não é uma assinatura
que comprova quem criou o arquivo.
package main
import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strings"
"time"
"github.com/Backblaze/blazer/b2"
)
func importObject(ctx context.Context, object *b2.Object, directory string, number int) error {
attrs, err := object.Attrs(ctx)
if err != nil {
return err
}
digest, err := hex.DecodeString(attrs.SHA1)
if err != nil || len(digest) != sha1.Size || attrs.Size < 0 {
return errors.New("object needs a whole-file SHA-1; large files without one are unsupported")
}
reader := object.NewReader(ctx)
reader.ConcurrentDownloads = 1
reader.ChunkSize = 1 << 20
defer reader.Close()
tmp, err := os.CreateTemp(directory, ".import-")
if err != nil {
return err
}
defer os.Remove(tmp.Name())
defer tmp.Close()
hash := sha1.New()
n, err := io.Copy(io.MultiWriter(tmp, hash), io.LimitReader(reader, attrs.Size+1))
if err != nil {
return err
}
if n != attrs.Size || hex.EncodeToString(hash.Sum(nil)) != strings.ToLower(attrs.SHA1) {
return errors.New("object changed or download integrity check failed")
}
if err := reader.Close(); err != nil {
return err
}
if err := tmp.Sync(); err != nil {
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
// Remote names never become local paths; even ../ and absolute keys stay contained.
name := fmt.Sprintf("%06d.bin", number)
if err := os.Rename(tmp.Name(), filepath.Join(directory, name)); err != nil {
return err
}
fmt.Printf("%s\t%q\t%d bytes\n", name, object.Name(), n)
return nil
}
func importPrefix(ctx context.Context, client *b2.Client, bucketName, prefix, parent string) (string, error) {
bucket, err := client.Bucket(ctx, bucketName)
if err != nil {
return "", err
}
if bucket == nil {
return "", errors.New("bucket lookup returned no bucket")
}
directory, err := os.MkdirTemp(parent, "b2-import-")
if err != nil {
return "", err
}
// Keep successful files after a later error. Never erase an earlier import.
iterator := bucket.List(ctx, b2.ListPrefix(prefix), b2.ListPageSize(2))
number := 0
for iterator.Next() {
if err := ctx.Err(); err != nil {
return directory, err
}
number++
if err := importObject(ctx, iterator.Object(), directory, number); err != nil {
return directory, fmt.Errorf("object %d: %w", number, err)
}
}
if err := iterator.Err(); err != nil {
return directory, err
}
if err := ctx.Err(); err != nil {
return directory, err
}
fmt.Printf("Imported %d verified files\n", number)
return directory, nil
}
func run() error {
if len(os.Args) != 4 {
return errors.New("usage: backblaze-import BUCKET PREFIX EXISTING_LOCAL_PARENT")
}
if os.Args[1] == "" || os.Args[2] == "" {
return errors.New("bucket and prefix must be nonempty")
}
keyID, key := os.Getenv("B2_KEY_ID"), os.Getenv("B2_APPLICATION_KEY")
if keyID == "" || key == "" {
return errors.New("set B2_KEY_ID and B2_APPLICATION_KEY")
}
timeout := 30 * time.Minute
if value := os.Getenv("B2_IMPORT_TIMEOUT"); value != "" {
var err error
timeout, err = time.ParseDuration(value)
if err != nil || timeout <= 0 {
return errors.New("B2_IMPORT_TIMEOUT must be a positive duration, such as 30m")
}
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
client, err := b2.NewClient(ctx, keyID, key)
if err != nil {
return err
}
directory, err := importPrefix(ctx, client, os.Args[1], os.Args[2], os.Args[3])
if directory != "" {
fmt.Fprintln(os.Stderr, "Import directory:", directory)
}
return err
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
(
cd backblaze-import &&
GOWORK=off GOTOOLCHAIN=local go build -o backblaze-import . &&
./backblaze-import "${B2_BUCKET:?Set B2_BUCKET}" "${B2_PREFIX:?Set B2_PREFIX}" "${LOCAL_PARENT:?Set LOCAL_PARENT}"
)
O comando imprime o diretório de importação na saída de erro padrão. A saída padrão mapeia cada
arquivo numerado para sua chave B2 entre aspas e sua contagem de bytes, seguida de
Imported N verified files em caso de sucesso. O código de saída zero significa que todos os
objetos retornados pela listagem passaram na verificação. Um prefixo sem objetos correspondentes
resulta em uma importação bem-sucedida de zero arquivos. Mantenha o mapeamento junto dos arquivos
se precisar dos nomes originais.
Lidar com diretórios e importações em lote: boas práticas
Os nomes de objetos B2 são chaves, não caminhos confiáveis do sistema de arquivos. Este exemplo
grava arquivos .bin numerados e imprime um mapeamento de cada nome de
arquivo local para sua chave remota entre aspas. Portanto, um objeto chamado
../report ou /absolute/path não pode escapar do novo diretório
de importação. O iterador busca as páginas seguintes automaticamente;
iterator.Err() deve ser verificado após a iteração. O exemplo solicita dois nomes
por página para que uma pequena coleção exercite a paginação. Isso exige mais requisições de
listagem do que uma página maior; não limita o número de objetos importados.
Tratar erros e depurar: garantir transferências de arquivos sem problemas
O Blazer cuida da própria classificação de novas tentativas HTTP e da renovação de tokens. Não
adicione um laço de novas tentativas incondicionais ao redor dele: falhas de permissão e buckets
inexistentes devem ser retornados. Todas as chamadas do SDK compartilham um contexto cancelável
com um prazo total de trinta minutos, incluindo autenticação, corpos transmitidos em fluxo e os
intervalos de espera entre novas tentativas do SDK. Uma transferência lenta pode atingir esse
prazo independentemente do tamanho do arquivo. O arquivo temporário de um objeto que falhou é
removido; os objetos anteriores processados com sucesso permanecem disponíveis.
Defina B2_IMPORT_TIMEOUT com uma duração positiva do Go, como
2h, antes de executar o comando quando a importação inteira precisar
de um prazo diferente. A compilação do executável acontece antes de esse temporizador começar.
Acompanhar o progresso de transferências de arquivos grandes
O programa imprime um registro de conclusão por arquivo verificado. Ele não apresenta os bytes recebidos até o momento como uma importação concluída. Para objetos enviados pela interface de arquivos grandes do B2, o SHA-1 do arquivo inteiro pode estar ausente; este exemplo falha explicitamente em vez de apresentar dados não verificados como verificados.
Problemas comuns e dicas de solução
Mantenha o prefixo de origem estável durante a importação. Uma listagem não é um snapshot atômico de um bucket em alteração; as verificações de tamanho e checksum detectam conteúdo alterado durante cada download, mas não congelam a própria listagem.
Gerenciamento de memória
O leitor do SDK usa um download por vez e blocos de 1 MiB. Os arquivos são gravados em fluxo no disco em vez de serem acumulados em um slice de bytes. Apenas um objeto é processado por vez, e cada leitor é fechado tanto em caso de sucesso quanto de falha.
Limitação de taxa
O processamento sequencial dos objetos limita a simultaneidade das requisições. O cliente na versão fixada respeita os intervalos de espera entre novas tentativas para respostas que permitem repetição; o prazo total do contexto limita a espera total. Se a conta sofrer limitação persistente, reduza a carga de trabalho ou agende outra execução após resolver a limitação.
Tratamento de interrupções de rede
Ctrl+C cancela as requisições reais do SDK e as esperas entre novas tentativas. O cancelamento não é implementado abandonando uma goroutine enquanto um download continua gravando. Uma execução posterior cria um novo diretório de importação em vez de sobrescrever resultados parciais de um lote anterior.
Manter os downloads de objetos sequenciais
Mantenha este laço de processamento em lote sequencial, a menos que medições justifiquem
importações paralelas de objetos. O SDK já oferece simultaneidade limitada de blocos por meio de
Reader.ConcurrentDownloads. Aumentá-la também aumenta o consumo de memória e a pressão das
requisições; isso não implementa um limite de taxa de requisições para toda a conta.
Manter o provisionamento de buckets separado
Localize um bucket existente antes de criar a saída local. Buckets inexistentes e falhas de
permissão retornam erros, e um bucket nil é rejeitado como medida de
proteção. A criação de buckets e as alterações de ciclo de vida devem ficar em uma etapa separada
de provisionamento, para que um erro de digitação durante uma importação não crie um recurso
indesejado.
Escolher a próxima etapa
Use este comando quando sua aplicação Go precisar de cópias locais verificadas obtidas pela API nativa do B2. Se a próxima etapa for um fluxo de processamento hospedado, o serviço de importação de arquivos da Transloadit descreve as opções de importação disponíveis.
