diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbd0003 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env.* \ No newline at end of file diff --git a/README.md b/README.md index 7881249..66cb487 100644 --- a/README.md +++ b/README.md @@ -1 +1,61 @@ -# Travel Backup Script +# Wasabi Restic Backup Script + +This script automates backups to a [Wasabi](https://wasabi.com/) (S3-compatible) bucket using [restic](https://restic.net/). +It supports environment-based secrets (via `.env` + [python-dotenv](https://pypi.org/project/python-dotenv/)), CLI overrides, and dry-run/verbose modes. + +--- + +## Features + +- Backup any file or directory to Wasabi S3 storage with restic. +- Secrets loaded from a `.env` file (no need to type passwords on the CLI). +- CLI arguments override `.env` and system environment variables. +- Verbose and dry-run modes for debugging. +- Environment variable redaction in output (so logs won’t leak secrets). + +--- + +## Requirements + +- Python 3.8+ +- [restic](https://restic.net/) installed and in your `PATH` +- `python-dotenv` installed: + ```bash + pip install python-dotenv + + +## Initialize the Repository (first run only) + +restic -r s3:s3.[REGION].wasabisys.com/[BUCKET_NAME]/[PREFIX] init + + +## CLI Options + +Option Description +--source, -s Source path to back up (required) +--bucket, -b Wasabi bucket name (required unless --repository used) +--endpoint, -e S3 endpoint (default: s3.wasabisys.com or from env) +--prefix, -p Path inside bucket (default: travel-backup) +--access-key Wasabi access key (overrides env/.env) +--secret-key Wasabi secret key (overrides env/.env) +--password, -P Restic password (overrides env/.env) +--repository, -r Full restic repository string (overrides bucket/endpoint/prefix) +--env-file Path to .env file (default: .env) +--dry-run Show command and env but do not execute +--verbose Show extra debug info + +## Example Dry Run +python travel-backup-backup.py --source /etc --bucket my-bucket --dry-run --verbose + + +## Run the Backup + +python backup.py --source /path/to/data --bucket my-bucket + +**Example with overrides:** +python backup.py \ + --source ~/Documents \ + --bucket my-bucket \ + --prefix laptop-backups \ + --verbose + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3e338bf --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python-dotenv \ No newline at end of file diff --git a/travel-backup-script.py b/travel-backup-script.py new file mode 100644 index 0000000..68db53b --- /dev/null +++ b/travel-backup-script.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import argparse +import os +import subprocess +import sys +from dotenv import load_dotenv + +def build_repo(endpoint: str, bucket: str, prefix: str = None) -> str: + """ + Build a restic S3 repository URL in the correct format: + s3:ENDPOINT/BUCKET[/PREFIX] + """ + parts = [f"s3:{endpoint.rstrip('/')}"] + if bucket: + parts.append(bucket.strip("/")) + if prefix: + parts.append(prefix.strip("/")) + return "/".join(parts) + +def run_restic(repo: str, source: str, dry_run: bool = False): + """Run restic backup command.""" + cmd = [ + "restic", + "-r", repo, + "backup", + source, + ] + print("Repository:", repo) + print("Command:", " ".join(cmd)) + print("Environment (redacted):") + for key in ("WASABI_ENDPOINT", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "RESTIC_PASSWORD"): + if key in os.environ: + print(f" {key}=***REDACTED***") + if dry_run: + print("Dry-run mode: not running restic.") + return 0 + return subprocess.call(cmd) + +def main(): + # Load environment variables from .env.local (if present) + load_dotenv(".env.local") + + parser = argparse.ArgumentParser(description="Backup files to Wasabi S3 with restic") + parser.add_argument("--source", required=True, help="Path to file or directory to back up") + parser.add_argument("--bucket", help="Wasabi S3 bucket name") + parser.add_argument("--prefix", help="Prefix (folder) inside bucket", default=None) + parser.add_argument("--region", default="us-east-1", help="Wasabi region, e.g. us-west-1") + parser.add_argument("--repository", help="Full restic repository string (overrides bucket/prefix)") + parser.add_argument("--dry-run", action="store_true", help="Print command instead of running it") + args = parser.parse_args() + + # Endpoint based on region (unless already set in env) + endpoint = os.getenv("WASABI_ENDPOINT", f"s3.{args.region}.wasabisys.com") + + # Build repo string + if args.repository: + repo = args.repository + else: + if not args.bucket: + print("error: --bucket is required when --repository is not provided") + return 2 + repo = build_repo(endpoint, args.bucket, args.prefix) + + return run_restic(repo, args.source, args.dry_run) + +if __name__ == "__main__": + sys.exit(main())