Files
BrainKP/app.py
T

402 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Backup UI - Flask backend
Serves the backup dashboard and handles restore operations.
"""
import os
import subprocess
import json
import tarfile
import re
from datetime import datetime
from flask import Flask, render_template, jsonify, request, send_file
app = Flask(__name__)
APP_DIR = "/root/Apps/backup-ui"
BACKUP_DIR = "/tmp/restore-temp"
LOG_FILE = "/root/backup.log"
PASSWORD = "@@Gi05Br;;" # Change this!
os.makedirs(BACKUP_DIR, exist_ok=True)
# ──────────────────────────────────────────────
# AUTH
# ──────────────────────────────────────────────
@app.route("/api/login", methods=["POST"])
def login():
data = request.json or {}
if data.get("password") == PASSWORD:
return jsonify({"ok": True})
return jsonify({"ok": False, "error": "Invalid password"}), 401
# ──────────────────────────────────────────────
# BACKUP STATUS
# ──────────────────────────────────────────────
def get_backup_log():
"""Parse the backup log to get last run info."""
if not os.path.exists(LOG_FILE):
return {"last_run": None, "entries": []}
with open(LOG_FILE, "r") as f:
lines = f.readlines()
entries = []
current = {}
for line in lines[-500:]: # last 500 lines
if "==========" in line:
if current:
entries.append(current)
ts = line.strip().split("==========")[1].strip()
current = {"timestamp": ts, "scripts": []}
elif "[Backup" in line and "started" in line:
script = line.strip().split("] ")[1].split(" started")[0]
current["scripts"].append({"name": script, "status": "running"})
elif "[Backup" in line and "finished" in line:
script = line.strip().split("] ")[1].split(" finished")[0]
for s in current.get("scripts", []):
if s["name"] == script:
s["status"] = "ok"
elif "error" in line.lower() or "failed" in line.lower():
current["has_error"] = True
current["error"] = line.strip()
if current:
entries.append(current)
entries.reverse()
return {
"last_run": entries[0] if entries else None,
"entries": entries[:10]
}
def list_rclone_contents(remote, path="", depth=1):
"""List contents of a rclone remote path."""
cmd = ["rclone", "lsjson", f"{remote}:{path}",
"--drive-root-folder-id", "1ey-5aABgHDtirxeJarWy0ZmeqQPVTYtm",
"--max-depth", str(depth)]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
return json.loads(result.stdout)
except:
pass
return []
def get_backup_stats():
"""Get backup stats from GDrive."""
stats = {
"apps": {"count": 0, "latest": None, "size": 0},
"bd": {"count": 0, "latest": None, "size": 0},
"git": {"count": 0, "latest": None, "size": 0},
"repos": {"count": 0, "latest": None, "size": 0}
}
# Apps
try:
items = list_rclone_contents("gdrive", "Apps")
stats["apps"]["count"] = len(items)
if items:
items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
stats["apps"]["latest"] = items[0].get("Name")
stats["apps"]["size"] = sum(i.get("Size", 0) for i in items)
except:
pass
# BD
try:
items = list_rclone_contents("gdrive", "BD")
stats["bd"]["count"] = len(items)
if items:
items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
stats["bd"]["latest"] = items[0].get("Name")
stats["bd"]["size"] = sum(i.get("Size", 0) for i in items)
except:
pass
# Git
try:
items = list_rclone_contents("gdrive", "Git")
git_items = [i for i in items if "gitea-data" in i.get("Name", "")]
stats["git"]["count"] = len(git_items)
if git_items:
git_items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
stats["git"]["latest"] = git_items[0].get("Name")
stats["git"]["size"] = sum(i.get("Size", 0) for i in git_items)
# Repos
raw_repo_items = list_rclone_contents("gdrive", "Git/repos", depth=2)
repo_items = [i for i in raw_repo_items if not i.get("IsDir")]
stats["repos"]["count"] = len(repo_items)
if repo_items:
repo_items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
stats["repos"]["latest"] = repo_items[0].get("Name")
stats["repos"]["size"] = sum(i.get("Size", 0) for i in repo_items)
except:
pass
return stats
@app.route("/api/status")
def status():
log = get_backup_log()
stats = get_backup_stats()
return jsonify({"log": log, "stats": stats})
# ──────────────────────────────────────────────
# LIST BACKUPS
# ──────────────────────────────────────────────
@app.route("/api/backups/<backup_type>")
def list_backups(backup_type):
remotes = {
"apps": "gdrive:Apps",
"bd": "gdrive:BD",
"git": "gdrive:Git",
"repos": "gdrive:Git/repos"
}
remote = remotes.get(backup_type)
if not remote:
return jsonify({"error": "Unknown type"}), 400
if backup_type == "repos":
items = list_rclone_contents("gdrive", "Git/repos", depth=2)
elif backup_type == "git":
items = list_rclone_contents("gdrive", "Git")
items = [i for i in items if "gitea-data" in i.get("Name", "")]
else:
items = list_rclone_contents("gdrive", backup_type.capitalize())
# Group repos by owner
if backup_type == "repos":
grouped = {}
for item in items:
if item.get("IsDir"): continue
path = item.get("Path", "")
parts = path.split("/")
if len(parts) >= 2:
owner = parts[0]
repo = "/".join(parts[1:])
if owner not in grouped:
grouped[owner] = []
grouped[owner].append({"name": repo, "size": item.get("Size", 0), "modified": item.get("ModTime", "")})
else:
grouped["other"] = grouped.get("other", [])
grouped["other"].append({"name": name, "size": item.get("Size", 0), "modified": item.get("ModTime", "")})
return jsonify({"items": grouped, "type": "repos"})
items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
return jsonify({"items": [{"name": i.get("Name"), "size": i.get("Size", 0), "modified": i.get("ModTime", "")} for i in items], "type": backup_type})
# ──────────────────────────────────────────────
# RESTORE
# ──────────────────────────────────────────────
@app.route("/api/restore/<backup_type>", methods=["POST"])
def restore(backup_type):
data = request.json or {}
filename = data.get("filename")
target = data.get("target", "") # for repos: owner/repo
if not filename:
return jsonify({"error": "No filename provided"}), 400
remotes = {
"apps": "gdrive:Apps",
"bd": "gdrive:BD",
"git": "gdrive:Git",
"repos": "gdrive:Git/repos"
}
remote = remotes.get(backup_type)
if not remote:
return jsonify({"error": "Unknown type"}), 400
remote_path = f"{remote}/{target}/{filename}" if target and backup_type == "repos" else f"{remote}/{filename}"
# Download
local_file = f"{BACKUP_DIR}/{filename}"
cmd = ["rclone", "copy", remote_path, BACKUP_DIR, "--drive-root-folder-id", "1ey-5aABgHDtirxeJarWy0ZmeqQPVTYtm"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode != 0:
return jsonify({"error": f"Download failed: {result.stderr}"}), 500
return jsonify({"ok": True, "file": local_file, "size": os.path.getsize(local_file)})
@app.route("/api/restore/apps", methods=["POST"])
def restore_apps():
data = request.json or {}
filename = data.get("filename")
if not filename:
return jsonify({"error": "No filename"}), 400
src = f"/tmp/restore-temp/{filename}"
if not os.path.exists(src):
return jsonify({"error": "File not found locally"}), 404
# Extract to temp
extract_dir = f"/tmp/restore-apps-extract"
os.makedirs(extract_dir, exist_ok=True)
subprocess.run(["tar", "-xzf", src, "-C", extract_dir], capture_output=True)
# List what was extracted
extracted = os.listdir(extract_dir)
return jsonify({"ok": True, "extracted_to": extract_dir, "files": extracted, "instructions": {
"Camila": f"Copiar {extract_dir}/Camila/* para /root/Apps/Camila/",
"RAG": f"Copiar {extract_dir}/RAG/* para /root/Apps/RAG/",
"VOXDO": f"Copiar {extract_dir}/VOXDO/* para /root/Apps/VOXDO/"
}})
@app.route("/api/restore/bd", methods=["POST"])
def restore_bd():
data = request.json or {}
filename = data.get("filename")
db_type = data.get("db", "auto") # supabase or gitea
if not filename:
return jsonify({"error": "No filename"}), 400
src = f"/tmp/restore-temp/{filename}"
if not os.path.exists(src):
return jsonify({"error": "File not found locally"}), 404
extract_dir = f"/tmp/restore-bd-extract"
os.makedirs(extract_dir, exist_ok=True)
subprocess.run(["tar", "-xzf", src, "-C", extract_dir], capture_output=True)
# Find the SQL file
sql_files = []
for root, dirs, files in os.walk(extract_dir):
for f in files:
if f.endswith(".sql"):
sql_files.append(os.path.join(root, f))
return jsonify({
"ok": True,
"extracted_to": extract_dir,
"sql_files": sql_files,
"instructions": {
"supabase": f"docker exec -i supabase-db psql -U postgres -d postgres < {sql_files[0] if sql_files else '<file>'}",
"gitea": f"docker exec -i coolify-db psql -U postgres -d gitea < {sql_files[0] if sql_files else '<file>'}"
}
})
@app.route("/api/restore/git", methods=["POST"])
def restore_git():
data = request.json or {}
filename = data.get("filename")
if not filename:
return jsonify({"error": "No filename"}), 400
src = f"/tmp/restore-temp/{filename}"
if not os.path.exists(src):
return jsonify({"error": "File not found locally"}), 404
extract_dir = f"/tmp/restore-git-extract"
os.makedirs(extract_dir, exist_ok=True)
subprocess.run(["tar", "-xzf", src, "-C", extract_dir], capture_output=True)
# List extracted
extracted = []
for root, dirs, files in os.walk(extract_dir):
for f in files:
extracted.append(os.path.join(root, f).replace(extract_dir + "/", ""))
return jsonify({
"ok": True,
"extracted_to": extract_dir,
"files": extracted,
"instructions": {
"gitea_data": f"Copiar conteúdo para /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/",
"gitea_db": f"docker exec -i <db-container> psql -U postgres -d gitea < <sql-file>"
}
})
@app.route("/api/restore/repo", methods=["POST"])
def restore_repo():
data = request.json or {}
owner = data.get("owner")
repo_name = data.get("repo")
filename = data.get("filename")
if not filename:
return jsonify({"error": "No filename"}), 400
src = f"/tmp/restore-temp/{filename}"
if not os.path.exists(src):
return jsonify({"error": "File not found locally"}), 404
extract_dir = f"/tmp/restore-repo-{owner}-{repo_name}"
os.makedirs(extract_dir, exist_ok=True)
# Extract tar.gz
result = subprocess.run(["tar", "-tzf", src], capture_output=True, text=True)
first_item = result.stdout.split("\n")[0] if result.stdout else ""
subprocess.run(["tar", "-xzf", src, "-C", extract_dir], capture_output=True)
# Find the .git folder
git_dir = None
for root, dirs, files in os.walk(extract_dir):
if ".git" in dirs:
git_dir = os.path.join(root, ".git")
work_tree = root
break
return jsonify({
"ok": True,
"extracted_to": extract_dir,
"git_dir": git_dir,
"work_tree": work_tree if 'work_tree' in dir() else extract_dir,
"instructions": {
"clone": f"git clone {git_dir} /tmp/{repo_name}-restored" if git_dir else "N/A",
"bare_restore": f"Copiar {git_dir} para /var/lib/docker/volumes/.../repositories/{owner}/{repo_name}.git" if git_dir else "N/A"
}
})
@app.route("/api/run-backup", methods=["POST"])
def run_backup():
data = request.json or {}
script = data.get("script")
scripts = {
"apps": "/root/backup-apps.sh",
"bd": "/root/backup-bd.sh",
"git": "/root/backup-git.sh",
"repos": "/root/backup-git-repos.sh"
}
if script not in scripts:
return jsonify({"error": "Unknown script"}), 400
# Run in background
import traceback
try:
proc = subprocess.Popen(
[scripts[script]],
stdout=open("/root/backup.log", "a"),
stderr=subprocess.STDOUT,
cwd="/root"
)
return jsonify({"ok": True, "message": f"{script} backup started", "pid": proc.pid})
except Exception as e:
return jsonify({"error": str(e), "trace": traceback.format_exc()}), 500
# ──────────────────────────────────────────────
# STATIC & LOGS
# ──────────────────────────────────────────────
@app.route("/")
def index():
return app.send_static_file("index.html")
@app.route("/api/log")
def log():
if os.path.exists(LOG_FILE):
with open(LOG_FILE, "r") as f:
lines = f.readlines()
return jsonify({"lines": lines[-200:]})
return jsonify({"lines": []})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8766, debug=False)