A pure-PHP directory synchronization tool inspired by the rsync command. Sync directories by copying new and modified files, removing deleted files, and skipping exclusions — all with a clean fluent API.
Requires PHP 8.5+
composer require diegobvasco/php-rsync
use DiegoVasconcelos\Rsync\Rsync;
$result = (new Rsync())
->copy('/path/to/source', '/path/to/destination')
->run();
echo $result->summary();
// Copied: 5 files (12.5 KB)
// Deleted: 2 files (1.2 KB)
// Skipped: 3 files
$result = (new Rsync())
->copy('/path/to/source', '/path/to/destination')
->skip(['*.log', 'vendor/*', '.git', 'node_modules/'])
->run();
foreach ($result->copied as $file) {
echo $file->relativePath . ' - ' . $file->formattedSize() . "\n";
}
$result = (new Rsync())
->copy('/path/to/source', '/path/to/destination')
->delete()
->run();
use DiegoVasconcelos\Rsync\Rsync;
use DiegoVasconcelos\Rsync\TerminalOutput;
// Print each file action to the terminal as it happens
$result = (new Rsync(new TerminalOutput()))
->copy('/path/to/source', '/path/to/destination')
->delete()
->run();
// Output:
// COPY new-file.txt (2.5 KB)
// DELETE old-file.txt
// SKIP unchanged.txt
$result = (new Rsync())
->copy('/path/to/source', '/path/to/destination')
->delete()
->dryRun()
->run();
// No files are actually copied or deleted
echo $result->summary();
$result->copiedCount(); // Number of files copied
$result->deletedCount(); // Number of files deleted
$result->skippedCount(); // Number of files skipped
$result->totalBytesCopied(); // Total bytes copied
$result->copiedPaths(); // Array of copied file paths
$result->deletedPaths(); // Array of deleted file paths
$result->skippedPaths(); // Array of skipped file paths
$result->summary(); // Human-readable summary string
| Pattern |
Meaning |
*.log |
All .log files |
vendor/* |
Everything inside vendor/ |
**/*.log |
Any .log file in any subdirectory |
logs/ |
The logs/ directory and all its contents |
??.txt |
Two-character .txt filenames |
[abc].txt |
Files named a.txt, b.txt, or c.txt |
[^ab].log |
Any single-character .log filename except a or b |
- Scans the source directory for all files
- Filters out files matching exclusion patterns
- Compares each file's modification time and size against the destination
- Copies only new or changed files
- Deletes destination files that no longer exist in the source (when
delete() is enabled)
- Removes empty directories from the destination
- Returns a
Result object with full sync details
new Rsync(?Output $output = null)
The optional Output interface receives callbacks for each file action during sync. A TerminalOutput implementation is included that prints to STDOUT (or a custom stream).
| Method |
Description |
copy(string $source, string $destination): self |
Set source and destination directories |
skip(string|array $patterns): self |
Add glob patterns to exclude from sync |
delete(): self |
Enable delete mode — removes destination files not in source |
recursive(): self |
Enable recursive mode |
archive(): self |
Enable archive mode (equivalent to -rlptgoD) |
run(): Result |
Execute the sync and return results |
toCommand(): string |
Generate equivalent rsync shell command for debugging |
reset(): self |
Reset all flags and options |
| Method |
Description |
times(): self |
Preserve modification times (--times) |
perms(): self |
Preserve permissions (--perms) |
owner(): self |
Preserve owner (--owner) |
group(): self |
Preserve group (--group) |
acls(): self |
Preserve ACLs (--acls) |
xattrs(): self |
Preserve extended attributes (--xattrs) |
devices(): self |
Preserve device files (--devices) |
specials(): self |
Preserve special files (--specials) |
numericIds(): self |
Don't map uid/gid values (--numeric-ids) |
| Method |
Description |
checksum(): self |
Use checksum instead of mod-time & size (--checksum) |
ignoreTimes(): self |
Don't skip files that match size and time (--ignore-times) |
sizeOnly(): self |
Skip files that match size only (--size-only) |
update(): self |
Skip files newer on the receiver (--update) |
| Method |
Description |
exclude(string|array $patterns): self |
Add patterns to exclude (--exclude) |
excludeFrom(string $file): self |
Read exclude patterns from file (--exclude-from) |
excludeDir(string|array $patterns): self |
Add directory exclusion patterns (--exclude-dir) |
include(string|array $patterns): self |
Add patterns to include (--include) |
includeFrom(string $file): self |
Read include patterns from file (--include-from) |
pruneEmptyDirs(): self |
Remove empty directories from file list (--prune-empty-dirs) |
| Method |
Description |
backup(): self |
Make backups of changed files (--backup) |
backupDir(string $dir): self |
Set backup directory (--backup-dir) |
suffix(string $suffix): self |
Set backup suffix (--suffix) |
| Method |
Description |
links(): self |
Copy symlinks as symlinks (--links) |
copyLinks(): self |
Transform symlinks to referent files (--copy-links) |
copyUnsafeLinks(): self |
Transform unsafe symlinks to referent files in directories (--copy-unsafe-links) |
safeLinks(): self |
Ignore symlinks that go outside tree (--safe-links) |
hardLinks(): self |
Preserve hard links (--hard-links) |
| Method |
Description |
maxSize(int|string $size): self |
Maximum file size to transfer (--max-size) |
minSize(int|string $size): self |
Minimum file size to transfer (--min-size) |
| Method |
Description |
dryRun(): self |
Show what would be done without doing it (--dry-run) |
force(): self |
Force deletion of non-empty directories (--force) |
removeSourceFiles(): self |
Remove source files after successful transfer (--remove-source-files) |
| Method |
Description |
verbose(): self |
Verbose output (--verbose) |
quiet(): self |
Suppress output (--quiet) |
progress(): self |
Show progress (--progress) |
stats(): self |
Show statistics (--stats) |
itemizeChanges(): self |
Show itemized changes (--itemize-changes) |
humanReadable(): self |
Human-readable numbers (--human-readable) |
| Method |
Description |
delete(): self |
Delete files in destination not in source (--delete) |
deleteBefore(): self |
Delete before transfer (--delete-before) |
deleteAfter(): self |
Delete after transfer (--delete-after) |
deleteExcluded(): self |
Delete excluded files from destination (--delete-excluded) |
| Method |
Description |
copiedCount(): int |
Number of files copied |
deletedCount(): int |
Number of files deleted |
skippedCount(): int |
Number of files skipped |
totalBytesCopied(): int |
Total bytes copied |
totalBytesDeleted(): int |
Total bytes deleted |
summary(): string |
Human-readable summary |
copiedPaths(): array |
List of copied file paths |
deletedPaths(): array |
List of deleted file paths |
skippedPaths(): array |
List of skipped file paths |
| Method |
Description |
formattedSize(): string |
Human-readable file size (e.g. "12.5 KB") |
formattedMtime(): string |
ISO 8601 formatted modification time |
Implement this to receive real-time file action callbacks during sync.
| Method |
Description |
copied(FileInfo $file): void |
Called when a file is copied |
deleted(FileInfo $file): void |
Called when a file is deleted |
skipped(FileInfo $file): void |
Called when a file is skipped |
Built-in implementation of Output that prints actions to a stream.
new TerminalOutput(?resource $stream = null)
Defaults to STDOUT. Pass a custom stream to redirect output:
$output = new TerminalOutput(fopen('php://memory', 'r+'));
This package enforces high code quality standards:
- 100% code coverage via Pest
- 100% type coverage via
pestphp/pest-plugin-type-coverage
- PHPStan at max level for static analysis
- Laravel Pint for code style
- Rector for automated refactoring
composer test # Run entire test suite
composer test:unit # Unit tests only
composer test:lint # Code style check
composer test:types # Static analysis
composer lint # Auto-fix code style
composer refactor # Auto-refactor code
Please see CHANGELOG for more information on recent changes.
Please see CONTRIBUTING for details.
The MIT License (MIT). Please see LICENSE for more information.