Dateien aus Backblaze in PHP mit der Native API importieren
Um ein privates Backblaze-B2-Objekt in PHP zu importieren, autorisieren Sie sich mit der Native API und streamen den Download in eine temporäre Datei. Die Open-Source-Erweiterung cURL von PHP übernimmt die HTTP-Anfragen ohne SDK-Abhängigkeit. Der folgende Befehl ersetzt Ihr gewähltes Ziel erst, wenn die Übertragung und die Schreibvorgänge auf dem Datenträger erfolgreich waren.
Voraussetzungen
- Eine Linux-Kommandozeile mit PHP CLI 8.5.10 oder einem späteren Patch für PHP 8.5 und
ext-curl. Dieses Beispiel wurde mit PHP 8.5.10 getestet. Verwenden Sie den neuesten Patch dieses gepflegten Versionszweigs. - Ein vorhandener privater B2-Bucket und der genaue Name eines Objekts, das Sie ohne SSE-C-Header lesen können.
- Ein Anwendungsschlüssel mit Zugriff über
readFilesauf diesen Bucket und das Objektpräfix. Sie benötigen die Schlüssel-ID und den Anwendungsschlüssel. Das sind separate Werte. - Ein vorhandenes, beschreibbares Zielverzeichnis auf einem lokalen Dateisystem. Führen Sie für ein bestimmtes Ziel gleichzeitig nur einen Importer aus.
Prüfen Sie die installierte CLI, bevor Sie das Programm speichern:
php -r 'exit(PHP_MAJOR_VERSION === 8 && PHP_MINOR_VERSION === 5 && version_compare(PHP_VERSION, "8.5.10", ">=") && extension_loaded("curl") ? 0 : 1);'
Ein Exitcode von null bedeutet, dass diese CLI die Voraussetzungen erfüllt. Schlägt die Prüfung fehl, installieren oder aktivieren Sie cURL für Ihre PHP-CLI oder wählen Sie die unterstützte PHP-Version. Wiederholen Sie dann die Prüfung. Eine Installation mit Composer ist nicht nötig.
Die B2 Native API verwenden
Die Autorisierungsantwort von v4 enthält
downloadUrl innerhalb von apiInfo.storageApi; das Autorisierungstoken
bleibt auf der obersten Ebene. Die Schlüssel-ID und der Anwendungsschlüssel authentifizieren diese
erste Anfrage mit HTTP Basic Auth. Der Download verwendet anschließend das zurückgegebene Token
im Header Authorization.
Speichern Sie das folgende vollständige Programm als import-b2.php. Es akzeptiert
HTTPS-Endpunkte und HTTP-Endpunkte auf der Loopback-Adresse für lokale Tests und lehnt bei beiden
Anfragen Weiterleitungen ab.
Ein Objekt importieren
<?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);
}
Führen Sie den folgenden Befehl in diesem Verzeichnis aus und setzen Sie Ihre Zugangsdaten, Ihren
Bucket und den Objektnamen ein. Übergeben Sie Zugangsdaten in einer Anwendung über Ihren
Secret-Manager, statt sie im Quellcode oder im Shell-Verlauf zu speichern. Verwenden Sie
./ vor einem relativen Zielpfad, auch bei Namen, die mit
- beginnen.
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
Bei Erfolg endet das Programm mit Exitcode null ohne Ausgabe; ./report.bin
enthält die heruntergeladenen Bytes. Ein erfolgreicher erneuter Durchlauf ersetzt diese Datei.
Der Code speichert die Bytes zunächst in einer eindeutig benannten Datei mit dem Präfix
.b2- neben der Zieldatei, schließt sie und verwendet dann
rename(), um sie am Ziel bereitzustellen.
Auf einem lokalen Linux-Dateisystem ist das Ersetzen atomar,
sodass lesende Zugriffe entweder die alte Datei oder den vollständigen Ersatz sehen. Das
gewährleistet weder dauerhaften Datenerhalt bei einem Absturz noch die Koordination gleichzeitig
laufender Importer.
Das Standardlimit beträgt 100 MiB und schließt diesen Wert ein. Setzen Sie B2_MAX_BYTES bei
Bedarf auf eine andere positive Ganzzahl in Bytes. Für Autorisierungsantworten gilt eine separate
Grenze von 1 MiB. Jede Anfrage hat eine
Frist von 60 Sekunden für die gesamte Übertragung, einschließlich
des Verbindungsaufbaus; B2_TIMEOUT_SECONDS akzeptiert Ganzzahlen von 1 bis 3.600.
Für Autorisierung und Download gelten separate Fristen. Daher kann auf einer langsamen Verbindung
auch der Download einer kleinen Datei fehlschlagen. Keine der beiden Anfragen wird wiederholt.
Bei einem Fehler endet das Programm mit Exitcode eins und protokolliert eine allgemeine
Fehlermeldung ohne Antwortkörper oder Zugangsdaten. Eine fehlgeschlagene Authentifizierung, ein
nicht zugängliches oder fehlendes Objekt, eine zu große oder unterbrochene HTTP-Übertragung und
ein fehlgeschlagener Schreibvorgang auf dem Datenträger lassen das bisherige Ziel unverändert.
Dabei wird die temporäre Datei dieses Aufrufs entfernt. Gab es zuvor kein Ziel, wird bei einem
Fehler keines hinterlassen. Ein erzwungenes Beenden des Prozesses oder ein Rechnerabsturz kann
eine Datei mit dem Präfix .b2- hinterlassen. Entfernen Sie diese erst, nachdem
Sie bestätigt haben, dass der zugehörige Importer nicht mehr läuft.
Den Importer anpassen
Setzen Sie B2_OBJECT auf den exakten Objektnamen in UTF-8, ohne vorherige
URL-Kodierung. Gemäß den Kodierungsregeln von B2
kodiert der Code jedes Pfadsegment. Dabei bleiben die Trennzeichen /
sowie literale Leerzeichen und die Zeichen +,
%, # und ?
erhalten. Die Pfadsegmente . und ..
werden abgelehnt. Beim Download nach Namen wird die
aktuell sichtbare Version ausgewählt.
Dieser Befehl listet weder den Inhalt eines Buckets auf noch wählt er eine historische Datei-ID aus.
Sichere und effiziente Dateiverarbeitung
Beschränken Sie die Berechtigungen des Schlüssels auf den benötigten Bucket und das benötigte
Präfix. Lassen Sie die TLS-Prüfung aktiviert und verwenden Sie den standardmäßigen
Autorisierungsendpunkt für B2. B2_AUTHORIZE_URL dient dazu, einen kontrollierten
lokalen Testdienst anzusprechen, und nicht dazu, einen Endpunkt von einem nicht vertrauenswürdigen
Aufrufer zu akzeptieren.
Der Importer prüft, ob die HTTP-Übertragung abgeschlossen ist und das Byte-Limit eingehalten wird. Er vergleicht keine vom Herausgeber bereitgestellte Prüfsumme und entschlüsselt keine Dateien, die mit kundenseitig verwalteten Verschlüsselungsschlüsseln hochgeladen wurden. Benötigt Ihr Workflow eine Authentizitätsprüfung des Inhalts oder SSE-C, ergänzen Sie diese Prüfung oder die Schlüsselverarbeitung, bevor die Datei am Ziel bereitgestellt wird.
Für die serverseitige Verarbeitung nach einem Import steht Ihnen der Robot für den Backblaze-Import zur Verfügung.
