PHP 8.4+ wrapper for rclone, focused on safe, testable file operations across local disks, S3-compatible storage, SFTP, FTP, Google Drive, Dropbox, Mega, B2, and the rest of rclone's backends.
- Object-oriented providers around rclone remotes.
- Single-remote and two-remote operations through the same
Rcloneclass. - Transfer statistics and optional progress callbacks.
- Typed exceptions for rclone exit codes.
- Retry support with exponential backoff.
- Fluent include/exclude filters.
- Debug logging with secret redaction.
- v5 safety hardening for long-lived workers: instance-scoped options, provider collision detection, safe temporary remotes, private temp directories, and inline SFTP key support.
- PHP >= 8.4
ext-jsonrclonebinary available inPATH, or configured withRclone::setBIN()
composer require verseles/flyclone:^5.0use Verseles\Flyclone\Providers\LocalProvider;
use Verseles\Flyclone\Providers\S3Provider;
use Verseles\Flyclone\Rclone;
$local = new LocalProvider('local_disk');
$s3 = new S3Provider('archive_bucket', [
'access_key_id' => 'YOUR_KEY',
'secret_access_key' => 'YOUR_SECRET',
'region' => 'us-east-1',
]);
$rclone = new Rclone($local, $s3);
$result = $rclone->copy('/var/backups', 'my-bucket/backups');
if ($result->success) {
echo "Transferred {$result->stats->bytes} bytes at {$result->stats->speed_human}";
}For one-remote operations, pass only one provider:
$files = (new Rclone($local))->ls('/var/backups');| Backend | Class | Notes |
|---|---|---|
| Local filesystem | LocalProvider |
Local paths and temporary helper remotes |
| Amazon S3 / MinIO / R2 | S3Provider |
S3-compatible flags map to rclone env config |
| SFTP | SFtpProvider |
Supports password, key_file, and v5 inline key_pem/private_key |
| FTP | FtpProvider |
FTP/FTPS |
| Google Drive | GDriveProvider |
OAuth/config-driven rclone backend |
| Dropbox | DropboxProvider |
OAuth/config-driven rclone backend |
| Mega.nz | MegaProvider |
Mega backend |
| Backblaze B2 | B2Provider |
B2 backend |
| Encrypted remote | CryptProvider |
Wraps another provider |
| Union filesystem | UnionProvider |
Merges upstream providers |
| Any other rclone backend | Provider |
Generic provider class |
Provider names are normalized to rclone-safe uppercase remote names. In v5, names that normalize to an empty value are rejected, and conflicting provider environment variables fail fast instead of being silently overwritten.
$rclone = new Rclone($sourceProvider, $destProvider);
$rclone->copy('/source/dir', '/dest/dir');
$rclone->copyto('/source/file.txt', '/dest/file.txt');
$rclone->move('/source/dir', '/dest/dir');
$rclone->moveto('/source/file.txt', '/dest/file.txt');
$rclone->sync('/source/dir', '/dest/dir');
$rclone->delete('/path/to/delete');Upload and download helpers create unique temporary local remotes internally:
$rclone = new Rclone($s3);
$rclone->upload_file('/tmp/report.pdf', 'bucket/reports/report.pdf');
$download = $rclone->download_to_local('bucket/reports/report.pdf');
echo $download->local_path;Prefer inline PEM keys in v5. Flyclone passes the value to rclone as RCLONE_CONFIG_<REMOTE>_KEY_PEM; no key file is created by Flyclone.
use Verseles\Flyclone\Providers\SFtpProvider;
$sftp = new SFtpProvider('deploy', [
'host' => 'sftp.example.com',
'user' => 'deploy',
'private_key' => $privateKeyPem,
'key_use_agent' => false,
]);private_key is a convenience alias for key_pem. key_pem and key_file are mutually exclusive.
CryptProvider wraps another provider through the required remote flag.
use Verseles\Flyclone\Providers\CryptProvider;
use Verseles\Flyclone\Providers\S3Provider;
use Verseles\Flyclone\Rclone;
$s3 = new S3Provider('raw_archive', [
'access_key_id' => 'YOUR_KEY',
'secret_access_key' => 'YOUR_SECRET',
'region' => 'us-east-1',
]);
$encrypted = new CryptProvider('encrypted_archive', [
'remote' => $s3,
'remote_path' => 'encrypted-prefix',
'password' => Rclone::obscure('my-secret-password'),
'password2' => Rclone::obscure('my-salt'),
]);
(new Rclone($encrypted))->copy('/local/sensitive-data', '/');UnionProvider receives upstream providers through upstream_providers.
use Verseles\Flyclone\Providers\LocalProvider;
use Verseles\Flyclone\Providers\S3Provider;
use Verseles\Flyclone\Providers\UnionProvider;
use Verseles\Flyclone\Rclone;
$cache = new LocalProvider('cache', ['root' => '/tmp/cache']);
$archive = new S3Provider('archive', [/* config */]);
$union = new UnionProvider('combined', [
'upstream_providers' => [$cache, $archive],
'action_policy' => 'all',
'create_policy' => 'ff',
]);
$files = (new Rclone($union))->ls('/');Static configuration is still available and is captured by new Rclone instances at construction time:
Rclone::setBIN('/custom/path/to/rclone');
Rclone::setFlags(['checksum' => true]);
Rclone::setEnvs(['RCLONE_BUFFER_SIZE' => '64M']);
Rclone::setTimeout(300);
Rclone::setIdleTimeout(120);For long-lived workers, prefer instance-scoped options:
$rclone = (new Rclone($source, $dest))
->withFlags(['checksum' => true])
->withEnvs(['RCLONE_BUFFER_SIZE' => '64M'])
->withTimeout(300)
->withIdleTimeout(120);use Verseles\Flyclone\FilterBuilder;
$filter = FilterBuilder::create()
->extensions(['jpg', 'png', 'gif'])
->minSize('100K')
->maxSize('50M')
->exclude('**/thumbnails/**');
$rclone->withFilter($filter)->copy('/source', '/dest');use Verseles\Flyclone\RetryHandler;
$handler = RetryHandler::create()
->maxAttempts(5)
->baseDelay(500)
->multiplier(2.0)
->maxDelay(30000)
->onRetry(fn (int $attempt, Throwable $e) => logger("Retry {$attempt}: {$e->getMessage()}"));
$progress = function (object $progress): void {
echo $progress->percentage . "%\n";
};
$rclone->withRetry($handler)
->dryRun(false)
->copy('/source', '/dest', onProgress: $progress);use Verseles\Flyclone\Exception\DirectoryNotFoundException;
use Verseles\Flyclone\Exception\FileNotFoundException;
use Verseles\Flyclone\Exception\TemporaryErrorException;
try {
$rclone->copy('/source', '/dest');
} catch (FileNotFoundException|DirectoryNotFoundException $e) {
// Permanent user/input error.
} catch (TemporaryErrorException $e) {
if ($e->isRetryable()) {
// Retry may succeed.
}
$context = $e->getContext();
}use Verseles\Flyclone\Logger;
Logger::setDebugMode(true);
$rclone->copy('/source', '/dest');
echo $rclone->getLastCommand();
$envs = $rclone->getLastEnvs(); // Secrets are redacted.
$logs = Logger::getLogs();$remotes = Rclone::listRemotes();
$config = Rclone::configDump();
$md5 = $rclone->md5sum('/path');
$sha1 = $rclone->sha1sum('/path');
$health = $rclone->healthCheck('/');- Provider names that normalize to an empty rclone remote name now throw
InvalidArgumentException. - Conflicting provider env vars now throw
LogicExceptioninstead of overwriting values. - Two providers with the same normalized remote name and different config now fail fast.
- Existing
Rcloneinstances no longer observe later changes to static flags/envs/timeouts; usewithFlags(),withEnvs(),withTimeout(), andwithIdleTimeout()for per-instance updates. SFtpProviderrejects ambiguouskey_pempluskey_fileconfiguration.
See MIGRATION.md for migration notes.
composer install
# Fast local/offline checks
composer test-local
make test
# Full offline provider suite, when podman-compose is available
make test-offline
# Static analysis and formatting
composer analyse
composer run-script format-checkProvider-specific tests such as make test_dropbox and make test_gdrive require a configured .env.
Flyclone v5 is organized around small components:
| Component | Responsibility |
|---|---|
Rclone |
Public API and operation orchestration |
Provider and subclasses |
rclone remote configuration |
CommandBuilder |
Command arguments and environment variables |
ProcessManager |
Symfony Process execution and binary detection |
StatsParser |
Transfer statistics parsing |
ProgressParser |
Real-time progress parsing |
RetryHandler |
Retry policy and backoff |
FilterBuilder |
Include/exclude filter construction |
TemporaryPath |
Private temp directories and unique temporary remote names |
SecretsRedactor |
Secret redaction for errors/logs/envs |
Logger |
Structured debug logging |
- Hardened provider configuration for long-lived workers.
- Added instance-scoped flags, envs, and timeouts.
- Added provider env collision detection.
- Added remote-name collision detection.
- Added safe temporary local provider names.
- Added private temporary download directories.
- Added SFTP inline private key support via
key_pem/private_key. - Added configuration/security tests to the fast test path.
- Modularized
Rcloneinternals into process, command, stats, progress, retry, filter, logger, and redaction components. - Added typed exception context, health checks, dry-run mode, command introspection, and static utilities.
- Added
CryptProviderandUnionProvidersupport.
- Added detailed transfer statistics and progress tracking improvements.
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International