new changes
This commit is contained in:
@@ -27,6 +27,9 @@ class CreateBackupPlanRequest(BaseModel):
|
||||
target_id: int
|
||||
schedule: str # cron, e.g. "0 2 * * *"
|
||||
enabled: bool = True
|
||||
s3_bucket: str = ""
|
||||
s3_endpoint: str = ""
|
||||
s3_key_prefix: str = ""
|
||||
|
||||
|
||||
@router.get("/plans")
|
||||
@@ -45,6 +48,9 @@ async def backup_plans_list(
|
||||
"target_id": r.target_id,
|
||||
"schedule": r.schedule,
|
||||
"enabled": r.enabled,
|
||||
"s3_bucket": getattr(r, "s3_bucket", None) or "",
|
||||
"s3_endpoint": getattr(r, "s3_endpoint", None) or "",
|
||||
"s3_key_prefix": getattr(r, "s3_key_prefix", None) or "",
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
@@ -79,6 +85,9 @@ async def backup_plan_create(
|
||||
target_id=body.target_id,
|
||||
schedule=body.schedule,
|
||||
enabled=body.enabled,
|
||||
s3_bucket=(body.s3_bucket or "")[:256],
|
||||
s3_endpoint=(body.s3_endpoint or "")[:512],
|
||||
s3_key_prefix=(body.s3_key_prefix or "")[:256],
|
||||
)
|
||||
db.add(plan)
|
||||
await db.commit()
|
||||
@@ -107,6 +116,9 @@ async def backup_plan_update(
|
||||
plan.target_id = body.target_id
|
||||
plan.schedule = body.schedule
|
||||
plan.enabled = body.enabled
|
||||
plan.s3_bucket = (body.s3_bucket or "")[:256]
|
||||
plan.s3_endpoint = (body.s3_endpoint or "")[:512]
|
||||
plan.s3_key_prefix = (body.s3_key_prefix or "")[:256]
|
||||
await db.commit()
|
||||
return {"status": True, "msg": "Updated"}
|
||||
|
||||
@@ -143,6 +155,27 @@ def _run_site_backup(site: Site) -> tuple[bool, str, str | None]:
|
||||
return False, str(e), None
|
||||
|
||||
|
||||
def _maybe_upload_s3(local_file: str, plan: BackupPlan) -> tuple[bool, str]:
|
||||
"""Copy backup file to S3-compatible bucket if plan.s3_bucket set. Uses AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY."""
|
||||
bucket = (getattr(plan, "s3_bucket", None) or "").strip()
|
||||
if not bucket or not os.path.isfile(local_file):
|
||||
return True, ""
|
||||
try:
|
||||
import boto3
|
||||
except ImportError:
|
||||
return False, "boto3 not installed (pip install boto3)"
|
||||
ep = (getattr(plan, "s3_endpoint", None) or "").strip() or None
|
||||
prefix = (getattr(plan, "s3_key_prefix", None) or "").strip().strip("/")
|
||||
key_base = os.path.basename(local_file)
|
||||
key = f"{prefix}/{key_base}" if prefix else key_base
|
||||
try:
|
||||
client = boto3.client("s3", endpoint_url=ep)
|
||||
client.upload_file(local_file, bucket, key)
|
||||
return True, f"s3://{bucket}/{key}"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def _run_database_backup(dbo: Database) -> tuple[bool, str, str | None]:
|
||||
"""Run database backup (sync). Returns (ok, msg, filename)."""
|
||||
cfg = get_runtime_config()
|
||||
@@ -164,15 +197,17 @@ async def backup_run_scheduled(
|
||||
"""Run all due backup plans. Call this from cron (e.g. every hour) or manually."""
|
||||
from datetime import datetime as dt
|
||||
now = dt.utcnow()
|
||||
cfg = get_runtime_config()
|
||||
result = await db.execute(select(BackupPlan).where(BackupPlan.enabled == True))
|
||||
plans = result.scalars().all()
|
||||
results = []
|
||||
for plan in plans:
|
||||
ok = False
|
||||
msg = ""
|
||||
try:
|
||||
prev_run = croniter(plan.schedule, now).get_prev(dt)
|
||||
# Run if we're within 15 minutes after the scheduled time
|
||||
secs_since = (now - prev_run).total_seconds()
|
||||
if secs_since > 900 or secs_since < 0: # Not within 15 min window
|
||||
if secs_since > 900 or secs_since < 0:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
@@ -183,6 +218,11 @@ async def backup_run_scheduled(
|
||||
results.append({"plan": plan.name, "status": "skipped", "msg": "Site not found or path invalid"})
|
||||
continue
|
||||
ok, msg, filename = _run_site_backup(site)
|
||||
if ok and filename:
|
||||
full = os.path.join(cfg["backup_path"], filename)
|
||||
u_ok, u_msg = _maybe_upload_s3(full, plan)
|
||||
if u_msg:
|
||||
msg = f"{msg}; {u_msg}" if u_ok else f"{msg}; S3 failed: {u_msg}"
|
||||
if ok:
|
||||
send_email(
|
||||
subject=f"YakPanel - Scheduled backup: {plan.name}",
|
||||
@@ -195,6 +235,11 @@ async def backup_run_scheduled(
|
||||
results.append({"plan": plan.name, "status": "skipped", "msg": "Database not found"})
|
||||
continue
|
||||
ok, msg, filename = _run_database_backup(dbo)
|
||||
if ok and filename:
|
||||
full = os.path.join(cfg["backup_path"], "database", filename)
|
||||
u_ok, u_msg = _maybe_upload_s3(full, plan)
|
||||
if u_msg:
|
||||
msg = f"{msg}; {u_msg}" if u_ok else f"{msg}; S3 failed: {u_msg}"
|
||||
if ok:
|
||||
send_email(
|
||||
subject=f"YakPanel - Scheduled backup: {plan.name}",
|
||||
|
||||
Reference in New Issue
Block a user