🚀 Auto-deploy: BrainKP atualizado em 24/07/2026 23:45:56
This commit is contained in:
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user