Importer des fichiers Backblaze en Go : techniques efficaces
Cet exemple importe un préfixe d’un bucket Backblaze B2 existant dans un nouveau répertoire local privé. Il utilise le client paginé de Blazer pour la Native API et vérifie la taille de chaque fichier et le SHA-1 du fichier complet avant sa publication locale. Les noms de fichiers numérotés évitent l’utilisation des clés distantes dans les chemins du système de fichiers.
Comprendre l’API Backblaze B2
Le dépôt d’origine kurin/blazer n’est plus maintenu. Son successeur est le
dépôt Blazer de Backblaze. Cet exemple fige sa dépendance à la
version étiquetée actuelle v0.8.0, qui utilise la Native API v3.
Il s’agit d’une intégration à la Native API pour un bucket existant ; les responsables de la
maintenance de Blazer recommandent un client S3 pour les intégrations qui n’ont pas besoin de cette API.
Utilisez une clé d’application existante compatible avec la v3. Les clés d’application multibuckets nécessitent une autorisation v4 et ne fonctionnent pas avec cette version, même si elles ne sélectionnent qu’un seul bucket. La politique de gestion des versions de l’API Backblaze décrit cette distinction et le maintien de la prise en charge des anciennes versions de l’API.
Configurer votre environnement Go pour l’importation de fichiers Backblaze
Les commandes ci-dessous utilisent Bash sous Linux et ont été testées avec Go 1.27.3. Installez le dernier correctif de sécurité de la branche Go 1.27 depuis la page de téléchargement de Go. Le package B2 n’a aucune dépendance externe à l’exécution.
Collez ces commandes depuis un répertoire accessible en écriture. Le sous-shell préserve votre
répertoire courant et s’arrête en cas d’échec ; un répertoire backblaze-import existant
reste intact. GOWORK=off empêche un espace de travail Go englobant d’interférer
avec ce projet autonome.
(
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
)
Utiliser le SDK Go pour se connecter à Backblaze B2
Définissez B2_KEY_ID sur l’identifiant de votre clé d’application et
B2_APPLICATION_KEY sur son secret à l’aide de votre gestionnaire de secrets ou de
l’environnement du shell. La clé doit avoir accès au bucket existant et disposer des capacités
listBuckets, listFiles et readFiles.
La recherche du bucket transmet son nom et fonctionne donc aussi avec une clé limitée à un seul
bucket. Si la clé restreint les noms de fichiers, B2_PREFIX doit respecter
cette restriction. Pour les informations d’accès, le premier argument est l’identifiant de la clé,
et non celui du bucket. Consultez les
conditions requises pour la recherche du bucket.
Définissez B2_BUCKET sur le nom du bucket,
B2_PREFIX sur un préfixe non vide et LOCAL_PARENT sur un
répertoire local existant accessible en écriture. Incluez le / final
lorsque vous sélectionnez un dossier virtuel : invoices/ exclut
invoices-old/. Le bucket doit déjà contenir les objets que vous souhaitez importer,
et les métadonnées de ces objets doivent contenir un SHA-1 du fichier complet. Ne modifiez pas ce
préfixe pendant l’exécution de la commande.
Implémenter une fonctionnalité de base d’importation de fichiers en Go
Enregistrez ce programme complet sous le nom backblaze-import/main.go. Chaque téléchargement
est vérifié par rapport à la taille et au SHA-1 de ses métadonnées, synchronisé, fermé et renommé
dans le même répertoire privé. Le SHA-1 vérifie ici l’intégrité du transfert ; ce n’est pas une
signature prouvant qui a créé le fichier.
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}"
)
La commande affiche le répertoire d’importation sur la sortie d’erreur standard. La sortie standard
associe chaque fichier numéroté à sa clé B2 entre guillemets et à son nombre d’octets, puis affiche
Imported N verified files en cas de succès. Un code de sortie égal à zéro signifie que chaque
objet renvoyé par la liste a passé la vérification. Un préfixe sans objet correspondant constitue
une importation réussie de zéro fichier. Conservez la table de correspondance avec les fichiers si
vous avez besoin des noms d’origine.
Gestion des répertoires et des importations par lots : bonnes pratiques
Les noms d’objets B2 sont des clés, pas des chemins de système de fichiers de confiance. Cet exemple
écrit des fichiers .bin numérotés et affiche une correspondance entre
chaque nom de fichier local et sa clé distante entre guillemets. Un objet nommé
../report ou /absolute/path ne peut donc pas sortir du nouveau
répertoire d’importation. L’itérateur récupère automatiquement les pages suivantes ;
iterator.Err() doit être vérifié après l’itération. L’exemple demande deux noms par
page afin qu’un petit ensemble d’objets permette de tester la pagination. Cela nécessite davantage
de requêtes de listage qu’une page plus grande ; cela ne limite pas le nombre d’objets importés.
Gestion des erreurs et débogage : assurer des transferts de fichiers fluides
Blazer gère lui-même la classification des erreurs HTTP pour les nouvelles tentatives et le
renouvellement des jetons. N’ajoutez pas de boucle de nouvelles tentatives inconditionnelle autour
de ces opérations : les erreurs d’autorisation et les buckets manquants doivent être signalés.
Tous les appels au SDK partagent un contexte annulable avec un délai maximal global de trente
minutes, qui couvre l’authentification, les corps transférés en flux et les attentes entre les
nouvelles tentatives du SDK. Un transfert lent peut atteindre cette limite quelle que soit la
taille du fichier. Le fichier temporaire d’un objet dont le traitement échoue est supprimé ; les
objets précédemment traités avec succès restent disponibles.
Définissez B2_IMPORT_TIMEOUT sur une durée Go positive, telle que
2h, avant d’exécuter la commande si l’importation complète nécessite un
autre délai maximal. La compilation de l’exécutable a lieu avant le démarrage de ce compteur.
Suivi de la progression des transferts de fichiers volumineux
Le programme affiche un enregistrement de fin de traitement par fichier vérifié. Il ne présente pas les octets reçus jusque-là comme une importation terminée. Pour les objets téléversés via l’interface de fichiers volumineux de B2, le SHA-1 du fichier complet peut être absent ; cet exemple échoue explicitement plutôt que de présenter des données non contrôlées comme vérifiées.
Pièges courants et conseils de dépannage
Ne modifiez pas le préfixe source pendant l’importation. Une liste n’est pas un instantané atomique d’un bucket qui évolue ; les contrôles de taille et de somme de contrôle détectent les changements de contenu pendant chaque téléchargement, mais ne figent pas la liste elle-même.
Gestion de la mémoire
Le lecteur du SDK utilise un seul téléchargement simultané et des blocs de 1 MiB. Les fichiers sont écrits sur disque au fil du transfert au lieu de s’accumuler dans une tranche d’octets. Un seul objet est traité à la fois, et chaque lecteur est fermé en cas de succès comme d’échec.
Limitation du débit
Le traitement séquentiel des objets borne le nombre de requêtes simultanées. Le client dont la version est figée respecte les délais progressifs entre les nouvelles tentatives pour les réponses qui le permettent ; le délai maximal global du contexte borne l’attente totale. Si le compte subit une limitation persistante, réduisez la charge de travail ou planifiez une nouvelle exécution après avoir résolu le problème de limite.
Gestion des interruptions réseau
Ctrl+C annule les requêtes réelles du SDK et les attentes entre les nouvelles tentatives. L’annulation ne consiste pas à abandonner une goroutine pendant qu’un téléchargement continue d’écrire. Une exécution ultérieure crée un nouveau répertoire d’importation au lieu d’écraser les résultats partiels d’un lot précédent.
Maintenir le téléchargement séquentiel des objets
Gardez cette boucle de traitement par lots séquentielle, sauf si des mesures justifient
l’importation parallèle d’objets. Le SDK expose déjà un parallélisme borné des blocs via
Reader.ConcurrentDownloads. L’augmenter accroît aussi la consommation de mémoire et la pression
des requêtes ; cela ne met pas en place une limite de débit des requêtes à l’échelle du compte.
Séparer le provisionnement du bucket
Recherchez un bucket existant avant de créer les fichiers de sortie locaux. Les buckets manquants
et les erreurs d’autorisation renvoient des erreurs, et un bucket nil
est rejeté par précaution. La création des buckets et les modifications de leur cycle de vie
relèvent d’une étape de provisionnement distincte, afin qu’une faute de frappe pendant une
importation ne puisse pas créer une ressource non souhaitée.
Choisir l’étape suivante
Utilisez cette commande lorsque votre application Go a besoin de copies locales vérifiées issues de la Native API de B2. Si l’étape suivante est un processus de traitement hébergé, le service d’importation de fichiers de Transloadit décrit les options d’importation disponibles.
