Compare commits

..

4 Commits

2 changed files with 60 additions and 18 deletions
+35 -10
View File
@@ -243,7 +243,7 @@ def cleanup_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])
import re
def get_group_key(item):
@@ -266,23 +266,41 @@ def cleanup_backups(backup_type):
grouped[key] = []
grouped[key].append(item)
deleted_count = 0
errors = []
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:
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)
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 += 1
deleted_count = len(to_delete_paths)
else:
errors.append(f"Failed to delete {path}: {result.stderr}")
errors.append(f"Failed to bulk delete: {result.stderr}")
# invalidate cache
global _stats_cache
@@ -316,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":
@@ -531,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
# ──────────────────────────────────────────────
+23 -6
View File
@@ -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>
@@ -359,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) {
@@ -501,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
@@ -539,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}`;
}
@@ -566,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`, {
@@ -585,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}`;
}
@@ -659,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') {