Add CLI interface and pyproject.toml for PyPI packaging

This commit is contained in:
KakiFilem Team 2026-02-02 19:33:18 +08:00
parent 3bbab71301
commit 2f7d5a7949
5 changed files with 394 additions and 185 deletions

BIN
__init__.py Normal file

Binary file not shown.

166
cli.py Normal file
View File

@ -0,0 +1,166 @@
import argparse
import shutil
import os
import sys
import textwrap
import importlib.metadata
from main import run_backup
def get_version():
try:
return importlib.metadata.version("pg-r2-backup")
except importlib.metadata.PackageNotFoundError:
return "dev"
def mask(value, show=4):
if not value:
return ""
if len(value) <= show:
return "*" * len(value)
return value[:show] + "*" * (len(value) - show)
def doctor():
print("pg-r2-backup doctor\n")
if shutil.which("pg_dump") is None:
print("[FAIL] pg_dump not found in PATH")
else:
print("[OK] pg_dump found")
required_envs = [
"DATABASE_URL",
"R2_ACCESS_KEY",
"R2_SECRET_KEY",
"R2_BUCKET_NAME",
"R2_ENDPOINT",
]
missing = [e for e in required_envs if not os.environ.get(e)]
if missing:
print("\n[FAIL] Missing environment variables:")
for m in missing:
print(f" - {m}")
else:
print("\n[OK] Required environment variables set")
use_public = os.environ.get("USE_PUBLIC_URL", "false").lower() == "true"
print(f"\nDatabase URL mode : {'public' if use_public else 'private'}")
if os.environ.get("BACKUP_PASSWORD"):
print("Compression : 7z (encrypted)")
else:
print("Compression : gzip")
print("\nDoctor check complete.")
def config_show():
print("pg-r2-backup config\n")
config = {
"USE_PUBLIC_URL": os.environ.get("USE_PUBLIC_URL", "false"),
"DUMP_FORMAT": os.environ.get("DUMP_FORMAT", "dump"),
"FILENAME_PREFIX": os.environ.get("FILENAME_PREFIX", "backup"),
"MAX_BACKUPS": os.environ.get("MAX_BACKUPS", "7"),
"BACKUP_TIME": os.environ.get("BACKUP_TIME", "00:00"),
"R2_BUCKET_NAME": os.environ.get("R2_BUCKET_NAME", ""),
"R2_ENDPOINT": os.environ.get("R2_ENDPOINT", ""),
"R2_ACCESS_KEY": mask(os.environ.get("R2_ACCESS_KEY")),
"R2_SECRET_KEY": mask(os.environ.get("R2_SECRET_KEY")),
}
for k, v in config.items():
print(f"{k:<16} : {v}")
def init_env():
if os.path.exists(".env"):
print("[ERROR] .env already exists")
return
example = ".env.example"
if not os.path.exists(example):
print("[ERROR] .env.example not found")
return
shutil.copy(example, ".env")
print("[SUCCESS] .env created from .env.example")
print("Edit the file before running backups.")
def schedule_info():
print(textwrap.dedent("""
pg-r2-backup scheduling
Linux / macOS (cron):
0 0 * * * pg-r2-backup run
Windows (Task Scheduler):
Program : pg-r2-backup
Args : run
Start in: folder containing .env (working directory)
Railway / Docker:
Use the platform scheduler
""").strip())
def main():
parser = argparse.ArgumentParser(
prog="pg-r2-backup",
description="PostgreSQL backup tool for Cloudflare R2",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent("""
Examples:
pg-r2-backup doctor
pg-r2-backup run
pg-r2-backup config show
pg-r2-backup init
pg-r2-backup schedule
""")
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {get_version()}"
)
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("run", help="Run backup immediately")
subparsers.add_parser("doctor", help="Check environment & dependencies")
subparsers.add_parser("schedule", help="Show scheduling examples")
config_parser = subparsers.add_parser("config", help="Show configuration")
config_sub = config_parser.add_subparsers(dest="subcommand")
config_sub.add_parser("show", help="Show current configuration")
subparsers.add_parser("init", help="Create .env from .env.example")
args = parser.parse_args()
if args.command == "run":
run_backup()
elif args.command == "doctor":
doctor()
elif args.command == "config" and args.subcommand == "show":
config_show()
elif args.command == "init":
init_env()
elif args.command == "schedule":
schedule_info()
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()

14
main.py
View File

@ -4,13 +4,14 @@ import boto3
from boto3.session import Config from boto3.session import Config
from datetime import datetime, timezone from datetime import datetime, timezone
from boto3.s3.transfer import TransferConfig from boto3.s3.transfer import TransferConfig
from dotenv import load_dotenv from dotenv import load_dotenv, find_dotenv
import time import time
import schedule import schedule
import py7zr import py7zr
import shutil import shutil
import gzip
load_dotenv() load_dotenv(find_dotenv(usecwd=True), override=True)
## ENV ## ENV
@ -51,6 +52,13 @@ def get_database_url():
raise ValueError("[ERROR] DATABASE_URL not set!") raise ValueError("[ERROR] DATABASE_URL not set!")
return DATABASE_URL return DATABASE_URL
def gzip_compress(src):
dst = src + ".gz"
with open(src, "rb") as f_in:
with gzip.open(dst, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
return dst
def run_backup(): def run_backup():
if shutil.which("pg_dump") is None: if shutil.which("pg_dump") is None:
log("[ERROR] pg_dump not found. Install postgresql-client.") log("[ERROR] pg_dump not found. Install postgresql-client.")
@ -99,7 +107,7 @@ def run_backup():
log("[SUCCESS] Backup encrypted successfully") log("[SUCCESS] Backup encrypted successfully")
else: else:
log("[INFO] Compressing backup with gzip...") log("[INFO] Compressing backup with gzip...")
subprocess.run(["gzip", "-f", backup_file], check=True) gzip_compress(backup_file)
log("[SUCCESS] Backup compressed successfully") log("[SUCCESS] Backup compressed successfully")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:

35
pyproject.toml Normal file
View File

@ -0,0 +1,35 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "pg-r2-backup"
version = "1.0.5"
description = "PostgreSQL backup tool for Cloudflare R2 (S3 Compatible)"
readme = "README.md"
requires-python = ">=3.9"
authors = [
{ name = "Aman" }
]
license = "MIT"
dependencies = [
"boto3",
"python-dotenv",
"schedule",
"py7zr"
]
[project.urls]
Homepage = "https://github.com/BigDaddyAman/pg-r2-backup"
Repository = "https://github.com/BigDaddyAman/pg-r2-backup"
Issues = "https://github.com/BigDaddyAman/pg-r2-backup/issues"
[tool.setuptools]
packages = ["cli"]
py-modules = ["main"]
[project.scripts]
pg-r2-backup = "cli.cli:main"

View File

@ -1,4 +1,4 @@
boto3==1.42.26 boto3==1.42.39
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
python-dotenv==1.2.1 python-dotenv==1.2.1
py7zr==1.1.0 py7zr==1.1.0