From 120a716e72cc03865e47b26243f58952def9784a Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Mon, 22 Jun 2026 17:02:05 +0000 Subject: [PATCH] Initial commit: BrainKP Backup UI --- Dockerfile | 8 + app.py | 388 +++++++++++++++++++++++++++++ docker-compose.yml | 28 +++ requirements.txt | 2 + static/index.html | 600 +++++++++++++++++++++++++++++++++++++++++++++ update.sh | 38 +++ 6 files changed, 1064 insertions(+) create mode 100644 Dockerfile create mode 100644 app.py create mode 100755 docker-compose.yml create mode 100644 requirements.txt create mode 100644 static/index.html create mode 100755 update.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b0edbc2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . +COPY static/ static/ +EXPOSE 8766 +CMD ["gunicorn", "--bind", "0.0.0.0:8766", "--workers", "2", "--timeout", "120", "app:app"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..2dde1bf --- /dev/null +++ b/app.py @@ -0,0 +1,388 @@ +#!/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=""): + """List contents of a rclone remote path.""" + cmd = ["rclone", "lsjson", f"{remote}:{path}", "--drive-root-folder-id", "1ey-5aABgHDtirxeJarWy0ZmeqQPVTYtm"] + 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 + repo_items = list_rclone_contents("gdrive", "Git/repos") + 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/") +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") + 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: + name = item.get("Name", "") + parts = name.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/", 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 ''}", + "gitea": f"docker exec -i coolify-db psql -U postgres -d gitea < {sql_files[0] if sql_files else ''}" + } + }) + +@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 psql -U postgres -d gitea < " + } + }) + +@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 + subprocess.Popen([scripts[script]], stdout=open("/root/backup.log", "a"), stderr=subprocess.STDOUT) + return jsonify({"ok": True, "message": f"{script} backup started"}) + +# ────────────────────────────────────────────── +# 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) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..fde17e3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + brainkp: + build: . + container_name: brainkp-backup + restart: unless-stopped + expose: + - "8766" + volumes: + - .:/app + - /tmp/restore-temp:/tmp/restore-temp + - /root/backup.log:/root/backup.log:ro + env_file: + - .env + networks: + - coolify + labels: + - "traefik.enable=true" + - "traefik.http.routers.brainkp.rule=Host(`bck.brainsteel.com.br`)" + - "traefik.http.routers.brainkp.entrypoints=https" + - "traefik.http.routers.brainkp.tls=true" + - "traefik.http.routers.brainkp.tls.certresolver=letsencrypt" + - "traefik.http.services.brainkp.loadbalancer.server.port=8766" + +networks: + coolify: + external: true diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a36229c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=3.0.0 +gunicorn>=21.0.0 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..66f81c0 --- /dev/null +++ b/static/index.html @@ -0,0 +1,600 @@ + + + + + +BrainSteel Backup + + + + + +
+
+

🛡️ BrainSteel Backup

+

Digite a senha para acessar

+ + +
Senha incorreta
+
+
+ + +
+ + +
+

🛡️ BrainSteel Backup

+
+ + +
+
+ + +
+ + +
+
+

Status dos Backups

+ +
+
+
+ + +
+
+

Backups

+
+
+ + + + +
+
+
+ +
+ + + + + +
+ + + + diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..6720ec1 --- /dev/null +++ b/update.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# --------------------------------------------------------- +# BRAINKP: SCRIPT DE DEPLOY TOTAL & SINCRONIZAÇÃO AUTOMÁTICA +# --------------------------------------------------------- + +# Cores e Formatação Premium +CYAN='\033[0;36m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "\n${CYAN}🚀 Iniciando Ciclo Automático de Deploy BrainKP...${NC}" + +# 1. Sincronização com Repositório (Git - Gitea) +echo -e "${YELLOW}📝 Sincronizando código com o Gitea...${NC}" +git add . + +# Verifica se existem alterações para o commit +if git diff-index --quiet HEAD --; then + echo -e "${GREEN}✨ Código local já está sincronizado com o último commit.${NC}" +else + TIMESTAMP=$(date +"%d/%m/%Y %H:%M:%S") + echo -e "${CYAN}📤 Enviando alterações (Auto-update $TIMESTAMP)...${NC}" + git commit -m "🚀 Auto-deploy: BrainKP atualizado em $TIMESTAMP" + git push origin master +fi + +# 2. Gatilho de Deploy no Coolify (API Oficial v1) +if [ ! -z "$COOLIFY_RESOURCE_UUID" ]; then + echo -e "${YELLOW}🔄 Disparando Deploy via API Oficial no Coolify...${NC}" + curl -s -X GET "https://painel.reifonas.cloud/api/v1/deploy?uuid=${COOLIFY_RESOURCE_UUID}&force=false" \ + -H "Authorization: Bearer $COOLIFY_TOKEN" + echo -e "\n${GREEN}✅ Deploy oficial via API engatilhado.${NC}" +fi + +# 3. Resultado Final +echo -e "${GREEN}🏁 Ciclo concluído com sucesso.${NC}\n"