Importa archivos de Backblaze en Go: técnicas eficientes
Este ejemplo importa un prefijo de un bucket existente de Backblaze B2 a un nuevo directorio local privado. Usa el cliente paginado de la API nativa de Blazer y verifica el tamaño y el SHA-1 de cada archivo completo antes de publicarlo localmente. Los nombres de archivo numerados evitan incluir las claves remotas en las rutas del sistema de archivos.
Comprende la API de Backblaze B2
El repositorio original kurin/blazer ya no recibe mantenimiento. Su sucesor es
el repositorio Blazer de Backblaze. Este ejemplo fija su versión
etiquetada actual, v0.8.0, que usa la API nativa v3. Es una integración
con la API nativa para un bucket existente; quienes mantienen Blazer recomiendan un cliente S3
para las integraciones que no necesitan esta API.
Usa una clave de aplicación existente compatible con v3. Las claves de aplicación para varios buckets requieren autorización v4 y no funcionan con esta versión, aunque seleccionen un solo bucket. La política de versiones de la API de Backblaze describe esa distinción y la continuidad del soporte para versiones anteriores de la API.
Configura tu entorno Go para importar archivos de Backblaze
Los siguientes comandos usan Bash en Linux y se probaron con Go 1.27.3. Instala el último parche de seguridad de la línea Go 1.27 desde las descargas de Go. El paquete B2 no tiene dependencias externas en tiempo de ejecución.
Pega estos comandos desde un directorio con permisos de escritura. El subshell conserva tu
directorio actual y se detiene ante un fallo; si ya existe un directorio
backblaze-import, lo deja intacto. GOWORK=off mantiene este proyecto
independiente fuera de cualquier espacio de trabajo de Go que lo contenga.
(
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
)
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 del shell.
La clave necesita acceso al bucket existente y las capacidades listBuckets,
listFiles y readFiles. La búsqueda del bucket usa su nombre,
por lo que también funciona con una clave para un solo bucket. Si la clave restringe los nombres
de archivo, B2_PREFIX debe respetar esa restricción. El primer argumento de
credenciales es el ID de la clave, no el ID del bucket. Consulta los
requisitos de búsqueda de buckets.
Asigna a B2_BUCKET el nombre del bucket, a B2_PREFIX un
prefijo no vacío y a LOCAL_PARENT un directorio local existente con permisos de
escritura. Incluye el carácter final / al seleccionar una carpeta
virtual: invoices/ excluye invoices-old/. El bucket ya debe
contener los objetos que quieres importar, y esos objetos deben tener un SHA-1 del archivo
completo en sus metadatos. Mantén este prefijo estable mientras se ejecuta el comando.
Implementa la funcionalidad básica de importación de archivos en Go
Guarda este programa completo como backblaze-import/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 comprueba 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/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}"
)
El comando imprime el directorio de importación en la salida de error estándar. La salida estándar
relaciona cada archivo numerado con su clave B2 entre comillas y su cantidad de bytes, seguida de
Imported N verified files si la operación termina correctamente. Un código de salida de cero
significa que todos los objetos devueltos por el listado superaron la verificación. Un prefijo sin
coincidencias constituye una importación correcta de cero archivos. Conserva la correspondencia
junto con los archivos si necesitas los nombres originales.
Gestiona directorios e importaciones por lotes: buenas prácticas
Los nombres de objeto de B2 son claves, no rutas confiables del sistema de archivos. Este ejemplo
escribe archivos .bin numerados e imprime 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 automáticamente las páginas siguientes; debes comprobar
iterator.Err() después de la iteración. El ejemplo solicita dos nombres por página
para que una colección pequeña permita probar la paginación. Esto requiere más solicitudes de
listado que una página más grande; no limita la cantidad de objetos importados.
Gestiona errores y depura para garantizar transferencias de archivos sin problemas
Blazer gestiona su propia clasificación de reintentos HTTP y la renovación de tokens. No lo
rodees con un bucle de reintentos incondicional: los fallos de permisos y los buckets inexistentes
deben devolverse como errores. Todas las llamadas al SDK comparten un contexto cancelable con un
plazo total de treinta minutos, que incluye la autenticación, los cuerpos transmitidos en streaming
y las esperas entre reintentos del SDK. Una transferencia lenta puede alcanzar ese plazo sin
importar el tamaño del archivo. El archivo temporal de un objeto fallido se elimina; los objetos
anteriores que se importaron correctamente permanecen disponibles.
Asigna a B2_IMPORT_TIMEOUT una duración positiva de Go, como
2h, antes de ejecutar el comando si toda la importación necesita un
plazo diferente. La compilación del ejecutable se realiza antes de que comience este temporizador.
Sigue el progreso de las transferencias de archivos grandes
El programa imprime un registro de finalización por cada archivo verificado. No presenta los bytes recibidos hasta el momento como una importación completada. Los objetos subidos mediante la interfaz de archivos grandes de B2 pueden carecer de un SHA-1 del archivo completo; este ejemplo falla explícitamente en lugar de presentar datos sin comprobar como verificados.
Problemas frecuentes y consejos para resolverlos
Mantén el prefijo de origen estable durante la importación. Un listado no es una instantánea atómica de un bucket cambiante; las comprobaciones de tamaño y suma de verificación detectan cambios en el contenido durante cada descarga, pero no congelan el propio listado.
Gestión de memoria
El lector del SDK usa una descarga concurrente y bloques de 1 MiB. Los archivos se transfieren 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 la tasa de solicitudes
El procesamiento secuencial de objetos limita la concurrencia de las solicitudes. El cliente fijado respeta las esperas entre reintentos para las respuestas que admiten reintentos; el plazo total del contexto limita la espera total. Si la cuenta sufre restricciones de forma persistente, reduce la carga de trabajo o programa otra ejecución después de resolver el límite.
Gestión de 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 inicia un nuevo directorio de importación en lugar de sobrescribir los resultados parciales de un lote anterior.
Mantén las descargas de objetos secuenciales
Mantén este bucle por lotes secuencial a menos que las mediciones justifiquen importar objetos en
paralelo. El SDK ya ofrece concurrencia acotada de bloques mediante
Reader.ConcurrentDownloads. Aumentarla también incrementa el consumo de memoria y la presión
de las solicitudes; no implementa un límite de tasa de solicitudes para toda la cuenta.
Mantén separado el aprovisionamiento de buckets
Localiza un bucket existente antes de crear la salida local. Los buckets inexistentes 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 corresponden a un paso de
aprovisionamiento separado, para que un error tipográfico durante una importación no pueda crear
un recurso no deseado.
Elige el siguiente paso
Usa este comando cuando tu aplicación Go necesite copias locales verificadas de la API nativa de B2. Si el siguiente paso es un flujo de trabajo de procesamiento alojado, el servicio de importación de archivos de Transloadit describe las opciones de importación disponibles.
