Dateien aus Backblaze mit Go importieren: effiziente Verfahren
Dieses Beispiel importiert ein Präfix aus einem bestehenden Backblaze-B2-Bucket in ein neues privates lokales Verzeichnis. Es nutzt den paginierten Client von Blazer für die Native API und prüft Größe und SHA-1 der gesamten Datei, bevor es sie lokal bereitstellt. Nummerierte Dateinamen halten entfernte Schlüssel aus Dateisystempfaden heraus.
Die Backblaze B2 API verstehen
Das ursprüngliche Repository kurin/blazer wird nicht mehr gepflegt. Sein Nachfolger ist
das Blazer-Repository von Backblaze. Dieses Beispiel verwendet fest die
aktuelle getaggte Release-Version v0.8.0, die die Native API v3 nutzt.
Es integriert die Native API für einen bestehenden Bucket; die Verantwortlichen von Blazer empfehlen
einen S3-Client für Integrationen, die diese API nicht benötigen.
Verwenden Sie einen bestehenden v3-kompatiblen Anwendungsschlüssel. Multi-Bucket Application Keys erfordern eine v4-Autorisierung und funktionieren mit dieser Release-Version nicht, selbst wenn sie nur einen Bucket auswählen. Die Richtlinie zu API-Versionen von Backblaze erläutert diesen Unterschied und die fortgesetzte Unterstützung älterer API-Versionen.
Ihre Go-Umgebung für den Dateiimport aus Backblaze einrichten
Die folgenden Befehle verwenden Bash unter Linux und wurden mit Go 1.27.3 getestet. Installieren Sie den neuesten Sicherheitspatch der Go-1.27-Versionsreihe von der Go-Downloadseite. Das B2-Paket hat keine externen Laufzeitabhängigkeiten.
Fügen Sie diese Befehle in einem beschreibbaren Verzeichnis ein. Die Subshell behält Ihr aktuelles
Verzeichnis bei und stoppt bei einem Fehler; ein bestehendes Verzeichnis
backblaze-import bleibt unverändert. GOWORK=off hält einen
übergeordneten Go-Workspace aus diesem eigenständigen Projekt heraus.
(
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
)
Mit dem Go-SDK eine Verbindung zu Backblaze B2 herstellen
Setzen Sie B2_KEY_ID auf die ID Ihres Anwendungsschlüssels und
B2_APPLICATION_KEY auf dessen geheimen Wert. Verwenden Sie dafür Ihre Verwaltung für
Secrets oder die Shell-Umgebung. Der Schlüssel benötigt Zugriff auf den bestehenden Bucket und die
Berechtigungen listBuckets, listFiles und
readFiles. Bei der Bucket-Suche wird dessen Name übergeben, sodass sie auch mit
einem Schlüssel für einen einzelnen Bucket funktioniert. Beschränkt der Schlüssel die Dateinamen,
muss B2_PREFIX innerhalb dieser Beschränkung bleiben. Das erste Argument für die
Zugangsdaten ist die Schlüssel-ID, nicht die Bucket-ID. Siehe die
Anforderungen für die Bucket-Suche.
Setzen Sie B2_BUCKET auf den Namen des Buckets,
B2_PREFIX auf ein nicht leeres Präfix und LOCAL_PARENT auf ein
bestehendes beschreibbares lokales Verzeichnis. Geben Sie bei der Auswahl eines virtuellen Ordners
das abschließende / an: invoices/ schließt
invoices-old/ aus. Der Bucket muss die zu importierenden Objekte bereits enthalten,
und deren Metadaten müssen eine SHA-1-Prüfsumme für die gesamte Datei enthalten. Halten Sie dieses
Präfix während der Ausführung des Befehls stabil.
Grundlegende Dateiimportfunktionen in Go implementieren
Speichern Sie dieses vollständige Programm als backblaze-import/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/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}"
)
Der Befehl gibt das Importverzeichnis auf der Standardfehlerausgabe aus. Die Standardausgabe ordnet
jeder nummerierten Datei ihren B2-Schlüssel in Anführungszeichen und ihre Byteanzahl zu. Bei Erfolg
folgt Imported N verified files. Der Exit-Status null bedeutet, dass jedes von der Auflistung
zurückgegebene Objekt die Prüfung bestanden hat. Ein passendes Präfix ohne Objekte gilt als
erfolgreicher Import von null Dateien. Bewahren Sie die Zuordnung zusammen mit den Dateien auf,
wenn Sie die ursprünglichen Namen benötigen.
Verzeichnisse und Batch-Importe: bewährte Verfahren
B2-Objektnamen sind Schlüssel, keine vertrauenswürdigen Dateisystempfade. Dieses Beispiel schreibt
nummerierte Dateien mit der Endung .bin und gibt eine Zuordnung jedes
lokalen Dateinamens zu seinem entfernten Schlüssel in Anführungszeichen aus. Ein Objekt namens
../report oder /absolute/path kann daher nicht außerhalb des
neu angelegten Importverzeichnisses geschrieben werden. Der Iterator ruft Folgeseiten automatisch ab;
iterator.Err() muss nach der Iteration geprüft werden. Das Beispiel fordert zwei
Namen pro Seite an, damit schon eine kleine Sammlung die Paginierung durchläuft. Das erfordert mehr
Listenanfragen als eine größere Seite; die Anzahl importierter Objekte wird dadurch nicht begrenzt.
Fehlerbehandlung und Debugging für reibungslose Dateiübertragungen
Blazer übernimmt selbst die Klassifizierung von HTTP-Fehlern für Retries und die Token-Erneuerung.
Legen Sie keine bedingungslose Wiederholungsschleife darum: Berechtigungsfehler und fehlende Buckets
müssen als Fehler zurückgegeben werden. Alle SDK-Aufrufe teilen sich einen abbrechbaren Kontext mit
einer Gesamtfrist von dreißig Minuten. Diese umfasst Authentifizierung, gestreamte Anfrage- und
Antwortkörper sowie die Backoff-Wartezeiten des SDKs. Eine langsame Übertragung kann diese Frist
unabhängig von der Dateigröße erreichen. Die temporäre Datei eines fehlgeschlagenen Objekts wird
entfernt; zuvor erfolgreich importierte Objekte bleiben verfügbar.
Setzen Sie B2_IMPORT_TIMEOUT vor der Ausführung des Befehls auf eine positive
Go-Zeitdauer, etwa 2h, wenn der gesamte Import eine andere Frist benötigt.
Die ausführbare Datei wird erstellt, bevor dieser Timer startet.
Fortschritt großer Dateiübertragungen verfolgen
Das Programm gibt pro geprüfter Datei einen Abschlussdatensatz aus. Es stellt bisher empfangene Bytes nicht als abgeschlossenen Import dar. Bei Objekten, die über die Schnittstelle für große Dateien von B2 hochgeladen wurden, kann die SHA-1-Prüfsumme für die gesamte Datei fehlen. Dieses Beispiel schlägt dann explizit fehl, statt ungeprüfte Daten als geprüft zu melden.
Häufige Fallstricke und Tipps zur Fehlerbehebung
Halten Sie das Quellpräfix während des Imports stabil. 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 nutzt einen gleichzeitigen Download und Blöcke von 1 MiB. Dateien werden auf den Datenträger gestreamt, statt sich in einem Byte-Slice anzusammeln. Es wird jeweils nur ein Objekt verarbeitet, und jeder Reader wird sowohl bei Erfolg als auch bei einem Fehler geschlossen.
Begrenzung der Anfragerate
Die sequenzielle Objektverarbeitung begrenzt gleichzeitige Anfragen. Der festgelegte Client beachtet Retry-Backoff bei Antworten, die einen erneuten Versuch zulassen; die Gesamtfrist des Kontexts begrenzt die gesamte Wartezeit. Wird das Konto dauerhaft gedrosselt, reduzieren Sie die Arbeitslast oder planen Sie einen weiteren Durchlauf, nachdem Sie die Ursache für die Begrenzung behoben haben.
Netzwerkunterbrechungen behandeln
Ctrl+C bricht die tatsächlichen SDK-Anfragen und Retry-Wartezeiten ab. Der Abbruch erfolgt nicht dadurch, dass eine Goroutine aufgegeben wird, während ein Download weiter Daten schreibt. Ein späterer Durchlauf legt ein neues Importverzeichnis an, statt Teilergebnisse eines früheren Batches zu überschreiben.
Objekte weiterhin sequenziell herunterladen
Lassen Sie diese Batch-Schleife sequenziell laufen, sofern Messungen keine parallelen Objektimporte
rechtfertigen. Das SDK bietet über Reader.ConcurrentDownloads bereits begrenzte Parallelität beim
Herunterladen von Blöcken. Ein höherer Wert erhöht auch Speicherverbrauch und Anfrageaufkommen;
er setzt kein kontoweites Limit für die Anfragerate um.
Buckets separat bereitstellen
Ermitteln Sie einen bestehenden Bucket, bevor Sie lokale Ausgabedateien erstellen. Fehlende Buckets
und Berechtigungsprobleme führen zu Fehlern, und ein Bucket mit dem Wert
nil wird vorsorglich abgelehnt. Bucket-Erstellung und
Lebenszyklusänderungen gehören in einen separaten Bereitstellungsschritt, damit ein Tippfehler beim
Import keine unbeabsichtigte Ressource anlegen kann.
Den nächsten Schritt wählen
Verwenden Sie diesen Befehl, wenn Ihre Go-Anwendung geprüfte lokale Kopien über die Native API von B2 benötigt. Wenn als Nächstes ein gehosteter Verarbeitungsworkflow folgen soll, beschreibt der Dateiimportdienst von Transloadit die verfügbaren Importoptionen.
