Skip to content

andrasfe/backup-compare

Repository files navigation

Backup Compare

CI Docker

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.

Features

  • 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.txt with full paths and timestamps
  • Web Interface: Modern, dark-themed UI accessible via browser

Installation

# Clone or navigate to the project
cd backup-compare

# Install dependencies
pip install -r requirements.txt

# Run the application
python app.py

Then open http://127.0.0.1:5000 in your browser.

Using Scripts

./start.sh    # Start the app in background
./stop.sh     # Stop the app
./restart.sh  # Restart the app

Using Docker (from Docker Hub)

docker pull andrasfe/backup-compare:latest

Quick Start (all platforms)

docker run -d --name backup-compare -p 5000:5000 andrasfe/backup-compare:latest

Open http://localhost:5000 and use the folder browser (πŸ“ icons) to select directories.

With Home Directory Access

Linux / macOS:

docker run -d \
  --name backup-compare \
  -p 5000:5000 \
  -u $(id -u):$(id -g) \
  -v $HOME:/home \
  andrasfe/backup-compare:latest

Windows (PowerShell):

docker run -d `
  --name backup-compare `
  -p 5000:5000 `
  -v ${HOME}:/home `
  andrasfe/backup-compare:latest

Windows (Command Prompt):

docker run -d --name backup-compare -p 5000:5000 -v %USERPROFILE%:/home andrasfe/backup-compare:latest

Your home folder is available at /home in the UI. Example paths:

  • /home/Documents/source-folder
  • /home/Desktop/backup

Useful Commands

# 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:latest

Using Docker (build locally)

Use the helper scripts (Linux/macOS only):

./docker-start.sh                    # Start with home directory mounted
./docker-stop.sh                     # Stop the container

Or with docker-compose:

# Linux/macOS
export UID=$(id -u) GID=$(id -g)
docker-compose up -d

# Stop
docker-compose down

Usage

  1. Enter Paths: Input the source directory (where files exist) and destination directory (where files should be copied to)

  2. Scan: Click "Scan & Compare" to analyze both directories and see what's missing

  3. Review: Browse the list of missing files with their sizes. Select/deselect files individually or use "Select All"

  4. Dry Run (Optional): Check the "Dry Run" checkbox to preview what would happen without copying

  5. Sync: Click "Copy Selected Files" to copy the files to the destination

  6. View Log: Check the operation log at the bottom of the page for a history of all operations

Approve Mode

For more controlled syncing, enable "Approve Mode" to review files directory-by-directory:

  1. Check the "Approve Mode" checkbox after scanning
  2. Click "Start Approval Process"
  3. 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
  4. After reviewing all directories, confirm to copy the approved files

Project Structure

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

API Endpoints

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

POST /api/scan

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"
  }
}

POST /api/sync

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
  }
}

Log Format

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

Running Tests

# 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

Requirements

  • Python 3.7+
  • Flask 3.0.0
  • pytest 7.4.3 (for testing)
  • pytest-cov 4.1.0 (for coverage)

Safety Guarantee

This application will NEVER delete or overwrite any files.

Protected Operations

Operation Status
Delete files BLOCKED
Overwrite existing files BLOCKED
Modify existing files BLOCKED
Replace existing content BLOCKED
Follow symlinks BLOCKED

Safety Layers

  1. Pre-copy verification - Checks if destination exists before any operation
  2. Symlink detection - Refuses to write to symlink destinations
  3. Race condition guard - Double-checks right before copy
  4. Blocked operations - Delete/overwrite functions disabled at code level
  5. Dry run validation - Even dry runs verify no overwrites would occur
  6. Path segment matching - Verifies destination path ends with correct relative path
  7. Path traversal protection - Blocks ../ attacks, ensures files stay under dest root

Behavior

  • 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

Path Validation

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.

Disclaimer

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.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published