https://github.com/cdevroe/bulldozer
A simple PHP script to move images from one directory into /yyyy/mm/dd directories based on date created.
https://github.com/cdevroe/bulldozer
Last synced: 28 days ago
JSON representation
A simple PHP script to move images from one directory into /yyyy/mm/dd directories based on date created.
- Host: GitHub
- URL: https://github.com/cdevroe/bulldozer
- Owner: cdevroe
- License: gpl-3.0
- Created: 2021-01-26T02:02:52.000Z (over 5 years ago)
- Default Branch: main
- Last Pushed: 2026-05-05T15:18:11.000Z (about 2 months ago)
- Last Synced: 2026-05-05T17:22:52.716Z (about 2 months ago)
- Language: PHP
- Size: 36.1 KB
- Stars: 4
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# Bulldozer
Bulldozer is a small PHP CLI script that organizes images into `yyyy/mm/dd` directories based on the image date.
It targets **PHP 8.2 or newer** and has no Composer dependencies.
By [Colin Devroe](http://cdevroe.com/projects/bulldozer)
## Setup
Edit the configuration block at the top of `bulldozer.php`:
```php
$config = [
'source' => '/path/to/images_to_organize',
'locations' => [
'default' => '/path/to/image_library',
'backup' => '/Volumes/BackupDrive/image_library',
],
];
```
The source and destination directories must already exist. Bulldozer creates the dated `yyyy/mm/dd` directories inside the destination as needed.
## Config File
You can also put configuration in JSON and load it with `--config`:
```json
{
"source": "/path/to/images_to_organize",
"destination": "/path/to/image_library",
"date": "mtime",
"timezone": "America/New_York",
"duplicates": "rename"
}
```
Run with:
```sh
php bulldozer.php --config=/path/to/bulldozer.config.json --json
```
CLI flags override config-file values. For safety, config files cannot enable
`--run`; write operations still require the explicit `--run` CLI flag.
## Usage
Always start with a dry run:
```sh
php bulldozer.php
```
Actually copy files:
```sh
php bulldozer.php --run
```
Move files instead of copying them:
```sh
php bulldozer.php --mode=move --run
```
Use a configured destination:
```sh
php bulldozer.php --location=backup --run
```
Override the configured source and destination:
```sh
php bulldozer.php --source=/path/to/source --destination=/path/to/library --run
```
Legacy usage still works:
```sh
php bulldozer.php backup true
```
## Options
```text
--config=PATH Load a JSON config file. CLI flags override config values.
--source=PATH Source directory. Defaults to the configured source.
--destination=PATH Destination directory. Overrides --location.
--location=NAME Configured destination name. Default: default.
--run Actually copy or move files. Default is dry run.
--dry-run Force dry run.
--mode=copy|move Copy or move files. Default: copy.
--date=exif|mtime Date source. Default: exif with mtime fallback.
--duplicates=rename|skip Rename or skip filename collisions. Default: rename.
--timezone=TIMEZONE Timezone for EXIF and mtime dates.
--all-files Process every file except ignored system files.
--json Print machine-readable JSON instead of text summary.
--manifest=PATH Write a JSON manifest of planned or completed actions.
--quiet Suppress verbose per-file output.
--verbose Print per-file progress. Ignored when --json is used.
--log[=PATH] Write a log file.
--hooks[=PATH] Load a PHP hooks file.
--help Show help.
```
## Agent Usage
Agents should dry-run first and use JSON plus a manifest:
```sh
php bulldozer.php \
--source=/path/to/source \
--destination=/path/to/library \
--date=mtime \
--timezone=America/New_York \
--json \
--manifest=/tmp/bulldozer-manifest.json
```
Then inspect the manifest before running:
```sh
php bulldozer.php \
--source=/path/to/source \
--destination=/path/to/library \
--date=mtime \
--timezone=America/New_York \
--json \
--manifest=/tmp/bulldozer-manifest.json \
--run
```
Agent-specific guidance lives in `AGENTS.md`. More detailed references are in
`docs/cli-reference.md`, `docs/manifest-schema.md`, `docs/hooks.md`, and
`docs/agent-examples.md`.
## Exit Codes
```text
0 Success.
1 Usage, configuration, or validation error.
2 Run completed with file operation errors.
3 Run completed with hook, log, or manifest reporting errors.
```
## Dates
By default, Bulldozer tries EXIF image dates first:
1. `DateTimeOriginal`
2. `DateTimeDigitized`
3. `DateTime`
If no usable EXIF date is found, it falls back to filesystem modified time.
Use `--date=mtime` to skip EXIF and use modified time for every file.
Use `--timezone=America/New_York` if you want to force the timezone used when building date folders. Without this option, Bulldozer uses PHP's configured default timezone.
## Duplicates
The default duplicate behavior is `rename`:
```text
IMG_0001.jpg
IMG_0001-1.jpg
IMG_0001-2.jpg
```
Use `--duplicates=skip` to leave colliding files untouched.
When duplicates are skipped, Bulldozer records whether the files appear to be the same by comparing file size first, then SHA-256 hashes only when sizes match.
## Logging
Use `--log` to write a basic operation log:
```sh
php bulldozer.php --log --run
```
That writes to `bulldozer.log` next to the script. You can also choose a path:
```sh
php bulldozer.php --log=/tmp/bulldozer.log --run
```
The log includes run configuration, skipped files, directory creation, copy/move operations, duplicate handling, hook errors, and final stats.
## Hooks
A full plugin system would add too much complexity for a small CLI script. The script instead supports a deliberately small event hook file for developers who need to run extra code as files are planned, copied, moved, skipped, or when a run starts or finishes.
Create a hook file:
```php
on('file.copied', static function (array $event): void {
echo "Copied {$event['source']} to {$event['destination']}" . PHP_EOL;
});
};
```
Run with hooks:
```sh
php bulldozer.php --hooks=/path/to/bulldozer-hooks.php --run
```
Available events:
```text
run.start
directory.planned
directory.created
directory.error
file.planned
file.copied
file.moved
file.duplicate
file.skipped
file.error
run.finish
```
Hook failures are logged and counted as errors, but they do not stop the run.
## Notes
- The script is intended for images: JPG, PNG, HEIC, TIFF.
- EXIF reading is available for formats supported by PHP's `exif` extension,
typically JPEG and TIFF. HEIC files usually fall back to modified time.
- Use `--all-files` if you want Bulldozer to process non-image files too.
- Move mode uses `rename()` first, then falls back to copy-and-delete when moving across filesystems.
## Version History
- **2026.05** - May 5, 2026
- Target PHP 8.2+.
- Added dry-run by default, explicit `--run`, copy/move modes, safer
duplicate handling, EXIF date support, streaming traversal, logging, and
optional event hooks.
- Added agent-ready JSON output, manifest output, stable exit codes, CLI
reference docs, and a smoke test.
- **2021.01** - January 25, 2021
- Initial public release.