Importer des fichiers depuis Backblaze en PHP avec l’API native
Pour importer un objet privé Backblaze B2 en PHP, obtenez une autorisation via l’API native et transférez son contenu en flux vers un fichier temporaire. L’extension open source cURL de PHP traite les requêtes HTTP sans dépendance à un SDK. La commande ci-dessous remplace la destination choisie uniquement après la réussite du transfert et des écritures sur disque.
Prérequis
- Une ligne de commande Linux avec PHP CLI 8.5.10 ou un correctif 8.5 ultérieur et
ext-curl. Cet exemple a été testé avec PHP 8.5.10. Utilisez le dernier correctif de cette branche maintenue. - Un bucket B2 privé existant et le nom exact d’un objet que vous pouvez lire sans en-têtes SSE-C.
- Une clé d’application disposant de l’accès
readFilesà ce bucket et à ce préfixe d’objet. Vous avez besoin de son identifiant de clé et de la clé d’application, qui sont deux valeurs distinctes. - Un répertoire de destination existant et accessible en écriture sur un système de fichiers local. Exécutez un seul importateur à la fois pour une destination donnée.
Vérifiez la CLI installée avant d’enregistrer le programme :
php -r 'exit(PHP_MAJOR_VERSION === 8 && PHP_MINOR_VERSION === 5 && version_compare(PHP_VERSION, "8.5.10", ">=") && extension_loaded("curl") ? 0 : 1);'
Un code de sortie zéro signifie que cette CLI satisfait aux prérequis. Si la vérification échoue, installez ou activez cURL pour votre CLI PHP, ou sélectionnez la version de PHP prise en charge, puis recommencez la vérification. Aucune installation avec Composer n’est nécessaire.
Utiliser l’API native B2
La réponse d’autorisation v4 place
downloadUrl dans apiInfo.storageApi ; le jeton d’autorisation reste au niveau racine.
L’identifiant de clé et la clé d’application authentifient cette première requête au moyen de
l’authentification HTTP Basic. Le téléchargement utilise ensuite le jeton renvoyé dans son en-tête
Authorization.
Enregistrez le programme complet suivant sous le nom import-b2.php. Il accepte les points de
terminaison HTTPS et les points de terminaison HTTP de bouclage pour les tests locaux, et refuse
les redirections pour les deux requêtes.
Importer un objet
<?php
declare(strict_types=1);
final class CurlTransport
{
private const MAX_AUTH_BYTES = 1_048_576;
public function __construct(private readonly int $timeoutSeconds = 60) {}
/** @return array<string, mixed> */
public function authorize(string $url, string $keyId, string $applicationKey): array
{
$this->assertSafeUrl($url);
$body = '';
$curl = curl_init($url);
if ($curl === false) {
throw new RuntimeException('Could not initialize cURL');
}
try {
if (!curl_setopt_array($curl, [
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => $this->timeoutSeconds,
CURLOPT_USERPWD => $keyId . ':' . $applicationKey,
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$body): int {
if (strlen($body) + strlen($chunk) > self::MAX_AUTH_BYTES) {
return 0;
}
$body .= $chunk;
return strlen($chunk);
},
])) {
throw new RuntimeException('Could not configure cURL');
}
$ok = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($ok !== true || $status !== 200) {
throw new RuntimeException("B2 authorization failed with HTTP {$status}");
}
} finally {
unset($curl);
}
$decoded = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($decoded)) {
throw new RuntimeException('B2 returned an invalid authorization response');
}
return $decoded;
}
/** @param resource $output */
public function download(string $url, string $token, $output, int $maxBytes): void
{
$this->assertSafeUrl($url);
$written = 0;
$curl = curl_init($url);
if ($curl === false) {
throw new RuntimeException('Could not initialize cURL');
}
try {
if (!curl_setopt_array($curl, [
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPHEADER => ['Authorization: ' . $token],
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => $this->timeoutSeconds,
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (
$output,
$maxBytes,
&$written,
): int {
$length = strlen($chunk);
if ($written + $length > $maxBytes) {
return 0;
}
$result = fwrite($output, $chunk);
if ($result !== $length) {
return 0;
}
$written += $result;
return $result;
},
])) {
throw new RuntimeException('Could not configure cURL');
}
$ok = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($ok !== true || $status !== 200) {
throw new RuntimeException("B2 download failed with HTTP {$status}");
}
} finally {
unset($curl);
}
}
private function assertSafeUrl(string $url): void
{
$scheme = parse_url($url, PHP_URL_SCHEME);
$host = parse_url($url, PHP_URL_HOST);
$loopback = $scheme === 'http' && in_array($host, ['127.0.0.1', '::1'], true);
if ($scheme !== 'https' && !$loopback) {
throw new RuntimeException('B2 endpoints must use HTTPS');
}
}
}
/** @param array<string, mixed> $authorization */
function requiredString(array $authorization, string $key): string
{
$value = $authorization[$key] ?? null;
if (!is_string($value) || $value === '') {
throw new RuntimeException("B2 authorization response is missing {$key}");
}
return $value;
}
function encodeObjectName(string $objectName): string
{
$segments = explode('/', $objectName);
if (in_array('.', $segments, true) || in_array('..', $segments, true)) {
throw new InvalidArgumentException('Object names cannot contain dot path segments');
}
return implode('/', array_map('rawurlencode', $segments));
}
function importB2Object(
CurlTransport $transport,
string $authorizeUrl,
string $keyId,
string $applicationKey,
string $bucket,
string $objectName,
string $destination,
int $maxBytes,
): void {
if ($maxBytes < 1 || $bucket === '' || $objectName === '') {
throw new InvalidArgumentException('Bucket, object name, and a positive limit are required');
}
$authorization = $transport->authorize($authorizeUrl, $keyId, $applicationKey);
$apiInfo = $authorization['apiInfo'] ?? null;
$storageApi = is_array($apiInfo) ? ($apiInfo['storageApi'] ?? null) : null;
if (!is_array($storageApi)) {
throw new RuntimeException('B2 authorization response is missing storageApi');
}
$downloadUrl = requiredString($storageApi, 'downloadUrl');
$token = requiredString($authorization, 'authorizationToken');
$url = rtrim($downloadUrl, '/') . '/file/' . rawurlencode($bucket) . '/'
. encodeObjectName($objectName);
$directory = dirname($destination);
$realDirectory = realpath($directory);
if ($realDirectory === false || !is_dir($realDirectory) || !is_writable($realDirectory)) {
throw new RuntimeException('Destination directory must exist and be writable');
}
$temporary = tempnam($realDirectory, '.b2-');
if ($temporary === false) {
throw new RuntimeException('Could not create a temporary destination');
}
if (dirname($temporary) !== $realDirectory) {
@unlink($temporary);
throw new RuntimeException('Temporary file was not created beside the destination');
}
$output = fopen($temporary, 'wb');
if ($output === false) {
@unlink($temporary);
throw new RuntimeException('Could not open the temporary destination');
}
try {
$transport->download($url, $token, $output, $maxBytes);
$flushed = fflush($output);
$closed = fclose($output);
$output = null;
if (!$flushed || !$closed) {
throw new RuntimeException('Could not finish writing the downloaded object');
}
if (!rename($temporary, $destination)) {
throw new RuntimeException('Could not publish the downloaded object');
}
} finally {
if (is_resource($output)) {
fclose($output);
}
if (file_exists($temporary)) {
unlink($temporary);
}
}
}
function main(array $arguments): void
{
if (count($arguments) !== 2) {
throw new InvalidArgumentException('Usage: php import-b2.php DESTINATION');
}
foreach (['B2_KEY_ID', 'B2_APPLICATION_KEY', 'B2_BUCKET', 'B2_OBJECT'] as $name) {
if (getenv($name) === false || getenv($name) === '') {
throw new RuntimeException("Missing environment variable: {$name}");
}
}
$configuredMaxBytes = getenv('B2_MAX_BYTES');
$maxBytes = filter_var(
$configuredMaxBytes === false ? 100 * 1024 * 1024 : $configuredMaxBytes,
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1]],
);
if ($maxBytes === false) {
throw new RuntimeException('B2_MAX_BYTES must be a positive integer');
}
$configuredTimeout = getenv('B2_TIMEOUT_SECONDS');
$timeoutSeconds = filter_var(
$configuredTimeout === false ? 60 : $configuredTimeout,
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1, 'max_range' => 3600]],
);
if ($timeoutSeconds === false) {
throw new RuntimeException('B2_TIMEOUT_SECONDS must be an integer from 1 to 3600');
}
importB2Object(
new CurlTransport($timeoutSeconds),
getenv('B2_AUTHORIZE_URL') ?: 'https://api.backblazeb2.com/b2api/v4/b2_authorize_account',
(string) getenv('B2_KEY_ID'),
(string) getenv('B2_APPLICATION_KEY'),
(string) getenv('B2_BUCKET'),
(string) getenv('B2_OBJECT'),
$arguments[1],
$maxBytes,
);
}
try {
main($argv);
} catch (Throwable) {
error_log('B2 import failed');
exit(1);
}
Depuis ce répertoire, remplacez les informations d’authentification, le bucket et le nom d’objet
par les vôtres dans cette commande. Dans une application, fournissez les informations
via votre gestionnaire de secrets plutôt que de les enregistrer dans le code source ou
l’historique du shell. Utilisez ./ avant une destination relative, y compris pour les
noms commençant par -.
B2_KEY_ID='your-key-id' \
B2_APPLICATION_KEY='your-application-key' \
B2_BUCKET='my-bucket' \
B2_OBJECT='exports/report.bin' \
php import-b2.php ./report.bin
En cas de réussite, le programme se termine avec un code de sortie zéro sans rien afficher ;
./report.bin contient les octets téléchargés. Une nouvelle exécution réussie remplace ce
fichier. Le code écrit d’abord les octets dans un fichier .b2- unique à côté de la
destination, le ferme, puis utilise rename()
pour le publier. Sur un système de fichiers Linux local, le remplacement est atomique,
si bien que les lecteurs voient soit l’ancien fichier, soit son remplacement complet. Cela ne
garantit pas la persistance des données en cas de panne et ne coordonne pas les importateurs
concurrents.
La limite par défaut est de 100 MiB ; un téléchargement de cette taille est autorisé.
Définissez B2_MAX_BYTES sur un autre entier positif
exprimé en octets si nécessaire. Les réponses d’autorisation ont une limite distincte de 1 MiB.
Chaque requête est soumise à un
délai maximal de 60 secondes pour l’ensemble du transfert,
établissement de la connexion compris ; B2_TIMEOUT_SECONDS accepte les entiers de 1 à 3 600.
L’autorisation et le téléchargement ont des délais maximaux distincts ; un petit fichier peut donc
quand même échouer sur une connexion lente. Aucune des deux requêtes n’est retentée.
En cas d’échec, le programme se termine avec un code de sortie un et journalise une erreur générique
sans corps de réponse ni informations d’authentification. Un échec d’authentification, un objet
dont l’accès est refusé ou qui est absent, un transfert HTTP trop volumineux ou interrompu et un échec d’écriture sur
disque laissent la destination précédente intacte et suppriment le fichier temporaire de cette
exécution. Si aucune destination n’existait auparavant, l’échec n’en laisse aucune. Un arrêt forcé
du processus ou une panne de la machine peut laisser un fichier .b2- ; ne le supprimez
qu’après avoir confirmé l’arrêt de l’importateur correspondant.
Adapter l’importateur
Définissez B2_OBJECT sur le nom littéral de l’objet en UTF-8, sans l’encoder au préalable
pour une URL. Conformément aux règles d’encodage de B2,
le code encode chaque segment du chemin, en préservant les séparateurs / ainsi que
les espaces littéraux et les caractères littéraux +, %, # et ?.
Il rejette les segments de chemin . et ... Le téléchargement par nom sélectionne la
version actuellement visible ;
cette commande ne répertorie pas le contenu d’un bucket et ne sélectionne pas d’identifiant de
fichier historique.
Gestion sûre et efficace des fichiers
Limitez la portée de la clé au bucket et au préfixe dont vous avez besoin. Laissez la vérification
TLS activée et utilisez le point de terminaison d’autorisation par défaut de B2 ;
B2_AUTHORIZE_URL sert à tester un service local contrôlé, et non à accepter un point de terminaison
fourni par un appelant non fiable.
L’importateur vérifie que le transfert HTTP est terminé et que la limite d’octets est respectée. Il ne compare pas de somme de contrôle fournie par l’éditeur et ne déchiffre pas les fichiers téléversés avec des clés de chiffrement gérées par le client. Si votre flux de travail nécessite l’authentification du contenu ou SSE-C, ajoutez cette vérification ou la gestion des clés avant de publier le fichier.
Pour le traitement côté serveur après un import, consultez le Robot d’import depuis Backblaze.
