Dateien aus Backblaze in Go importieren: effiziente Techniken
Dieses Beispiel importiert die Objekte unter einem Präfix aus einem bestehenden Backblaze-B2-Bucket in ein neues privates lokales Verzeichnis. Es verwendet den Community-Client Blazer, festgelegt auf v0.5.3, mit kontextbezogenen Anfragen und einem paginierten Iterator. Es unterstützt bewusst nur Objekte mit einer SHA-1-Prüfsumme für die gesamte Datei.
Die Backblaze-B2-API verstehen
Blazer implementiert Version 1 der B2 Native API. Es handelt sich um einen älteren Drittanbieter-Client, nicht um ein offizielles Go-SDK von Backblaze. Backblaze unterstützt weiterhin ältere API-Versionen, doch Anwendungsschlüssel für mehrere Buckets erfordern die Autorisierung mit Version 4 und können mit diesem Client nicht verwendet werden. Verwenden Sie für dieses Wartungsbeispiel einen bestehenden, mit Version 1 kompatiblen Schlüssel. Wählen Sie einen aktuellen Client, wenn Ihre Integration neuere Schlüsseltypen erfordert.
Richtlinie von Backblaze zur Kompatibilität von API-Versionen und Schlüsseln
API und Quellcode von Blazer v0.5.3
Ihre Go-Umgebung für den Dateiimport aus Backblaze einrichten
Verwenden Sie Go 1.22 oder neuer und ein neues Projektverzeichnis. Das B2-Paket dieser Version nutzt die Standardbibliothek ohne zusätzliche Laufzeitabhängigkeiten.
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
Mit dem Go-SDK eine Verbindung zu Backblaze B2 herstellen
Setzen Sie B2_KEY_ID auf Ihre Anwendungsschlüssel-ID und
B2_APPLICATION_KEY auf das zugehörige Secret. Nutzen Sie dafür Ihren Secret-Manager oder
Ihre Shell-Umgebung. Gewähren Sie dem Schlüssel Zugriff auf den bestehenden Bucket sowie die
Berechtigungen listBuckets, listFiles und
readFiles. Das erste Argument für die Zugangsdaten ist die Schlüssel-ID, nicht
die Bucket-ID. Das Programm erstellt niemals einen Bucket.
Grundlegende Dateiimportfunktionen in Go implementieren
Speichern Sie dieses vollständige Programm als main.go. Jeder Download wird
anhand der Größe und der SHA-1-Prüfsumme aus seinen Metadaten geprüft, synchronisiert, geschlossen
und innerhalb desselben privaten Verzeichnisses umbenannt. SHA-1 prüft hier die Übertragungsintegrität;
es ist keine Signatur, die nachweist, wer die Datei erstellt hat.
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}"
Verzeichnisse und Stapelimporte verwalten: bewährte Verfahren
B2-Objektnamen sind Schlüssel, keine vertrauenswürdigen Dateisystempfade. Dieses Beispiel schreibt
nummerierte Dateien mit der Endung .bin und gibt für jeden lokalen
Dateinamen die Zuordnung zu seinem entfernten Schlüssel in Anführungszeichen aus. Ein Objekt namens
../report oder /absolute/path kann daher nicht außerhalb des
neuen Importverzeichnisses geschrieben werden. Der Iterator ruft nachfolgende Seiten automatisch ab;
iterator.Err() muss nach der Iteration geprüft werden.
Fehlerbehandlung und Debugging: reibungslose Dateiübertragungen sicherstellen
Blazer entscheidet selbst, welche HTTP-Anfragen wiederholt werden, und erneuert Tokens eigenständig. Umschließen Sie diese Logik nicht mit einer bedingungslosen Wiederholungsschleife: Fehlende Berechtigungen und Buckets müssen als Fehler zurückgegeben werden. Alle SDK-Aufrufe teilen sich einen abbrechbaren Kontext mit einem Gesamtzeitlimit von dreißig Minuten, einschließlich der Backoff-Wartezeiten des SDKs. Die temporäre Datei eines fehlgeschlagenen Objekts wird entfernt; zuvor erfolgreich importierte Objekte bleiben verfügbar.
Fortschritt bei großen Dateiübertragungen verfolgen
Das Programm gibt pro verifizierter Datei einen Abschlusseintrag aus. Es meldet bisher empfangene Bytes nicht als abgeschlossenen Import. Bei Objekten, die über die Schnittstelle von B2 für große Dateien hochgeladen wurden, kann eine SHA-1-Prüfsumme für die gesamte Datei fehlen. In diesem Fall bricht das Beispiel explizit mit einem Fehler ab, statt ungeprüfte Daten als verifiziert zu melden.
Häufige Fallstricke und Tipps zur Fehlerbehebung
Halten Sie das Quellpräfix während des Imports unverändert. Eine Auflistung ist keine atomare Momentaufnahme eines sich ändernden Buckets. Größen- und Prüfsummenprüfungen erkennen veränderte Inhalte während jedes Downloads, frieren aber die Auflistung selbst nicht ein.
Speicherverwaltung
Der Reader des SDKs verwendet einen gleichzeitigen Download und Blöcke von 1 MiB. Dateien werden als Stream auf die Festplatte geschrieben, statt sich in einem Byte-Slice anzusammeln. Es wird immer nur ein Objekt verarbeitet, und jeder Reader wird sowohl bei Erfolg als auch bei einem Fehler geschlossen.
Anfrageraten begrenzen
Die sequenzielle Objektverarbeitung begrenzt die Anzahl gleichzeitiger Anfragen. Der auf eine feste Version festgelegte Client beachtet Backoff-Wartezeiten bei Antworten, die einen erneuten Versuch erlauben. Das Gesamtzeitlimit des Kontexts begrenzt die gesamte Wartezeit. Wenn das Konto dauerhaft gedrosselt wird, reduzieren Sie die Arbeitslast oder planen Sie einen weiteren Durchlauf, nachdem Sie das Problem mit dem Limit behoben haben.
Netzwerkunterbrechungen behandeln
Ctrl-C bricht die tatsächlichen SDK-Anfragen und Wartezeiten vor erneuten Versuchen ab. Der Abbruch erfolgt nicht dadurch, dass eine Goroutine aufgegeben wird, während ein Download weiter Daten schreibt. Ein späterer Durchlauf beginnt mit einem neuen Importverzeichnis, statt Teilergebnisse eines früheren Stapels zu überschreiben.
Gleichzeitige Downloads mit begrenzter Anfragerate
Behalten Sie diese sequenzielle Stapelschleife bei, sofern Messungen keine parallelen Objektimporte
rechtfertigen. Das SDK bietet bereits mit Reader.ConcurrentDownloads die Möglichkeit, Blöcke
mit begrenzter Parallelität herunterzuladen. Eine Erhöhung steigert auch den Speicherverbrauch und
die Anfragelast; sie setzt kein kontoweites Limit für die Anfragerate um.
Bewährte Verfahren zur Verwaltung des Bucket-Lebenszyklus
Ermitteln Sie einen bestehenden Bucket, bevor Sie lokale Ausgaben erstellen. Fehlende Buckets und
Berechtigungen führen zu Fehlern, und ein Bucket, dessen Wert nil ist,
wird vorsorglich abgewiesen. Die Erstellung von Buckets und Änderungen am Lebenszyklus gehören in
einen separaten Bereitstellungsschritt, damit ein Tippfehler beim Import keine unbeabsichtigte
Ressource erzeugen kann.
Fazit und weiterführende Ressourcen
Dieser Importer kombiniert den tatsächlichen Abbruch von SDK-Anfragen und Paginierung mit Streaming bei begrenztem Ressourcenverbrauch und verifizierter lokaler Bereitstellung. Er behält erfolgreich importierte Dateien nach einem späteren Fehler bei und gibt das private Importverzeichnis zur Überprüfung aus.
