esac/backblaze-b2
Laravel package for integrating Backblaze B2 cloud storage, offering a simple API to upload, download, list, and manage files in B2 buckets. It wraps Backblaze B2's native API for use within Laravel applications and storage workflows.
Install
composer require esac/backblaze-b2
There is no Laravel auto-discovery, no service provider, and no facade. Expect to new the client yourself.
First place to look
README.md — likely thin; treat method names as provisional.src/ — the client class is the whole API surface. Grep for function to enumerate authorizeAccount, getUploadUrl, uploadFile, listFileNames, deleteFileVersion, getDownloadUrl, etc.composer.json — check the PHP constraint and whether it depends on Guzzle or raw cURL. This determines how easily you can fake it in tests.Config you actually need Get these from the Backblaze B2 console under App Keys:
applicationKeyIdapplicationKey (secret — never commit; put in .env)bucketId (the ID, not the bucket name).env
B2_APPLICATION_KEY_ID=
B2_APPLICATION_KEY=
B2_BUCKET_ID=
B2_BUCKET_NAME=
First use case: upload a file and get a URL back
use Esac\BackblazeB2\Client; // verify namespace in src/
$client = new Client();
$client->authorizeAccount(env('B2_APPLICATION_KEY_ID'), env('B2_APPLICATION_KEY'));
$upload = $client->getUploadUrl(env('B2_BUCKET_ID'));
$file = $client->uploadFile(
$upload['uploadUrl'],
$upload['authorizationToken'],
'invoices/2024/0001.pdf',
file_get_contents($path),
'application/pdf'
);
// $file['fileId'], $file['fileName'] -> persist in DB
That round trip — authorize, get upload URL, upload, store fileId — is the 90% case. Everything else in the SDK is variations on it.
Register a singleton in a service provider
Don't authorize on every request; the auth token is valid for ~24h but the round trip is a wasted 200ms. Bind one client per process:
// app/Providers/AppServiceProvider.php
$this->app->singleton(\Esac\BackblazeB2\Client::class, function () {
$client = new \Esac\BackblazeB2\Client();
$client->authorizeAccount(
config('services.b2.key_id'),
config('services.b2.key')
);
return $client;
});
Add config/services.php:
'b2' => [
'key_id' => env('B2_APPLICATION_KEY_ID'),
'key' => env('B2_APPLICATION_KEY'),
'bucket' => env('B2_BUCKET_ID'),
],
Wrap it in a domain service, not controllers
The SDK is a transport layer. Give your app a B2Storage class with methods like store(UploadedFile $file): StoredFile and delete(string $fileId): void. This is your seam for swapping to the Flysystem B2 adapter later — and you probably will.
Upload pattern: stream, don't buffer
file_get_contents() on a 500MB video is a memory bomb. For small files (<100MB) it's fine. For anything larger, use the large-file API:
$large = $client->startLargeFile($bucketId, $fileName, 'video/mp4');
$part = $client->getUploadPartUrl($large['fileId']);
$client->uploadPart($part['uploadUrl'], $part['authorizationToken'], 1, $chunk);
$client->finishLargeFile($large['fileId'], $sha1OfWholeFile, $partCount);
Note the SHA1 requirement on finishLargeFile — back it up or the file is orphaned.
Store metadata, not URLs
B2 download URLs are signed and expire. Persist fileId and fileName; generate the download URL on read:
$url = $client->getDownloadUrl($bucketName, $fileId); // or the auth-token variant
Backup/sync job pattern
listFileNames is paginated (startFileName, maxFileCount) and returns a nextFileName. Loop until it's null:
$cursor = null;
do {
$page = $client->listFileNames($bucketId, $cursor, 1000);
foreach ($page['files'] as $f) { /* upsert into DB */ }
$cursor = $page['nextFileName'] ?? null;
} while ($cursor);
Queue long uploads
B2 uploads are slow and the SDK is synchronous. Push to a queued job, return a pending status, and let the job write fileId back to the model.
Testing
There's no mock client. Two options: (a) wrap the SDK behind your own interface and fake that, or (b) put a fake Client in the container via $this->mock(Client::class) and shouldReceive() the methods you use. Option (a) survives the imminent swap to a maintained library; option (b) is faster.
This package is archived and last released 2019-04-16. That's a real, not theoretical, problem. Consequences:
strlen(null)-style bugs.composer require on your PHP version and do a smoke test upload. If it breaks, vendor the source into app/Support/Backblaze or use the Flysystem B2 adapter instead.License: LGPL-3.0. Fine for linking from a Laravel app, but if you modify the SDK source and distribute your app (SaaS is generally fine; shipped binaries are murkier), you may need to publish those modifications. Keeping it as an unmodified vendor dependency is the safe posture. Legal should glance at this if the product is commercial and distributed.
Bucket ID vs. bucket name. Most methods want bucketId; getDownloadUrl-style calls want bucketName. Mixing them yields 400 bad_request with a useless message. Keep both in config and be deliberate about which you pass where.
Auth tokens expire in 24 hours. In a long-running queue worker, a singleton authorized at boot will 401 after a day. Either authorize per job, or catch 401s and re-authorize once before retrying. A naive retry() in Horizon will not save you — every retry uses the same stale token.
Upload URLs are single-use and per-shard. getUploadUrl returns a URL valid for one upload. Do not cache it in Redis and reuse across requests; B2 will reject the second call. Fetch one per file.
Errors are inconsistent. The SDK may throw exceptions, return arrays with statusCode, or return false depending on the method and the fork's error handling. Read src/ for the method you're calling and code defensively. Log the full decoded response body — B2's code/message fields are the only thing that makes debugging possible.
Large-file SHA1. finishLargeFile requires a base64 SHA1 of the complete file. If you don't have it, the upload fails after you've paid for all the transfer. Compute it as you stream the chunks.
Memory limits on download. downloadFileById returns the body as a string. Set memory_limit deliberately or stream to a temp file; a 2GB backup restore will OOM a default PHP process.
Extension point: re-authorize. The most common patch developers make is a withRetry() decorator that catches 401/403, calls authorizeAccount again, and retries once. Do this before you hit production, not after.
Escape hatch. When (not if) this package fails on a PHP upgrade, the migration target is backblaze-b2 + Flysystem (or league/flysystem-b2), which gives you a Storage::disk('b2') Laravel-native API. Structuring your B2Storage service around file IDs now means that swap is a one-file change later.
How can I help you explore Laravel packages today?