Importa archivos de Backblaze con Go: técnicas eficientes
Este ejemplo importa los objetos de un prefijo de un bucket existente de Backblaze B2 a un nuevo directorio local privado. Usa el cliente comunitario Blazer, fijado en v0.5.3, con solicitudes que respetan el contexto y un iterador paginado. Por diseño, solo admite objetos con un SHA-1 del archivo completo.
Comprende la API de Backblaze B2
Blazer implementa la versión 1 de la API nativa de B2. Es un cliente antiguo de terceros, no un SDK oficial de Backblaze para Go. Backblaze sigue ofreciendo soporte para versiones anteriores de la API, pero las claves de aplicación para varios buckets requieren autorización de la versión 4 y no pueden usarse con este cliente. Usa una clave existente compatible con la versión 1 para este ejemplo de mantenimiento; elige un cliente actual cuando tu integración requiera tipos de claves más recientes.
Política de compatibilidad de versiones de la API y claves de Backblaze
API y código fuente de Blazer v0.5.3
Configura tu entorno de Go para importar archivos de Backblaze
Usa Go 1.22 o una versión posterior y un directorio de proyecto nuevo. El paquete B2 de esta versión usa la biblioteca estándar sin dependencias adicionales en tiempo de ejecución.
mkdir backblaze-import
cd backblaze-import
go mod init backblaze-import
go mod edit -go=1.22
GOTOOLCHAIN=local go get github.com/kurin/blazer/b2@v0.5.3
Usa el SDK de Go para conectarte a Backblaze B2
Asigna a B2_KEY_ID el ID de tu clave de aplicación y a
B2_APPLICATION_KEY su secreto mediante tu gestor de secretos o el entorno de tu shell.
Concede a la clave acceso al bucket existente y las capacidades listBuckets,
listFiles y readFiles. El primer argumento de autenticación
es el ID de la clave; no es el ID del bucket. El programa nunca crea un bucket.
Implementa la funcionalidad básica de importación de archivos en Go
Guarda este programa completo como main.go. Cada descarga se verifica con el
tamaño y el SHA-1 de sus metadatos, se sincroniza, se cierra y se renombra dentro del mismo directorio
privado. Aquí, SHA-1 verifica la integridad de la transferencia; no es una firma que demuestre quién
creó el archivo.
package main
import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strings"
"time"
"github.com/kurin/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(100))
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
}
return directory, ctx.Err()
}
func run() error {
if len(os.Args) != 4 {
return errors.New("usage: backblaze-import BUCKET PREFIX EXISTING_LOCAL_PARENT")
}
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")
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
ctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
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)
}
}
GOTOOLCHAIN=local go build -o backblaze-import .
./backblaze-import "${B2_BUCKET:?Set B2_BUCKET}" "${B2_PREFIX:-}" "${LOCAL_PARENT:?Set an existing local directory}"
Gestiona directorios e importaciones por lotes: prácticas recomendadas
Los nombres de objetos de B2 son claves, no rutas de confianza del sistema de archivos. Este ejemplo
escribe archivos .bin numerados y muestra la correspondencia entre cada
nombre de archivo local y su clave remota entre comillas. Por lo tanto, un objeto llamado
../report o /absolute/path no puede salir del nuevo directorio de
importación. El iterador obtiene las páginas siguientes automáticamente; debes comprobar
iterator.Err() después de la iteración.
Gestiona errores y depura para transferir archivos sin problemas
Blazer gestiona su propia clasificación de reintentos HTTP y la renovación de tokens. No lo envuelvas en un bucle de reintentos incondicional: los fallos de permisos y la ausencia de buckets deben devolverse como errores. Todas las llamadas al SDK comparten un contexto cancelable con un plazo máximo global de treinta minutos, incluidas las esperas entre reintentos del SDK. Se elimina el archivo temporal del objeto que falla; los objetos anteriores descargados correctamente siguen disponibles.
Supervisa el progreso de las transferencias de archivos grandes
El programa muestra un registro de finalización por cada archivo verificado. No considera que los bytes recibidos hasta ese momento constituyan una importación completa. Los objetos subidos mediante la interfaz de B2 para archivos grandes pueden carecer de un SHA-1 del archivo completo; en ese caso, este ejemplo falla explícitamente en lugar de presentar datos sin comprobar como verificados.
Problemas frecuentes y consejos para solucionarlos
Mantén estable el prefijo de origen durante la importación. Un listado no es una instantánea atómica de un bucket que está cambiando; las comprobaciones de tamaño y suma de verificación detectan cambios en el contenido durante cada descarga, pero no congelan el listado en sí.
Gestión de la memoria
El lector del SDK usa una descarga concurrente y fragmentos de 1 MiB. Los archivos se transfieren directamente al disco en lugar de acumularse en un segmento de bytes. Solo se procesa un objeto a la vez y cada lector se cierra tanto si la operación tiene éxito como si falla.
Limitación de frecuencia de solicitudes
El procesamiento secuencial de objetos acota la concurrencia de las solicitudes. El cliente fijado a esa versión respeta las esperas entre reintentos para las respuestas que los admiten; el plazo máximo global del contexto limita la espera total. Si se limita la frecuencia de solicitudes de la cuenta de forma persistente, reduce la carga de trabajo o programa otra ejecución después de resolver la limitación.
Gestión de las interrupciones de red
Ctrl-C cancela las solicitudes reales del SDK y las esperas entre reintentos. La cancelación no se implementa abandonando una goroutine mientras una descarga sigue escribiendo. Una ejecución posterior crea un nuevo directorio de importación en lugar de sobrescribir los resultados parciales de un lote anterior.
Descargas concurrentes con limitación de frecuencia
Mantén secuencial este bucle por lotes, salvo que las mediciones justifiquen importar objetos en
paralelo. El SDK ya permite una concurrencia acotada de fragmentos mediante
Reader.ConcurrentDownloads. Aumentarla también incrementa el consumo de memoria y la carga de
solicitudes; no implementa un límite de frecuencia de solicitudes para toda la cuenta.
Prácticas recomendadas para gestionar el ciclo de vida de los buckets
Localiza un bucket existente antes de crear la salida local. La ausencia de buckets y los fallos de
permisos devuelven errores, y un bucket nil se rechaza como medida
preventiva. La creación de buckets y los cambios en su ciclo de vida deben realizarse en un paso de
aprovisionamiento separado, para que un error tipográfico durante una importación no pueda crear un
recurso no deseado.
Conclusión y recursos adicionales
Este importador combina la cancelación efectiva y la paginación del SDK con transferencias en streaming acotadas y publicación local verificada. Conserva los archivos descargados correctamente si se produce un fallo posterior e indica el directorio privado de importación para que puedas inspeccionarlo.
