diff --git a/app.py b/app.py index 1e18321..dd60f8e 100644 --- a/app.py +++ b/app.py @@ -221,6 +221,79 @@ def status(): stats = get_backup_stats() return jsonify({"log": log, "stats": stats}) +# ────────────────────────────────────────────── +# CLEANUP +# ────────────────────────────────────────────── + +@app.route("/api/cleanup/", 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 # ────────────────────────────────────────────── diff --git a/static/index.html b/static/index.html index 1a82972..99fef0e 100644 --- a/static/index.html +++ b/static/index.html @@ -319,7 +319,8 @@ async function loadStatus() { ${c.running ? `
Sincronizando...
` - : `` + : ` + ` } `).join(''); @@ -448,6 +449,33 @@ async function runBackup(type) { 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 = ' 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; function openRestoreModal(type, filename, size) {