omniscient / omnidrive
Omnidrive: one contract for reading and writing files wherever they are kept - a directory of this machine, an S3 bucket, an SFTP (SSH) or FTP server - by the name the application gives each storage; Flysystem is the engine behind it, never in its signatures.
Requires
- php: >=8.2
- league/flysystem: ^3.0
- psr/container: ^1.1|^2.0
Requires (Dev)
- league/flysystem-local: ^3.0
- league/flysystem-memory: ^3.0
- phpunit/phpunit: ^11.0
Suggests
- league/flysystem-memory: Omnidrive\Testing\InMemoryDriveFactory: a drive in memory for an application's tests
- omnidrive/ftp: An FTP or FTPS server (league/flysystem-ftp, ext-ftp)
- omnidrive/local: A directory of this machine (league/flysystem-local)
- omnidrive/s3: An S3 bucket: AWS, MinIO, Scaleway, OVH, Cloudflare R2... (league/flysystem-async-aws-s3)
- omnidrive/sftp: An SFTP server, over SSH (league/flysystem-sftp-v3, phpseclib)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-24 13:51:21 UTC
README
One contract for reading and writing files wherever they are kept - a directory of this machine, an S3 bucket, an SFTP (SSH) or FTP server - by the name the application gives each storage.
use Omnidrive\Local\LocalDriveFactory; use Omnidrive\Registry; use Omnidrive\S3\S3DriveFactory; $drives = new Registry([new LocalDriveFactory(), new S3DriveFactory()], [ 'uploads' => ['factory' => 'local', 'options' => ['root' => __DIR__.'/var/uploads', 'public_url' => '/uploads']], 'archive' => ['factory' => 's3', 'options' => ['bucket' => 'archive', 'region' => 'eu-west-3', 'key' => getenv('S3_KEY'), 'secret' => getenv('S3_SECRET')]], ]); $drives->get('uploads')->write('avatars/1.png', $bytes); $drives->get('uploads')->publicUrl('avatars/1.png'); // /uploads/avatars/1.png $drives->get('archive')->temporaryUrl('2026/report.pdf', new DateTimeImmutable('+1 hour')); foreach ($drives->get('archive')->list('2026', deep: true) as $entry) { echo $entry->path, ' ', $entry->size, "\n"; }
No bundle, no container: a factory per provider package, the registry built by hand. docs/installation.md opens on a whole script that runs as it is.
The contract, and Flysystem behind it
An application type-hints Omnidrive\Drive (one storage) and Omnidrive\Drives (the storages by
name), and nothing else:
Drive |
|
|---|---|
read, readStream, write, writeStream |
the bytes, as a string or a stream; a write replaces, missing directories are made |
exists, directoryExists, delete, deleteDirectory, createDirectory |
a missing file or directory deleted is no error |
list($path, $deep) |
lazily, Entry objects: path, directory or file, size, last change, visibility |
move, copy |
within the drive |
metadata, size, mimeType, lastModified, checksum |
|
visibility, setVisibility |
Visibility::Public or Private |
publicUrl, temporaryUrl($path, $expiresAt) |
the public address, a signed one that expires |
localPath |
the file of this machine behind the path, null for a remote drive |
capabilities() |
said beforehand: local, publicUrls, temporaryUrls |
What fails is an Omnidrive\Exception\DriveException - its operation, drive and path kept,
FileNotFoundException when the file is not there; what a drive cannot do (a temporary address
from an FTP server) is a NotSupportedException, and its Capabilities say so first.
league/flysystem is the engine: every drive is a
Omnidrive\Flysystem\FlysystemDrive over a Flysystem operator, and each provider maps its options
onto the league's own adapter - no adapter is rewritten here, none is forked. Flysystem appears in
no signature of the contract: an application that keeps to Drive and Drives would not change
if the engine did. Its exceptions are kept as the contract's previous one. This package follows
Flysystem's current major (^3.0); a new major is taken when it ships.
Providers
| Package | Drive | Engine |
|---|---|---|
omnidrive/local |
a directory of this machine: its root, Unix modes, links refused or skipped | league/flysystem-local |
omnidrive/s3 |
an S3 bucket - AWS, MinIO, Scaleway, OVHcloud, Cloudflare R2...: region or endpoint, keys, prefix, public and signed addresses | league/flysystem-async-aws-s3 (AsyncAws) |
omnidrive/sftp |
a directory of an SFTP server, over SSH: password, private key or agent, the server's key checked | league/flysystem-sftp-v3 (phpseclib 3) |
omnidrive/ftp |
a directory of an FTP or FTPS server: passive mode, a server behind NAT | league/flysystem-ftp (ext-ftp) |
omnidrive/webdav |
a collection of a WebDAV server - Nextcloud, ownCloud, Apache, nginx: basic, digest or NTLM | league/flysystem-webdav (sabre/dav) |
omnidrive/azure |
a container of Azure Blob Storage: connection string or account key, prefix, read-only SAS addresses | azure-oss/storage-blob-flysystem (the successor The PHP League names for its abandoned adapter) |
omnidrive/dropbox |
a folder of Dropbox: a refresh token renewed on its own, a team member or space, temporary links | spatie/flysystem-dropbox |
omnidrive/gdrive |
a folder of Google Drive - My Drive, a shared drive, a shared folder: OAuth or a service account, paths by name | masbug/flysystem-google-drive-ext (google/apiclient) |
Not provided: OneDrive. The Flysystem 3 adapters for it are justus/flysystem-onedrive (it requires Laravel's illuminate/support, and Microsoft's Graph SDK 1.x, two majors behind) and as247/flysystem-onedrive (no release since May 2024, Graph SDK 1.x too); shitware-ltd/flysystem-msgraph has had none since 2023. omnidrive/onedrive waits for a maintained, framework-free one.
Every drive also takes public_url (a prefix, or a list of them), visibility,
directory_visibility and retain_visibility; the local, SFTP and FTP drives permissions (the
Unix mode of each visibility). Omnidrive\Testing\InMemoryDriveFactory (league/flysystem-memory)
gives an application's tests a drive in memory, and Omnidrive\Testing\DriveContractTests the
tests every drive passes - for a drive of one's own too.
Hosts
A framework already has its storages; it gives them as drives rather than configuring them twice.
Omnidrive\Flysystem\OperatorDrives is Drives over a PSR-11 container of Flysystem operators:
- Symfony: omnibridge/symfony registers
Omnidrive\Drivesfrom the storages of league/flysystem-bundle's configuration (the services taggedflysystem.storage), by their names; - Laravel: omnibridge/laravel registers
it from the disks of
config/filesystems.php.
docs/hosts.md has both, and how omniscient/omnibase reads its storages through it.
Documentation
- Installation and a first script
- Drives: the contract, the options, writing a provider
- Hosts: Symfony, Laravel
- The Docker harness: bare PHP, the servers it runs
Docker: every provider, and a server of each kind
cd docker && cp .env.dist .env docker compose run --rm omnidrive bare # plain PHP: memory and local drives used whole, what PHP loaded docker compose run --rm omnidrive bare --live # and S3 (MinIO), SFTP (OpenSSH), FTP (Pure-FTPd), WebDAV (Apache) and Azure (Azurite) used whole docker compose run --rm omnidrive test # every package's tests, the live ones against those servers docker compose down
Verified, and not
- Verified (2026-10-23,
docker compose run --rm omnidrive testindocker/, PHP 8.4, Flysystem 3.x as Composer resolves it): the contract's tests (DriveContractTests: write, replace, read, streams of 300 KB, missing files, shallow and deep listings, move, copy, directories, metadata, MD5 checksums, visibility, addresses, local paths) on the memory and local drives, and live on MinIO (S3, path style), OpenSSH's SFTP (atmoz/sftp, password) and Pure-FTPd (passive mode); a temporary S3 address signed and fetched, the file served, the bucket's plain address refused. In plain PHP (BareTest,bin/bare --live): the registry built by hand, every drive used whole, no class of a framework loaded - of Symfony only the HTTP client AsyncAws calls S3 through. The S3 requests offline (MockHttpClient): path style, prefix, ACL headers, signatures; PHPStan at its highest level on each package installed alone. - Verified (2026-10-24, the same harness): the contract's tests live on Apache's mod_dav (WebDAV,
bytemark/webdav, basic authentication) and on Nextcloud's WebDAV (the nextcloud:apache image of that day, SQLite,
--profile nextcloud), a file's WebDAV address served with the account's credentials; live on Azurite (Azure Blob Storage, its development account), a SAS address signed and fetched, the blob's plain address refused; Dropbox against a stand-in of its HTTP API written from Dropbox's documentation (not recorded responses: no account here), with the refresh token's renewal after a 401; Google Drive offline only: the OAuth refresh token and a service account's signed assertion reach Google's token endpoint, the Drive API is asked with the token.bare --liveuses the WebDAV and Azure drives whole. - Not verified: Dropbox and Google Drive themselves - their live tests run with an account's tokens
(
OMNIDRIVE_DROPBOX_*,OMNIDRIVE_GDRIVE_*), none was at hand; Azure Blob Storage itself (Azurite only) and an account with a hierarchical namespace; ownCloud; AWS S3 itself and the other S3 services (Scaleway, OVHcloud, R2) - MinIO only; an object's ACL (MinIO answers 501 to it; an AWS bucket made since April 2023 refuses ACLs unless its Object Ownership allows them:OMNIDRIVE_S3_ACL=1runs that test against one that does); SFTP with a private key or the SSH agent, and the server's key checked (password only); FTPS; vsftpd: its STAT answers nothing for a file below a subdirectory (seen with vsftpd 3.0.5), so Flysystem cannot read such a file's visibility, and a copy - which keeps its source's visibility by default - fails there: give such a driveretain_visibility: false; Windows FTP servers.
Responsibility
omniscient/omnidrive and its omnidrive/* providers are independent, unofficial clients: none is
affiliated with, endorsed by or sponsored by the services or the software they reach (Amazon Web
Services, Microsoft, Dropbox, Google, Nextcloud, MinIO, any hosting company) nor by The PHP League, whose Flysystem they run on. Each
service keeps its own terms and its own prices - a bucket's requests and transfers are billed by its
provider. The MIT licence covers this code only.
Whoever installs and runs these packages is responsible for the data they store and for the access they give to it - a public address or a temporary one serves the file to whoever holds it. The authors publish them free of charge, make no commercial use of them, and accept no liability for how they are used (see LICENSE).
License: MIT.