🚀 Auto-deploy: BrainKP atualizado em 24/07/2026 23:45:56

This commit is contained in:
2026-07-24 23:45:56 +00:00
parent 1f586ef441
commit 57fc5249e4
2 changed files with 102 additions and 1 deletions
+73
View File
@@ -221,6 +221,79 @@ def status():
stats = get_backup_stats() stats = get_backup_stats()
return jsonify({"log": log, "stats": stats}) return jsonify({"log": log, "stats": stats})
# ──────────────────────────────────────────────
# CLEANUP
# ──────────────────────────────────────────────
@app.route("/api/cleanup/<backup_type>", methods=["POST"])
def cleanup_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())
import re
def get_group_key(item):
path = item.get("Path", item.get("Name", ""))
# For repos, path might be owner/repo-datetime.tar.gz
# Extract base name before the date string (e.g. -20260724_021744)
match = re.search(r'(-\d{8}_\d{6})', path)
if match:
return path[:match.start()]
return path.split('-')[0]
# Filter out directories
items = [i for i in items if not i.get("IsDir")]
# Group by key
grouped = {}
for item in items:
key = get_group_key(item)
if key not in grouped:
grouped[key] = []
grouped[key].append(item)
deleted_count = 0
errors = []
for key, group_items in grouped.items():
# Sort by ModTime descending (newest first)
group_items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
# Keep first 2
to_delete = group_items[2:]
for item in to_delete:
path = item.get("Path", item.get("Name"))
full_remote_path = f"{remote}/{path}"
cmd = ["rclone", "deletefile", full_remote_path, "--drive-root-folder-id", "1ey-5aABgHDtirxeJarWy0ZmeqQPVTYtm"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode == 0:
deleted_count += 1
else:
errors.append(f"Failed to delete {path}: {result.stderr}")
# invalidate cache
global _stats_cache
_stats_cache["timestamp"] = 0
return jsonify({
"ok": True,
"deleted_count": deleted_count,
"errors": errors
})
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
# LIST BACKUPS # LIST BACKUPS
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
+29 -1
View File
@@ -319,7 +319,8 @@ async function loadStatus() {
</div> </div>
${c.running ${c.running
? `<div style="margin-top:12px;text-align:center;font-size:11px;color:var(--accent);font-weight:bold"><span class="loading"></span> Sincronizando...</div>` ? `<div style="margin-top:12px;text-align:center;font-size:11px;color:var(--accent);font-weight:bold"><span class="loading"></span> Sincronizando...</div>`
: `<button class="run-btn" onclick="event.stopPropagation();runBackup('${c.id}')">▶ Executar Backup</button>` : `<button class="run-btn" onclick="event.stopPropagation();runBackup('${c.id}')">▶ Executar Backup</button>
<button class="run-btn" style="margin-top:5px; background:var(--danger)" onclick="event.stopPropagation();cleanupBackups('${c.id}')">🗑️ Limpar Antigos</button>`
} }
</div> </div>
`).join(''); `).join('');
@@ -448,6 +449,33 @@ async function runBackup(type) {
btn.innerHTML = '▶ Executar Backup'; btn.innerHTML = '▶ Executar Backup';
} }
async function cleanupBackups(type) {
if (!confirm(`Tem certeza que deseja apagar backups antigos de ${getTypeLabel(type)} (mantendo apenas os 2 mais recentes de cada)?`)) return;
const btn = event.target;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="loading"></span> Limpando...';
try {
const res = await fetch(`${API}/cleanup/${type}`, { method: 'POST' });
const d = await res.json();
if (d.ok) {
if (d.errors && d.errors.length > 0) {
showToast(`Concluído com alertas: ${d.deleted_count} apagados. ${d.errors.length} erros.`, 'warning');
} else {
showToast(`Limpeza concluída! ${d.deleted_count} backups antigos apagados.`, 'ok');
}
loadStatus();
loadBackups();
} else {
showToast(d.error || 'Erro na limpeza', 'error');
}
} catch(e) {
showToast('Erro ao realizar limpeza', 'error');
}
btn.disabled = false;
btn.innerHTML = originalText;
}
let restoreTarget = null; let restoreTarget = null;
function openRestoreModal(type, filename, size) { function openRestoreModal(type, filename, size) {