Compare commits
7 Commits
1f586ef441
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f371c23ec | |||
| 6ebb16634a | |||
| 15845d9be0 | |||
| 2046f85d6b | |||
| 3495a63fd1 | |||
| 2c8a8f94c4 | |||
| 57fc5249e4 |
@@ -221,6 +221,97 @@ def status():
|
||||
stats = get_backup_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", remote.split(":")[1])
|
||||
|
||||
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)
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
to_delete_paths = []
|
||||
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:
|
||||
to_delete_paths.append(item.get("Path", item.get("Name")))
|
||||
|
||||
deleted_count = 0
|
||||
errors = []
|
||||
|
||||
if to_delete_paths:
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
||||
for p in to_delete_paths:
|
||||
f.write(p + '\n')
|
||||
list_file = f.name
|
||||
|
||||
cmd = [
|
||||
"rclone", "delete", remote,
|
||||
"--files-from", list_file,
|
||||
"--drive-root-folder-id", "1ey-5aABgHDtirxeJarWy0ZmeqQPVTYtm",
|
||||
"--drive-use-trash=false" # permanently delete to immediately free space
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
os.remove(list_file)
|
||||
|
||||
if result.returncode == 0:
|
||||
deleted_count = len(to_delete_paths)
|
||||
else:
|
||||
errors.append(f"Failed to bulk delete: {result.stderr}")
|
||||
|
||||
# invalidate cache
|
||||
global _stats_cache
|
||||
_stats_cache["timestamp"] = 0
|
||||
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"deleted_count": deleted_count,
|
||||
"errors": errors
|
||||
})
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# LIST BACKUPS
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -243,7 +334,7 @@ def list_backups(backup_type):
|
||||
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())
|
||||
items = list_rclone_contents("gdrive", remote.split(":")[1])
|
||||
|
||||
# Group repos by owner
|
||||
if backup_type == "repos":
|
||||
@@ -458,6 +549,13 @@ def run_backup():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e), "trace": traceback.format_exc()}), 500
|
||||
|
||||
@app.route("/api/download/<filename>")
|
||||
def download_file(filename):
|
||||
path = f"/tmp/restore-temp/{filename}"
|
||||
if os.path.exists(path):
|
||||
return send_file(path, as_attachment=True)
|
||||
return "File not found locally", 404
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# STATIC & LOGS
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
+54
-9
@@ -55,8 +55,8 @@ input,button,select{font-family:inherit}
|
||||
.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 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:hover .run-btn{display:block}
|
||||
.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{opacity:0.9}
|
||||
|
||||
/* 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}
|
||||
@@ -221,6 +221,7 @@ input,button,select{font-family:inherit}
|
||||
</div>
|
||||
<div class="modal-body" id="modal-body"></div>
|
||||
<div class="modal-footer" id="modal-footer">
|
||||
<a id="modal-download" class="btn" style="display:none; text-decoration:none;" target="_blank">📥 Baixar</a>
|
||||
<button class="btn" onclick="closeModal()">Cancelar</button>
|
||||
<button class="btn btn-primary" id="modal-confirm" onclick="doRestore()">Restaurar</button>
|
||||
</div>
|
||||
@@ -319,7 +320,8 @@ async function loadStatus() {
|
||||
</div>
|
||||
${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>`
|
||||
: `<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>
|
||||
`).join('');
|
||||
@@ -358,18 +360,21 @@ function renderBackupList(items) {
|
||||
list.innerHTML = '<div class="empty"><div class="empty-icon">📭</div>Nenhum backup encontrado</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = items.map(item => `
|
||||
list.innerHTML = items.map(item => {
|
||||
const appName = getAppBaseName(item.name);
|
||||
return `
|
||||
<div class="backup-item">
|
||||
<div class="bi-left">
|
||||
<span class="bi-name">${item.name}</span>
|
||||
<span class="bi-date">${formatDateTime(item.modified)}</span>
|
||||
<div class="bi-left" style="flex: 1; display: flex; align-items: center; gap: 12px; min-width: 0;">
|
||||
<span class="badge yellow" style="min-width: 100px; max-width: 120px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${appName}">${appName}</span>
|
||||
<span class="bi-name" style="flex: 1; min-width: 0;">${item.name}</span>
|
||||
<span class="bi-date" style="white-space: nowrap; margin-right: 10px;">${formatDateTime(item.modified)}</span>
|
||||
</div>
|
||||
<div class="bi-right">
|
||||
<span class="bi-size">${formatSize(item.size)}</span>
|
||||
<span class="bi-size" style="min-width: 60px; text-align: right;">${formatSize(item.size)}</span>
|
||||
<button class="restore-btn" onclick="openRestoreModal('${currentTab}', '${item.name}', '${item.size}')">Restaurar</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
function renderRepos(grouped) {
|
||||
@@ -448,6 +453,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 = '<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;
|
||||
|
||||
function openRestoreModal(type, filename, size) {
|
||||
@@ -473,6 +505,7 @@ function openRestoreModal(type, filename, size) {
|
||||
<div class="progress-bar" id="dl-progress"><div class="fill" style="width:0%"></div></div>
|
||||
`;
|
||||
document.getElementById('modal-confirm').style.display = 'inline-block';
|
||||
document.getElementById('modal-download').style.display = 'none';
|
||||
document.getElementById('restore-modal').classList.add('show');
|
||||
|
||||
// Start download
|
||||
@@ -511,6 +544,8 @@ function openRestoreModal(type, filename, size) {
|
||||
` cp -r ${d.extracted_to}/* /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/`;
|
||||
}
|
||||
document.getElementById('restore-instructions').value = instructions || 'Pronto para uso.';
|
||||
document.getElementById('modal-download').href = `/api/download/${filename}`;
|
||||
document.getElementById('modal-download').style.display = 'inline-block';
|
||||
} else {
|
||||
document.getElementById('restore-instructions').value = `Erro: ${d.error}`;
|
||||
}
|
||||
@@ -538,6 +573,7 @@ function openRestoreRepo(owner, repoName, size) {
|
||||
<textarea id="restore-instructions" disabled>Baixando e preparando...</textarea>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('modal-download').style.display = 'none';
|
||||
document.getElementById('restore-modal').classList.add('show');
|
||||
|
||||
fetch(`${API}/restore/repo`, {
|
||||
@@ -557,6 +593,9 @@ function openRestoreRepo(owner, repoName, size) {
|
||||
` cp -r ${d.extracted_to}/* /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/git/repositories/${owner}/\n` +
|
||||
` # Reinicie o Gitea\n` +
|
||||
` docker start <gitea-container>`;
|
||||
|
||||
document.getElementById('modal-download').href = `/api/download/${repoName}`;
|
||||
document.getElementById('modal-download').style.display = 'inline-block';
|
||||
} else {
|
||||
document.getElementById('restore-instructions').value = `Erro: ${d.error}`;
|
||||
}
|
||||
@@ -631,6 +670,12 @@ function getTypeLabel(type) {
|
||||
return {apps:'📦 Apps', bd:'🗄️ Banco de Dados', git:'🐙 Git Full', repos:'📁 Repositórios'}[type] || type;
|
||||
}
|
||||
|
||||
function getAppBaseName(filename) {
|
||||
const match = filename.match(/(.*?)(?:-\d{8}_\d{6})/);
|
||||
if (match) return match[1];
|
||||
return filename.split('-')[0];
|
||||
}
|
||||
|
||||
// Auto-refresh every 30s
|
||||
setInterval(() => {
|
||||
if (document.getElementById('app').style.display !== 'none') {
|
||||
|
||||
@@ -32,7 +32,7 @@ fi
|
||||
# 2. Gatilho de Deploy no Coolify (API Oficial v1)
|
||||
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}"
|
||||
-H "Authorization: Bearer ${COOLIFY_TOKEN}" \
|
||||
-H "Content-Type: application/json"
|
||||
echo -e "\n${GREEN}✅ Deploy oficial via API engatilhado.${NC}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user