Important
This repository is a read-only mirror of the utopia-php monorepo. Development happens in packages/storage — please open issues and pull requests there.
Utopia Storage is a simple and lightweight library for managing application storage across multiple adapters. This library is designed to be easy to learn and use, with a consistent API regardless of the storage provider. This library is maintained by the Appwrite team.
This library is part of the Utopia Framework project.
Install using Composer:
composer require utopia-php/storageDevices are immutable value objects: construct one with its configuration and use it anywhere, including across coroutines.
<?php
require_once '../vendor/autoload.php';
use Utopia\Storage\Device\Local;
$device = new Local('/path/to/storage');
// Upload a file
$device->uploadData(file_get_contents('/local/path/to/file.png'), 'destination/path/file.png', 'image/png');
// Check if file exists
$exists = $device->exists('destination/path/file.png');
// Read file contents
$contents = $device->read('destination/path/file.png');
// Delete a file
$device->delete('destination/path/file.png');Use the local filesystem for storing files.
use Utopia\Storage\Device\Local;
$device = new Local('/path/to/storage');Store files in Amazon S3 or compatible services.
use Utopia\Storage\Acl;
use Utopia\Storage\Device\S3;
$device = new S3(
'root', // Root path in bucket
'YOUR_ACCESS_KEY',
'YOUR_SECRET_KEY',
'YOUR_BUCKET_NAME.s3.us-east-1.amazonaws.com', // Host
'us-east-1', // Region
Acl::Private // Access control (default: private)
);The provider-specific adapters below build the host for you from a bucket and region. Every S3-family adapter also accepts optional named constructor arguments:
use Utopia\Storage\Acl;
use Utopia\Storage\Device\AWS;
$device = new AWS(
'root',
'YOUR_ACCESS_KEY',
'YOUR_SECRET_KEY',
'YOUR_BUCKET_NAME',
AWS::US_EAST_1,
Acl::Private,
client: $psrClient, // any PSR-18 client (default: utopia-php/client with retries, see below)
);
// Available ACL options
// Acl::Private, Acl::PublicRead, Acl::PublicReadWrite, Acl::AuthenticatedReadStore files in DigitalOcean Spaces.
use Utopia\Storage\Acl;
use Utopia\Storage\Device\DOSpaces;
$device = new DOSpaces(
'root', // Root path in bucket
'YOUR_ACCESS_KEY',
'YOUR_SECRET_KEY',
'YOUR_BUCKET_NAME',
DOSpaces::NYC3, // Region (default: nyc3)
Acl::Private // Access control (default: private)
);
// Available regions
// DOSpaces::NYC3, DOSpaces::SGP1, DOSpaces::FRA1, DOSpaces::SFO2, DOSpaces::SFO3, DOSpaces::AMS3Store files in Backblaze B2 Cloud Storage.
use Utopia\Storage\Acl;
use Utopia\Storage\Device\Backblaze;
$device = new Backblaze(
'root', // Root path in bucket
'YOUR_ACCESS_KEY',
'YOUR_SECRET_KEY',
'YOUR_BUCKET_NAME',
Backblaze::US_WEST_004, // Region (default: us-west-004)
Acl::Private // Access control (default: private)
);
// Available regions (clusters)
// Backblaze::US_WEST_000, Backblaze::US_WEST_001, Backblaze::US_WEST_002,
// Backblaze::US_WEST_004, Backblaze::EU_CENTRAL_003Store files in Linode Object Storage.
use Utopia\Storage\Acl;
use Utopia\Storage\Device\Linode;
$device = new Linode(
'root', // Root path in bucket
'YOUR_ACCESS_KEY',
'YOUR_SECRET_KEY',
'YOUR_BUCKET_NAME',
Linode::EU_CENTRAL_1, // Region (default: eu-central-1)
Acl::Private // Access control (default: private)
);
// Available regions
// Linode::EU_CENTRAL_1, Linode::US_SOUTHEAST_1, Linode::US_EAST_1, Linode::AP_SOUTH_1Store files in Wasabi Cloud Storage.
use Utopia\Storage\Acl;
use Utopia\Storage\Device\Wasabi;
$device = new Wasabi(
'root', // Root path in bucket
'YOUR_ACCESS_KEY',
'YOUR_SECRET_KEY',
'YOUR_BUCKET_NAME',
Wasabi::EU_CENTRAL_1, // Region (default: eu-central-1)
Acl::Private // Access control (default: private)
);
// Available regions
// Wasabi::US_EAST_1, Wasabi::US_EAST_2, Wasabi::US_WEST_1, Wasabi::US_CENTRAL_1,
// Wasabi::EU_CENTRAL_1, Wasabi::EU_CENTRAL_2, Wasabi::EU_WEST_1, Wasabi::EU_WEST_2,
// Wasabi::AP_NORTHEAST_1, Wasabi::AP_NORTHEAST_2All storage adapters provide a consistent API for working with files:
// Upload file contents
$device->uploadData(file_get_contents('/path/to/local/file.jpg'), 'remote/path/file.jpg', 'image/jpeg');
// Check if file exists
$exists = $device->exists('remote/path/file.jpg');
// Get file size
$size = $device->getFileSize('remote/path/file.jpg');
// Get file MIME type
$mime = $device->getFileMimeType('remote/path/file.jpg');
// Get file MD5 hash
$hash = $device->getFileHash('remote/path/file.jpg');
// Read file contents
$contents = $device->read('remote/path/file.jpg');
// Read partial file contents
$chunk = $device->read('remote/path/file.jpg', 0, 1024); // Read first 1KB
// Multipart/chunked uploads
$metadata = [];
$device->uploadData($firstChunk, 'remote/video.mp4', 'video/mp4', 1, 3, $metadata); // Part 1 of 3
// Resumable uploads: prepare, upload chunks in any order, finalize
$device->prepareUpload('remote/video.mp4', 'video/mp4', 3, $metadata);
$device->uploadChunk($secondChunk, 'remote/video.mp4', 2, 3, $metadata);
$device->finalizeUpload('remote/video.mp4', 3, $metadata);
// List files under a prefix, one page at a time
$list = $device->listFiles('remote/directory', 100);
foreach ($list->files as $file) {
echo $file->path . ' (' . $file->size . " bytes)\n";
}
if ($list->cursor !== null) {
$list = $device->listFiles('remote/directory', 100, $list->cursor); // Next page
}
// Delete file
$device->delete('remote/path/file.jpg');
// Delete directory
$device->deletePath('remote/directory');
// Transfer files between storage devices
$sourceDevice->transfer('source/path.jpg', 'target/path.jpg', $targetDevice);
// Transfer with a custom chunk size (default: 20 MB)
$sourceDevice->transfer('source/path.jpg', 'target/path.jpg', $targetDevice, 10000000);The S3-family adapters send requests through any PSR-18 client. By default they use utopia-php/client with the cURL adapter, no request timeout, and the Retry decorator configured with S3\RetryStrategy — it retries transient S3 rate-limiting errors (SlowDown, ServiceUnavailable, Throttling, RequestThrottled, and plain 429/503 responses) three times with a 500 ms delay.
Inject your own client to change the transport or the retry policy — for example the Swoole coroutine adapter with more aggressive retries:
use Utopia\Client;
use Utopia\Client\Adapter\SwooleCoroutine\Client as SwooleAdapter;
use Utopia\Client\Decorator\Retry;
use Utopia\Storage\Device\S3;
use Utopia\Storage\Device\S3\RetryStrategy;
$client = new Retry(
new Client(new SwooleAdapter())->withTimeout(60),
new RetryStrategy(retries: 5, delay: 1.0),
);
$device = new S3('root', 'ACCESS_KEY', 'SECRET_KEY', 'HOST', 'us-east-1', client: $client);Omit the Retry decorator to disable retries entirely, or pass any Utopia\Client\Decorator\Retry\Strategy of your own.
Every runtime failure throws a subclass of Utopia\Storage\Exception\StorageException, so one catch covers any storage problem. Match more precisely when you need to branch:
use Utopia\Storage\Exception\NotFoundException;
use Utopia\Storage\Exception\RemoteException;
use Utopia\Storage\Exception\StorageException;
use Utopia\Storage\Exception\TransportException;
use Utopia\Storage\Exception\UploadException;
try {
$contents = $device->read('remote/path/file.jpg');
} catch (NotFoundException) {
// The file does not exist
} catch (TransportException $e) {
// The request never reached the service; $e->getPrevious() is the PSR-18 exception
} catch (RemoteException $e) {
// The service answered with an error: $e->getCode() is the HTTP status,
// $e->errorCode the service's own error identifier (for example `SlowDown`)
} catch (UploadException) {
// A chunked upload is in an invalid state (missing chunk, never prepared)
} catch (StorageException $e) {
// Anything else, such as a local filesystem failure
}Invalid arguments (a non-positive chunk size, a page size above the adapter limit) throw SPL \InvalidArgumentException — these are programmer errors, not storage failures.
Wrap any device with the Telemetry decorator to record a storage.operation histogram for every call through a utopia-php/telemetry adapter:
use Utopia\Storage\Device\Local;
use Utopia\Storage\Device\Telemetry;
$device = new Telemetry($telemetryAdapter, new Local('/path/to/storage'));Version 3.0 makes every device immutable and safe to share across coroutines, and removes all global state:
- The
Storageclass is gone entirely: replaceStorage::setDevice('files', $device)andStorage::getDevice('files')with your own wiring (a container, or passing the device instance directly), and inlineStorage::human()if you used it. setTelemetry()is gone: wrap the device in theUtopia\Storage\Device\Telemetrydecorator instead.setHttpVersion()is gone: transport options now belong to the injected PSR-18 client. The staticS3::setRetryAttempts()/S3::setRetryDelay()moved into theS3\RetryStrategyused by the default client'sRetrydecorator — inject your own client to tune or disable retries.setTransferChunkSize()/getTransferChunkSize()became a per-call argument:transfer($path, $destination, $device, $chunkSize).- String constants became enums: the
Storage::DEVICE_*constants are now theUtopia\Storage\DeviceTypeenum (getType()returns it), and theS3::ACL_*constants are now theUtopia\Storage\Aclenum. - The S3 adapter no longer stores request headers on the instance, so one device can serve concurrent requests (for example Swoole coroutines) without data races.
- Requests go through a PSR-18 client instead of raw cURL calls. The default is utopia-php/client with the cURL adapter; pass the
clientconstructor argument to swap the transport. - Uploads are data-based:
upload($sourcePath, ...)was removed — read the file yourself and calluploadData($data, ...)— anduploadChunk()now takes the chunk contents instead of a source file path. - Filesystem-only methods (
createDirectory(),getDirectorySize(),getPartitionFreeSpace(),getPartitionTotalSpace()) left the baseDevicecontract and remain onLocal; wrap devices in theTelemetrydecorator and use itsgetDevice()accessor when you need them.getFiles()— which returned path strings onLocalbut a rawListObjectsV2array onS3— is replaced bylistFiles(prefix, max, cursor)returning typedFileList/FileInfovalue objects consistently on every adapter. getName()andgetDescription()were removed; usegetType()to identify an adapter.- Exceptions are typed: every runtime failure extends
Utopia\Storage\Exception\StorageException(NotFoundException,TransportException,RemoteException,UploadException) instead of bare\Exception, and missing files throwNotFoundExceptionconsistently on every adapter and method — including S3 HEAD responses, which previously surfaced as a generic error with an empty message. Existingcatch (\Exception)blocks keep working.
For information on adding new storage adapters, see the Adding New Storage Adapter guide.
Utopia Storage requires PHP 8.5 or later. We recommend using the latest PHP version whenever possible.
For security issues, please email security@appwrite.io instead of posting a public issue in GitHub.
All code contributions - including those of people having commit access - must go through a pull request and be approved by a core developer before being merged. This is to ensure a proper review of all the code.
We welcome you to contribute to the Utopia Storage library. For details on how to do this, please refer to our Contributing Guide.
This library is available under the MIT License.
Copyright (c) 2019-2025 Appwrite Team <team@appwrite.io>