Importar arquivos do Backblaze em PHP com a Native API
Para importar um objeto privado do Backblaze B2 em PHP, obtenha autorização com a Native API e transfira o download por streaming para um arquivo temporário. A extensão cURL de código aberto do PHP faz as requisições HTTP sem depender de um SDK. O comando abaixo substitui o destino escolhido somente depois que a transferência e as gravações em disco forem concluídas com sucesso.
Requisitos
- Uma linha de comando Linux com PHP CLI 8.5.10 ou uma versão de correção posterior da linha PHP 8.5 e
ext-curl. Este exemplo foi testado com PHP 8.5.10. Use a versão de correção mais recente dessa linha com manutenção ativa. - Um bucket B2 privado existente e o nome exato de um objeto que você possa ler sem cabeçalhos SSE-C.
- Uma chave de aplicação com acesso
readFilesa esse bucket e prefixo de objeto. Você precisa do ID da chave e da chave de aplicação, que são valores distintos. - Um diretório de destino existente e com permissão de gravação em um sistema de arquivos local. Execute apenas um importador por vez para um mesmo destino.
Verifique a CLI instalada antes de salvar o programa:
php -r 'exit(PHP_MAJOR_VERSION === 8 && PHP_MINOR_VERSION === 5 && version_compare(PHP_VERSION, "8.5.10", ">=") && extension_loaded("curl") ? 0 : 1);'
Um código de saída zero significa que essa CLI atende aos pré-requisitos. Se a verificação falhar, instale ou habilite o cURL para sua CLI do PHP, ou selecione a versão compatível do PHP e repita a verificação. Não é necessário instalar nada com o Composer.
Usar a B2 Native API
A resposta de autorização v4 coloca
downloadUrl dentro de apiInfo.storageApi; o token de autorização permanece no nível superior.
O ID da chave e a chave de aplicação autenticam essa primeira requisição usando HTTP Basic Auth.
O download usa então o token retornado no cabeçalho Authorization.
Salve o programa completo a seguir como import-b2.php. Ele aceita endpoints HTTPS
e endpoints HTTP de loopback para testes locais e recusa redirecionamentos nas duas requisições.
Importar um 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);
}
Nesse diretório, substitua as credenciais, o bucket e o nome do objeto neste comando pelos seus.
Em uma aplicação, forneça as credenciais por meio do seu gerenciador de segredos em vez de salvá-las
no código-fonte ou no histórico do shell. Use ./ antes de um destino
relativo, inclusive para nomes que comecem com -.
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
Em caso de sucesso, o processo termina com código de saída zero sem imprimir nada;
./report.bin contém os bytes baixados. Uma nova execução bem-sucedida substitui
esse arquivo. O código armazena provisoriamente os bytes em um arquivo .b2-
exclusivo no mesmo diretório do destino, fecha esse arquivo e usa
rename() para publicá-lo. Em um sistema de
arquivos Linux local, a substituição é atômica, então os leitores
veem o arquivo antigo ou a substituição concluída. Isso não garante durabilidade em caso de pane
nem coordena importadores simultâneos.
O limite padrão é de 100 MiB, inclusive. Defina B2_MAX_BYTES com outro inteiro
positivo em bytes quando necessário. As respostas de autorização têm um limite separado de 1 MiB.
Cada requisição tem um prazo de 60 segundos para toda a transferência,
incluindo o estabelecimento da conexão; B2_TIMEOUT_SECONDS aceita inteiros de 1 a 3.600.
A autorização e o download usam prazos separados, então até um arquivo pequeno pode falhar em uma
conexão lenta. Nenhuma das requisições faz novas tentativas.
Em caso de falha, o processo termina com código de saída um e registra um erro genérico, sem corpos
de resposta nem credenciais. Falhas de autenticação, objetos com acesso negado ou inexistentes,
transferências HTTP acima do limite ou interrompidas e falhas de gravação em disco mantêm o destino
anterior intacto e removem o arquivo temporário desta execução. Se não houver um destino anterior,
a falha não deixa nenhum arquivo no destino. O encerramento forçado do processo ou uma pane na
máquina pode deixar um arquivo .b2-; remova-o somente após confirmar que
o importador correspondente parou.
Adaptar o importador
Defina B2_OBJECT com o nome literal do objeto em UTF-8, sem aplicar codificação
de URL previamente. Seguindo as regras de codificação do B2,
o código codifica cada segmento do caminho, preservando os separadores
/ e os caracteres literais de espaço, +,
%, # e ?.
Ele rejeita os segmentos de caminho . e ...
O download pelo nome seleciona a versão visível atual;
esse comando não lista o conteúdo de um bucket nem seleciona um ID de arquivo histórico.
Tratamento seguro e eficiente de arquivos
Restrinja o escopo da chave ao bucket e ao prefixo de que você precisa. Mantenha a verificação TLS
habilitada e use o endpoint de autorização padrão do B2; B2_AUTHORIZE_URL existe para
exercitar um serviço de teste local controlado, não para aceitar um endpoint de um chamador não confiável.
O importador verifica a conclusão da transferência HTTP e o limite de bytes. Ele não compara uma soma de verificação fornecida pelo publicador nem descriptografa arquivos enviados por upload com chaves de criptografia gerenciadas pelo cliente. Se o seu fluxo de trabalho precisar de autenticação do conteúdo ou SSE-C, adicione essa verificação ou o tratamento das chaves antes de publicar o arquivo.
Para processamento no servidor após uma importação, consulte o Robot de importação do Backblaze.
