Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b41fe47b | ||
|
|
c64ef3b95e |
@@ -1,24 +0,0 @@
|
|||||||
# Wasabi S3 endpoint (optional — defaults to s3.<region>.wasabisys.com)
|
|
||||||
WASABI_ENDPOINT=s3.us-east-2.wasabisys.com
|
|
||||||
|
|
||||||
# Wasabi region, e.g. us-east-2 (used to derive endpoint when WASABI_ENDPOINT is not set)
|
|
||||||
WASABI_REGION=
|
|
||||||
|
|
||||||
# Wasabi S3 bucket name (used when RESTIC_REPOSITORY is not set)
|
|
||||||
WASABI_BUCKET=
|
|
||||||
|
|
||||||
# AWS-compatible credentials for Wasabi
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
|
|
||||||
# Restic repository encryption password
|
|
||||||
RESTIC_PASSWORD=
|
|
||||||
|
|
||||||
# Full restic repository string (optional — overrides bucket/prefix/region)
|
|
||||||
RESTIC_REPOSITORY=
|
|
||||||
|
|
||||||
# Prefix (folder) inside bucket (optional)
|
|
||||||
RESTIC_PREFIX=
|
|
||||||
|
|
||||||
# Path to JSON config file containing {"paths": [...]} (optional, backup subcommand only)
|
|
||||||
FILE_PATH_CONFIG_PATH=
|
|
||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
.env.local
|
.env.*
|
||||||
.config/
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Repo Overview
|
|
||||||
|
|
||||||
- Purpose: run `restic backup` against a Wasabi S3-compatible repository.
|
|
||||||
- Main entry point: `travel-backup-script.py`.
|
|
||||||
- Dependencies are intentionally minimal:
|
|
||||||
- Python standard library
|
|
||||||
- `python-dotenv`
|
|
||||||
- External binary: `restic` must already be installed and available on `PATH`
|
|
||||||
- There are currently no tests, package metadata files, or CI config in this repo.
|
|
||||||
|
|
||||||
## File Map
|
|
||||||
|
|
||||||
- `travel-backup-script.py`: single-script CLI implementation.
|
|
||||||
- `requirements.txt`: Python dependency list (`python-dotenv` only).
|
|
||||||
- `README.md`: user-facing setup and usage docs.
|
|
||||||
- `.env.example`: template for environment variables.
|
|
||||||
- `.gitignore`: ignores `.env.*` files (except `.env.example`).
|
|
||||||
|
|
||||||
## Current Script Behavior
|
|
||||||
|
|
||||||
The script uses two subcommands: `init` and `backup`.
|
|
||||||
|
|
||||||
1. Loads `.env.local` automatically via `load_dotenv(".env.local")`.
|
|
||||||
2. Parses CLI args into subcommands with shared repo flags and backup-specific source flags.
|
|
||||||
3. Resolves the repository string:
|
|
||||||
- `--repository` / `RESTIC_REPOSITORY` takes priority (validated against `s3:s3.<region>.wasabisys.com/...` format).
|
|
||||||
- Otherwise built from `--bucket` / `WASABI_BUCKET`, `--region` / `WASABI_REGION`, `WASABI_ENDPOINT`, and `--prefix` / `RESTIC_PREFIX`.
|
|
||||||
4. Constructs a minimal subprocess environment for `restic`:
|
|
||||||
- `WASABI_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `RESTIC_PASSWORD`
|
|
||||||
- `PATH`, `HOME` if present
|
|
||||||
- `SYSTEMROOT`, `SystemRoot`, `COMSPEC` (Windows-specific, required for DNS resolution and shell access)
|
|
||||||
5. For `init`: runs `restic init` against the resolved repository.
|
|
||||||
6. For `backup`: resolves input from `--source`, `--file`, or `FILE_PATH_CONFIG_PATH` env var, then runs `restic backup`.
|
|
||||||
7. Prints the repo, command, and redacted env status before optionally executing `restic`.
|
|
||||||
|
|
||||||
## Known Drift / Things To Watch
|
|
||||||
|
|
||||||
- The JSON config file must contain a top-level `paths` key with a non-empty array of non-empty strings.
|
|
||||||
- `--source` and `--file` are mutually exclusive; one of them or `FILE_PATH_CONFIG_PATH` is required for backup.
|
|
||||||
- `--region` has no default — it must be provided via CLI or `WASABI_REGION` when not using `--repository`.
|
|
||||||
|
|
||||||
## Local Development Workflow
|
|
||||||
|
|
||||||
Use this order when changing the repo:
|
|
||||||
|
|
||||||
1. Read `travel-backup-script.py` first. Nearly all behavior lives there.
|
|
||||||
2. Treat `README.md` as partially authoritative only after comparing it to the script.
|
|
||||||
3. If you change CLI flags, env loading, or repository construction, update code, README, and `.env.example` in the same change.
|
|
||||||
4. Preserve secret-handling behavior. Do not print raw credentials.
|
|
||||||
|
|
||||||
## Suggested Verification
|
|
||||||
|
|
||||||
Because the repo has no automated tests, prefer lightweight verification:
|
|
||||||
|
|
||||||
- Static review of argument parsing and env handling.
|
|
||||||
- Dry-run init:
|
|
||||||
- `python travel-backup-script.py init --bucket <bucket> --region <region> --dry-run`
|
|
||||||
- Dry-run backup with a direct source:
|
|
||||||
- `python travel-backup-script.py backup --source <path> --bucket <bucket> --region <region> --dry-run`
|
|
||||||
- Dry-run backup with a JSON config file:
|
|
||||||
- `python travel-backup-script.py backup --file <config.json> --bucket <bucket> --region <region> --dry-run`
|
|
||||||
|
|
||||||
Avoid real backup runs unless the user explicitly asks for them and valid credentials are present.
|
|
||||||
|
|
||||||
## Editing Guidelines
|
|
||||||
|
|
||||||
- Keep the implementation as a simple single-file CLI unless the user asks for a larger refactor.
|
|
||||||
- Favor backwards-compatible flag changes where practical.
|
|
||||||
- Be careful with subprocess environment changes:
|
|
||||||
- `PATH` must remain available so `restic` can be found.
|
|
||||||
- `HOME` is intentionally forwarded when present.
|
|
||||||
- On Windows, `SYSTEMROOT` and `COMSPEC` are required for DNS resolution and shell access.
|
|
||||||
- Do not commit real `.env.local` or other secret files.
|
|
||||||
- If adding tests later, prefer small CLI/unit tests around:
|
|
||||||
- `build_repo`
|
|
||||||
- `load_sources`
|
|
||||||
- `resolve_repo` (priority order, validation)
|
|
||||||
- missing `--bucket` handling
|
|
||||||
- env minimization/redaction behavior
|
|
||||||
|
|
||||||
## Agent Notes
|
|
||||||
|
|
||||||
- Start discovery from the script, not the README.
|
|
||||||
- Keep `README.md`, `.env.example`, and the script aligned when flags change.
|
|
||||||
@@ -1,127 +1,61 @@
|
|||||||
# Wasabi Restic Backup Script
|
# Wasabi Restic Backup Script
|
||||||
|
|
||||||
This script automates backups to a [Wasabi](https://wasabi.com/) S3-compatible bucket using [restic](https://restic.net/).
|
This script automates backups to a [Wasabi](https://wasabi.com/) (S3-compatible) bucket using [restic](https://restic.net/).
|
||||||
It loads secrets from `.env.local`, builds a restic repository string, and runs `restic backup` with redacted environment logging.
|
It supports environment-based secrets (via `.env` + [python-dotenv](https://pypi.org/project/python-dotenv/)), CLI overrides, and dry-run/verbose modes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Two subcommands: `init` (initialize a new repository) and `backup` (run a backup).
|
- Backup any file or directory to Wasabi S3 storage with restic.
|
||||||
- Backup a single source path with `-s/--source`.
|
- Secrets loaded from a `.env` file (no need to type passwords on the CLI).
|
||||||
- Backup multiple source paths from a JSON config file with `-f/--file`.
|
- CLI arguments override `.env` and system environment variables.
|
||||||
- Load secrets from `.env.local` via [python-dotenv](https://pypi.org/project/python-dotenv/).
|
- Verbose and dry-run modes for debugging.
|
||||||
- Support a prebuilt restic repository via `--repository` or `RESTIC_REPOSITORY`.
|
- Environment variable redaction in output (so logs won’t leak secrets).
|
||||||
- Redact sensitive environment values in output.
|
|
||||||
- Support dry-run mode for command verification.
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.8+
|
- Python 3.8+
|
||||||
- [restic](https://restic.net/) installed and available on `PATH`
|
- [restic](https://restic.net/) installed and in your `PATH`
|
||||||
- Install dependencies:
|
- `python-dotenv` installed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install python-dotenv
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
The script loads `.env.local` automatically and uses these variables:
|
## Initialize the Repository (first run only)
|
||||||
|
|
||||||
| Variable | Required | Description |
|
restic -r s3:s3.[REGION].wasabisys.com/[BUCKET_NAME]/[PREFIX] init
|
||||||
|---|---|---|
|
|
||||||
| `AWS_ACCESS_KEY_ID` | Yes | Wasabi access key |
|
|
||||||
| `AWS_SECRET_ACCESS_KEY` | Yes | Wasabi secret key |
|
|
||||||
| `RESTIC_PASSWORD` | Yes | Restic repository encryption password |
|
|
||||||
| `WASABI_ENDPOINT` | No | S3 endpoint (defaults to `s3.<region>.wasabisys.com`) |
|
|
||||||
| `WASABI_REGION` | No | Wasabi region, e.g. `us-east-2` (used to derive endpoint) |
|
|
||||||
| `WASABI_BUCKET` | No | Bucket name (used when `RESTIC_REPOSITORY` is not set) |
|
|
||||||
| `RESTIC_REPOSITORY` | No | Full restic repository string (overrides bucket/prefix/region) |
|
|
||||||
| `RESTIC_PREFIX` | No | Prefix (folder) inside bucket |
|
|
||||||
| `FILE_PATH_CONFIG_PATH` | No | Path to JSON config file (backup subcommand only) |
|
|
||||||
|
|
||||||
Only `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `RESTIC_PASSWORD`, and `WASABI_ENDPOINT` are passed to the restic subprocess. The script builds a minimal environment to avoid leaking unrelated variables.
|
|
||||||
|
|
||||||
Create a `RESTIC_PASSWORD` via `openssl rand -base64 32` for best security.
|
|
||||||
|
|
||||||
## Subcommands
|
|
||||||
|
|
||||||
### `init` — Initialize a New Repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python travel-backup-script.py init --bucket my-bucket --region us-east-2 --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
### `backup` — Run a Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python travel-backup-script.py backup --source /etc --bucket my-bucket --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Repository Resolution
|
|
||||||
|
|
||||||
The repository string is resolved in this order:
|
|
||||||
|
|
||||||
1. `--repository` CLI flag or `RESTIC_REPOSITORY` env var (must match `s3:s3.<region>.wasabisys.com/<bucket>[/<prefix>]`).
|
|
||||||
2. Otherwise, built from `--bucket` / `WASABI_BUCKET`, `--region` / `WASABI_REGION`, `WASABI_ENDPOINT`, and `--prefix` / `RESTIC_PREFIX`.
|
|
||||||
|
|
||||||
## CLI Options
|
## CLI Options
|
||||||
|
|
||||||
Shared options (both `init` and `backup`):
|
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
|
||||||
|
|
||||||
- `--bucket`: Wasabi bucket name (or set `WASABI_BUCKET`)
|
## Example Dry Run
|
||||||
- `--prefix`: Prefix inside the bucket (or set `RESTIC_PREFIX`)
|
python travel-backup-backup.py --source /etc --bucket my-bucket --dry-run --verbose
|
||||||
- `--region`: Wasabi region, e.g. `us-east-2` (or set `WASABI_REGION`)
|
|
||||||
- `--repository`: Full restic repository string (or set `RESTIC_REPOSITORY`)
|
|
||||||
- `--dry-run`: Print the command and redacted environment without running `restic`
|
|
||||||
|
|
||||||
Backup-only options:
|
|
||||||
|
|
||||||
- `-s`, `--source`: Single file or directory to back up
|
## Run the Backup
|
||||||
- `-f`, `--file`: Path to a JSON file containing `{"paths": ["..."]}`
|
|
||||||
|
|
||||||
`-s/--source` and `-f/--file` are mutually exclusive. One of them (or the `FILE_PATH_CONFIG_PATH` env var) is required for backup.
|
python backup.py --source /path/to/data --bucket my-bucket
|
||||||
|
|
||||||
## JSON File Format
|
**Example with overrides:**
|
||||||
|
python backup.py \
|
||||||
|
--source ~/Documents \
|
||||||
|
--bucket my-bucket \
|
||||||
|
--prefix laptop-backups \
|
||||||
|
--verbose
|
||||||
|
|
||||||
Example config:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"paths": [
|
|
||||||
"/path/to/Documents",
|
|
||||||
"/path/to/Pictures"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Initialize a repository (dry run):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python travel-backup-script.py init --bucket my-bucket --region us-east-2 --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
Backup a single source (dry run):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python travel-backup-script.py backup --source /etc --bucket my-bucket --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
Backup from a JSON file (dry run):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python travel-backup-script.py backup --file backup-paths.json --bucket my-bucket --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a custom prefix:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python travel-backup-script.py backup --source ~/Documents --bucket my-bucket --prefix laptop-backups
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a full repository string:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python travel-backup-script.py backup --source ~/Documents --repository s3:s3.us-east-2.wasabisys.com/my-bucket/laptop-backups
|
|
||||||
```
|
|
||||||
|
|||||||
Executable → Regular
+23
-155
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -19,181 +17,51 @@ def build_repo(endpoint: str, bucket: str, prefix: str = None) -> str:
|
|||||||
parts.append(prefix.strip("/"))
|
parts.append(prefix.strip("/"))
|
||||||
return "/".join(parts)
|
return "/".join(parts)
|
||||||
|
|
||||||
def load_sources(source: str = None, source_file: str = None) -> list[str]:
|
def run_restic(repo: str, source: str, dry_run: bool = False):
|
||||||
"""Resolve backup sources from --source or a JSON file."""
|
|
||||||
if source is not None:
|
|
||||||
return [source]
|
|
||||||
|
|
||||||
with open(source_file, "r", encoding="utf-8") as fh:
|
|
||||||
config = json.load(fh)
|
|
||||||
|
|
||||||
paths = config.get("paths")
|
|
||||||
if not isinstance(paths, list) or not paths:
|
|
||||||
raise ValueError('config file must contain a non-empty "paths" array')
|
|
||||||
if not all(isinstance(path, str) and path.strip() for path in paths):
|
|
||||||
raise ValueError('each entry in "paths" must be a non-empty string')
|
|
||||||
|
|
||||||
return paths
|
|
||||||
|
|
||||||
def print_env_status(env: dict):
|
|
||||||
"""Print redacted environment variable status."""
|
|
||||||
env_vars = ("WASABI_ENDPOINT", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "RESTIC_PASSWORD")
|
|
||||||
for key in env_vars:
|
|
||||||
if key in env and env.get(key) is not None:
|
|
||||||
print(f" {key}=***REDACTED***")
|
|
||||||
else:
|
|
||||||
print(f" {key}=NOT SET")
|
|
||||||
|
|
||||||
def run_restic(repo: str, sources: list[str], dry_run: bool = False, env: dict = None, source_file: str = None):
|
|
||||||
"""Run restic backup command."""
|
"""Run restic backup command."""
|
||||||
cmd = [
|
cmd = [
|
||||||
"restic",
|
"restic",
|
||||||
"-r", repo,
|
"-r", repo,
|
||||||
"backup",
|
"backup",
|
||||||
*sources,
|
source,
|
||||||
]
|
]
|
||||||
|
|
||||||
print("Repository:", repo)
|
print("Repository:", repo)
|
||||||
print("Command:", " ".join(cmd))
|
print("Command:", " ".join(cmd))
|
||||||
print("Environment (redacted):")
|
print("Environment (redacted):")
|
||||||
|
for key in ("WASABI_ENDPOINT", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "RESTIC_PASSWORD"):
|
||||||
# Use provided env or fall back to current process env
|
if key in os.environ:
|
||||||
if env is None:
|
print(f" {key}=***REDACTED***")
|
||||||
env = os.environ.copy()
|
|
||||||
|
|
||||||
print_env_status(env)
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
if source_file:
|
|
||||||
print("Config file:", source_file)
|
|
||||||
print("Dry-run mode: not running restic.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Pass the provided environment explicitly to the restic subprocess
|
|
||||||
subprocess.check_call(cmd, env=env)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def run_restic_init(repo: str, dry_run: bool = False, env: dict = None):
|
|
||||||
"""Run restic init command to initialize a new repository."""
|
|
||||||
cmd = [
|
|
||||||
"restic",
|
|
||||||
"-r", repo,
|
|
||||||
"init",
|
|
||||||
]
|
|
||||||
|
|
||||||
print("Initializing repository:", repo)
|
|
||||||
print("Command:", " ".join(cmd))
|
|
||||||
print("Environment (redacted):")
|
|
||||||
|
|
||||||
if env is None:
|
|
||||||
env = os.environ.copy()
|
|
||||||
|
|
||||||
print_env_status(env)
|
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print("Dry-run mode: not running restic.")
|
print("Dry-run mode: not running restic.")
|
||||||
return 0
|
return 0
|
||||||
|
return subprocess.call(cmd)
|
||||||
subprocess.check_call(cmd, env=env)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def resolve_repo(args) -> str:
|
|
||||||
"""Resolve the restic repository string from args and environment."""
|
|
||||||
# CLI --repository takes priority, then RESTIC_REPOSITORY env var
|
|
||||||
repository = getattr(args, "repository", None) or os.getenv("RESTIC_REPOSITORY")
|
|
||||||
if repository:
|
|
||||||
if not re.match(r"^s3:s3\.[a-z0-9-]+\.wasabisys\.com/.+", repository):
|
|
||||||
print(f"error: repository '{repository}' does not match expected format: s3:s3.<region>.wasabisys.com/<bucket>[/<prefix>]")
|
|
||||||
return None
|
|
||||||
return repository
|
|
||||||
|
|
||||||
bucket = getattr(args, "bucket", None) or os.getenv("WASABI_BUCKET")
|
|
||||||
if not bucket:
|
|
||||||
print("error: --bucket or WASABI_BUCKET is required when --repository / RESTIC_REPOSITORY is not provided")
|
|
||||||
return None
|
|
||||||
|
|
||||||
region = getattr(args, "region", None) or os.getenv("WASABI_REGION")
|
|
||||||
if not region:
|
|
||||||
print("error: --region or WASABI_REGION is required when --repository / RESTIC_REPOSITORY is not provided")
|
|
||||||
return None
|
|
||||||
|
|
||||||
endpoint = os.getenv("WASABI_ENDPOINT", f"s3.{region}.wasabisys.com")
|
|
||||||
# CLI --prefix takes priority, then RESTIC_PREFIX env var
|
|
||||||
prefix = getattr(args, "prefix", None) or os.getenv("RESTIC_PREFIX")
|
|
||||||
return build_repo(endpoint, bucket, prefix)
|
|
||||||
|
|
||||||
def build_minimal_env() -> dict:
|
|
||||||
"""Build a minimal env dict to pass to restic subprocesses."""
|
|
||||||
proc_env = os.environ.copy()
|
|
||||||
|
|
||||||
needed_keys = ("WASABI_ENDPOINT", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "RESTIC_PASSWORD")
|
|
||||||
minimal_env = {}
|
|
||||||
for k in needed_keys:
|
|
||||||
v = proc_env.get(k)
|
|
||||||
if v is not None:
|
|
||||||
minimal_env[k] = v
|
|
||||||
|
|
||||||
# Preserve system env vars needed by subprocesses
|
|
||||||
# PATH/HOME: find restic and resolve home directory
|
|
||||||
# SYSTEMROOT/COMSPEC: required on Windows for DNS resolution and shell access
|
|
||||||
system_keys = ("PATH", "HOME", "SYSTEMROOT", "SystemRoot", "COMSPEC")
|
|
||||||
for k in system_keys:
|
|
||||||
v = proc_env.get(k)
|
|
||||||
if v is not None:
|
|
||||||
minimal_env[k] = v
|
|
||||||
|
|
||||||
return minimal_env
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Load environment variables from .env.local (if present)
|
# Load environment variables from .env.local (if present)
|
||||||
load_dotenv(".env.local")
|
load_dotenv(".env.local")
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Backup files to Wasabi S3 with restic")
|
parser = argparse.ArgumentParser(description="Backup files to Wasabi S3 with restic")
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
parser.add_argument("--source", required=True, help="Path to file or directory to back up")
|
||||||
|
parser.add_argument("--bucket", help="Wasabi S3 bucket name")
|
||||||
# Shared repo arguments
|
parser.add_argument("--prefix", help="Prefix (folder) inside bucket", default=None)
|
||||||
repo_args = argparse.ArgumentParser(add_help=False)
|
parser.add_argument("--region", default="us-east-1", help="Wasabi region, e.g. us-west-1")
|
||||||
repo_args.add_argument("--bucket", help="Wasabi S3 bucket name (or set WASABI_BUCKET)")
|
parser.add_argument("--repository", help="Full restic repository string (overrides bucket/prefix)")
|
||||||
repo_args.add_argument("--prefix", help="Prefix (folder) inside bucket (or set RESTIC_PREFIX)", default=None)
|
parser.add_argument("--dry-run", action="store_true", help="Print command instead of running it")
|
||||||
repo_args.add_argument("--region", default=None, help="Wasabi region, e.g. us-west-1 (or set WASABI_REGION)")
|
|
||||||
repo_args.add_argument("--repository", help="Full restic repository string (or set RESTIC_REPOSITORY)")
|
|
||||||
repo_args.add_argument("--dry-run", action="store_true", help="Print command instead of running it")
|
|
||||||
|
|
||||||
# init subcommand
|
|
||||||
subparsers.add_parser("init", parents=[repo_args], help="Initialize a new restic repository")
|
|
||||||
|
|
||||||
# backup subcommand
|
|
||||||
backup_parser = subparsers.add_parser("backup", parents=[repo_args], help="Run a backup")
|
|
||||||
source_group = backup_parser.add_mutually_exclusive_group(required=False)
|
|
||||||
source_group.add_argument("--source", "-s", help="Path to file or directory to back up")
|
|
||||||
source_group.add_argument("--file", "-f", help='Path to a JSON config file containing {"paths": [...]} (or set FILE_PATH_CONFIG_PATH)')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo = resolve_repo(args)
|
# Endpoint based on region (unless already set in env)
|
||||||
if repo is None:
|
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
|
return 2
|
||||||
|
repo = build_repo(endpoint, args.bucket, args.prefix)
|
||||||
|
|
||||||
minimal_env = build_minimal_env()
|
return run_restic(repo, args.source, args.dry_run)
|
||||||
|
|
||||||
try:
|
|
||||||
if args.command == "init":
|
|
||||||
return run_restic_init(repo, args.dry_run, env=minimal_env)
|
|
||||||
|
|
||||||
# backup command — resolve file path from CLI or env var
|
|
||||||
source_file = args.file or os.getenv("FILE_PATH_CONFIG_PATH")
|
|
||||||
if not args.source and not source_file:
|
|
||||||
print("error: --source, --file, or FILE_PATH_CONFIG_PATH is required for backup")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
sources = load_sources(args.source, source_file)
|
|
||||||
return run_restic(repo, sources, args.dry_run, env=minimal_env, source_file=source_file)
|
|
||||||
except subprocess.CalledProcessError as exc:
|
|
||||||
print(f"error: restic exited with code {exc.returncode}: {exc}", file=sys.stderr)
|
|
||||||
return exc.returncode
|
|
||||||
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
||||||
print(f"error: {exc}")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user