Importa archivos de Backblaze en PHP con la API nativa
Para importar un objeto privado de Backblaze B2 en PHP, obtén autorización con la API nativa y transfiere su descarga por streaming a un archivo temporal. La extensión cURL de PHP, de código abierto, gestiona las solicitudes HTTP sin depender de un SDK. El comando siguiente reemplaza el destino que elijas solo después de que la transferencia y las escrituras en disco finalicen correctamente.
Requisitos
- Una línea de comandos de Linux con PHP CLI 8.5.10 o un parche posterior de la versión 8.5 y
ext-curl. Este ejemplo se probó con PHP 8.5.10. Usa el parche más reciente de esa rama con mantenimiento. - Un bucket privado de B2 existente y el nombre exacto de un objeto que puedas leer sin encabezados SSE-C.
- Una clave de aplicación con acceso
readFilesa ese bucket y prefijo de objeto. Necesitas su ID de clave y su clave de aplicación, que son valores distintos. - Un directorio de destino existente con permisos de escritura en un sistema de archivos local. Ejecuta un solo importador a la vez para un destino determinado.
Verifica la CLI instalada antes de guardar el programa:
php -r 'exit(PHP_MAJOR_VERSION === 8 && PHP_MINOR_VERSION === 5 && version_compare(PHP_VERSION, "8.5.10", ">=") && extension_loaded("curl") ? 0 : 1);'
Un código de salida cero significa que esta CLI cumple los requisitos previos. Si la verificación falla, instala o habilita cURL para tu CLI de PHP, o selecciona la versión de PHP compatible y repite la verificación. No es necesario instalar dependencias con Composer.
Usa la API nativa de B2
La respuesta de autorización v4 coloca
downloadUrl dentro de apiInfo.storageApi; el token de autorización permanece en el nivel superior.
El ID de la clave y la clave de aplicación autentican esa primera solicitud mediante autenticación
básica HTTP. Luego, la descarga usa el token devuelto en su encabezado Authorization.
Guarda el siguiente programa completo como import-b2.php. Acepta endpoints HTTPS y
endpoints HTTP de loopback para pruebas locales, y rechaza las redirecciones en ambas solicitudes.
Importa un objeto
<?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);
}
Desde ese directorio, sustituye las credenciales, el bucket y el nombre del objeto en este comando.
En una aplicación, proporciona las credenciales mediante tu gestor de secretos en lugar de guardarlas
en el código fuente o en el historial del shell. Usa ./ antes de un destino
relativo, incluidos los nombres que comienzan con -.
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
Si la operación tiene éxito, termina con un código de salida cero sin imprimir nada;
./report.bin contiene los bytes descargados. Una nueva ejecución exitosa reemplaza
ese archivo. El código almacena provisionalmente los bytes en un archivo .b2-
único junto al destino, lo cierra y luego usa
rename() para publicarlo.
En un sistema de archivos local de Linux,
el reemplazo es atómico, por lo que los lectores ven el archivo
anterior o el reemplazo completo. Esto no garantiza la persistencia ante fallos ni coordina
importadores concurrentes.
El límite predeterminado es de 100 MiB, inclusive. Establece B2_MAX_BYTES en otro
entero positivo en bytes cuando sea necesario. Las respuestas de autorización tienen un límite
independiente de 1 MiB. Cada solicitud tiene un
plazo máximo de 60 segundos para toda la transferencia, incluido
el establecimiento de la conexión; B2_TIMEOUT_SECONDS acepta enteros de 1 a 3.600.
La autorización y la descarga usan plazos independientes, por lo que incluso un archivo pequeño
puede fallar con una conexión lenta. Ninguna de las solicitudes realiza reintentos.
Si la operación falla, termina con un código de salida uno y registra un error genérico sin cuerpos
de respuesta ni credenciales. Un fallo de autenticación, un objeto inexistente o cuyo acceso se
ha denegado, una transferencia HTTP interrumpida o que excede el límite de tamaño y un fallo de
escritura en disco dejan intacto el destino anterior y eliminan el archivo temporal de esta
invocación. Si no había un destino anterior, el fallo no deja ninguno. La terminación forzada del
proceso o un fallo del equipo pueden dejar un archivo .b2-; elimínalo solo
después de confirmar que su importador se ha detenido.
Adapta el importador
Establece B2_OBJECT en el nombre literal del objeto en UTF-8, sin codificarlo
primero para una URL. Siguiendo las
reglas de codificación de B2, el código codifica cada segmento de
la ruta y conserva los separadores /, los espacios literales y los
caracteres +, %, #
y ?.
Rechaza los segmentos de ruta . y ...
La descarga por nombre selecciona la
versión visible actual;
este comando no enumera el contenido de un bucket ni selecciona el ID de una versión histórica del archivo.
Gestión segura y eficiente de archivos
Limita el ámbito de la clave al bucket y al prefijo que necesites. Mantén habilitada la verificación
TLS y usa el endpoint de autorización predeterminado de B2; B2_AUTHORIZE_URL existe
para probar un servicio local de pruebas controlado, no para aceptar un endpoint de un solicitante
no confiable.
El importador verifica que la transferencia HTTP se complete y que se respete el límite de bytes. No compara una suma de verificación proporcionada por quien publica el archivo ni descifra archivos subidos con claves de cifrado gestionadas por el cliente. Si tu flujo de trabajo requiere autenticación del contenido o SSE-C, añade esa verificación o la gestión de claves antes de publicar el archivo.
Para procesar archivos en el servidor después de una importación, consulta el Robot de importación desde Backblaze.
