A Python web application for comparing and syncing directories. Identifies files present in a source directory but missing in a destination directory, and copies them over while maintaining a detailed operation log.
- Directory Comparison: Recursively scans source and destination directories to find missing files
- Selective Sync: Choose which files to copy with individual selection or select all
- Approve Mode: Review and approve files directory-by-directory before copying
- Dry Run Mode: Preview what would be copied without making any changes
- Operation Logging: All sync operations are logged to
sync_log.txtwith full paths and timestamps - Web Interface: Modern, dark-themed UI accessible via browser
# Clone or navigate to the project
cd backup-compare
# Install dependencies
pip install -r requirements.txt
# Run the application
python app.pyThen open http://127.0.0.1:5000 in your browser.
./start.sh # Start the app in background
./stop.sh # Stop the app
./restart.sh # Restart the appdocker pull andrasfe/backup-compare:latestdocker run -d --name backup-compare -p 5000:5000 andrasfe/backup-compare:latestOpen http://localhost:5000 and use the folder browser (π icons) to select directories.
Linux / macOS:
docker run -d \
--name backup-compare \
-p 5000:5000 \
-u $(id -u):$(id -g) \
-v $HOME:/home \
andrasfe/backup-compare:latestWindows (PowerShell):
docker run -d `
--name backup-compare `
-p 5000:5000 `
-v ${HOME}:/home `
andrasfe/backup-compare:latestWindows (Command Prompt):
docker run -d --name backup-compare -p 5000:5000 -v %USERPROFILE%:/home andrasfe/backup-compare:latestYour home folder is available at /home in the UI. Example paths:
/home/Documents/source-folder/home/Desktop/backup
# Stop the container
docker stop backup-compare
# Start it again
docker start backup-compare
# View logs
docker logs backup-compare
# Remove container
docker rm -f backup-compare
# Pull latest version
docker pull andrasfe/backup-compare:latestUse the helper scripts (Linux/macOS only):
./docker-start.sh # Start with home directory mounted
./docker-stop.sh # Stop the containerOr with docker-compose:
# Linux/macOS
export UID=$(id -u) GID=$(id -g)
docker-compose up -d
# Stop
docker-compose down-
Enter Paths: Input the source directory (where files exist) and destination directory (where files should be copied to)
-
Scan: Click "Scan & Compare" to analyze both directories and see what's missing
-
Review: Browse the list of missing files with their sizes. Select/deselect files individually or use "Select All"
-
Dry Run (Optional): Check the "Dry Run" checkbox to preview what would happen without copying
-
Sync: Click "Copy Selected Files" to copy the files to the destination
-
View Log: Check the operation log at the bottom of the page for a history of all operations
For more controlled syncing, enable "Approve Mode" to review files directory-by-directory:
- Check the "Approve Mode" checkbox after scanning
- Click "Start Approval Process"
- For each directory, review the files and click either:
- Approve & Continue: Include all files in this directory for copying
- Skip Directory: Exclude all files in this directory
- After reviewing all directories, confirm to copy the approved files
backup-compare/
βββ app.py # Flask application & API endpoints
βββ requirements.txt # Python dependencies
βββ pytest.ini # Test configuration
βββ sync_log.txt # Operation log (created on first sync)
βββ core/
β βββ __init__.py
β βββ scanner.py # Directory scanning logic
β βββ comparator.py # Compare source vs destination
β βββ copier.py # Copy files/directories
β βββ logger.py # Sync operation logging
βββ templates/
β βββ index.html # Web UI template
βββ static/
β βββ style.css # Styling
β βββ app.js # Frontend JavaScript
βββ tests/
βββ conftest.py # Test fixtures
βββ test_scanner.py # Scanner tests
βββ test_comparator.py # Comparator tests
βββ test_copier.py # Copier tests
βββ test_logger.py # Logger tests
βββ test_api.py # API endpoint tests
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Serve the web UI |
/api/scan |
POST | Scan and compare directories |
/api/sync |
POST | Copy missing files (supports dry run) |
/api/log |
GET | Retrieve sync history |
/api/log/clear |
POST | Clear the sync log |
Compare source and destination directories.
Request:
{
"source": "/path/to/source",
"destination": "/path/to/destination"
}Response:
{
"success": true,
"data": {
"source_path": "/path/to/source",
"dest_path": "/path/to/destination",
"missing_files_count": 3,
"missing_files": [
{
"path": "subdir/file.txt",
"full_source_path": "/path/to/source/subdir/file.txt",
"full_dest_path": "/path/to/destination/subdir/file.txt",
"size": 1024,
"size_formatted": "1.0 KB"
}
],
"total_size": 3072,
"total_size_formatted": "3.0 KB"
}
}Copy selected files from source to destination.
Request:
{
"source": "/path/to/source",
"destination": "/path/to/destination",
"files": ["file1.txt", "subdir/file2.txt"],
"dry_run": false
}Response:
{
"success": true,
"data": {
"total": 2,
"success": 2,
"failed": 0,
"copied_files": [...],
"errors": [],
"dry_run": false
}
}The sync log (sync_log.txt) uses the following format:
[2025-12-21 14:30:45] SYNC_START: /source -> /dest (3 files to copy)
[2025-12-21 14:30:45] COPIED: /source/file1.txt -> /dest/file1.txt
[2025-12-21 14:30:46] COPIED: /source/subdir/file2.txt -> /dest/subdir/file2.txt
[2025-12-21 14:30:46] SYNC_COMPLETE: 2 succeeded, 0 failed
[2025-12-21 14:35:00] DRY_RUN: /source -> /dest (5 files would be copied)
[2025-12-21 14:40:00] ERROR: Failed to copy /source/locked.txt: Permission denied
# Run all tests
python -m pytest -v
# Run with coverage
python -m pytest --cov=core --cov=app
# Run specific test file
python -m pytest tests/test_api.py -v
# Run specific test
python -m pytest tests/test_copier.py::TestSyncMissingFiles::test_dry_run_does_not_copy -v- Python 3.7+
- Flask 3.0.0
- pytest 7.4.3 (for testing)
- pytest-cov 4.1.0 (for coverage)
This application will NEVER delete or overwrite any files.
| Operation | Status |
|---|---|
| Delete files | BLOCKED |
| Overwrite existing files | BLOCKED |
| Modify existing files | BLOCKED |
| Replace existing content | BLOCKED |
| Follow symlinks | BLOCKED |
- Pre-copy verification - Checks if destination exists before any operation
- Symlink detection - Refuses to write to symlink destinations
- Race condition guard - Double-checks right before copy
- Blocked operations - Delete/overwrite functions disabled at code level
- Dry run validation - Even dry runs verify no overwrites would occur
- Path segment matching - Verifies destination path ends with correct relative path
- Path traversal protection - Blocks
../attacks, ensures files stay under dest root
- Only COPY operations to NEW locations are performed
- If a file already exists at destination β operation is blocked
- Error message:
BLOCKED: File already exists at destination (will NEVER overwrite) - The original file is preserved exactly as-is
Before copying, the application verifies:
- Destination path ends with the correct relative path segments
- Destination is actually under the specified destination root
- No path traversal (
../) is present
Example:
Source: /backup/photos/2024/vacation/img.jpg
Destination: /drive/photos/2024/vacation/img.jpg β Valid (ends with photos/2024/vacation/img.jpg)
Destination: /drive/docs/2024/vacation/img.jpg β Blocked (wrong path structure)
This is a one-way, additive-only sync tool. Files can only be added to the destination, never removed or modified.
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. THE AUTHORS AND COPYRIGHT HOLDERS ARE NOT RESPONSIBLE FOR ANY DAMAGES, DATA LOSS, OR OTHER ISSUES ARISING FROM THE USE OF THIS SOFTWARE.
Use at your own risk. Always verify that source and destination paths are correct before syncing. It is strongly recommended to use the Dry Run feature first to preview what files will be copied. The authors assume no liability for any unintended file operations, overwrites, or data loss.
MIT