Compare commits

...

3 Commits

3 changed files with 105 additions and 4 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
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
+31 -3
View File
@@ -55,8 +55,8 @@ input,button,select{font-family:inherit}
.card-name{font-size:13px;font-weight:600;margin-bottom:4px} .card-name{font-size:13px;font-weight:600;margin-bottom:4px}
.card-meta{font-size:11px;color:var(--muted);line-height:1.5} .card-meta{font-size:11px;color:var(--muted);line-height:1.5}
.card-meta span{display:block} .card-meta span{display:block}
.card .run-btn{display:none;margin-top:10px;padding:6px 12px;background:var(--accent);border:none;border-radius:6px;color:#fff;font-size:11px;cursor:pointer;width:100%} .card .run-btn{display:block;margin-top:10px;padding:6px 12px;background:var(--accent);border:none;border-radius:6px;color:#fff;font-size:11px;cursor:pointer;width:100%}
.card:hover .run-btn{display:block} .card:hover .run-btn{opacity:0.9}
/* BACKUP LIST */ /* BACKUP LIST */
.backup-item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;margin-bottom:8px;cursor:pointer;transition:border .2s} .backup-item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;margin-bottom:8px;cursor:pointer;transition:border .2s}
@@ -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) {
+1 -1
View File
@@ -32,7 +32,7 @@ fi
# 2. Gatilho de Deploy no Coolify (API Oficial v1) # 2. Gatilho de Deploy no Coolify (API Oficial v1)
echo -e "${YELLOW}🔄 Disparando Deploy via API Oficial no Coolify...${NC}" 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" \ curl -s -X GET "https://painel.reifonas.cloud/api/v1/deploy?uuid=${COOLIFY_RESOURCE_UUID}&force=false" \
-H "Authorization: Bearer ${COOLIFY_TOKEN}" -H "Authorization: Bearer ${COOLIFY_TOKEN}" \
-H "Content-Type: application/json" -H "Content-Type: application/json"
echo -e "\n${GREEN}✅ Deploy oficial via API engatilhado.${NC}" echo -e "\n${GREEN}✅ Deploy oficial via API engatilhado.${NC}"