Compare commits
4 Commits
3495a63fd1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f371c23ec | |||
| 6ebb16634a | |||
| 15845d9be0 | |||
| 2046f85d6b |
@@ -243,7 +243,7 @@ def cleanup_backups(backup_type):
|
|||||||
items = list_rclone_contents("gdrive", "Git")
|
items = list_rclone_contents("gdrive", "Git")
|
||||||
items = [i for i in items if "gitea-data" in i.get("Name", "")]
|
items = [i for i in items if "gitea-data" in i.get("Name", "")]
|
||||||
else:
|
else:
|
||||||
items = list_rclone_contents("gdrive", backup_type.capitalize())
|
items = list_rclone_contents("gdrive", remote.split(":")[1])
|
||||||
|
|
||||||
import re
|
import re
|
||||||
def get_group_key(item):
|
def get_group_key(item):
|
||||||
@@ -266,23 +266,41 @@ def cleanup_backups(backup_type):
|
|||||||
grouped[key] = []
|
grouped[key] = []
|
||||||
grouped[key].append(item)
|
grouped[key].append(item)
|
||||||
|
|
||||||
deleted_count = 0
|
import tempfile
|
||||||
errors = []
|
import os
|
||||||
|
|
||||||
|
to_delete_paths = []
|
||||||
for key, group_items in grouped.items():
|
for key, group_items in grouped.items():
|
||||||
# Sort by ModTime descending (newest first)
|
# Sort by ModTime descending (newest first)
|
||||||
group_items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
|
group_items.sort(key=lambda x: x.get("ModTime", ""), reverse=True)
|
||||||
# Keep first 2
|
# Keep first 2
|
||||||
to_delete = group_items[2:]
|
to_delete = group_items[2:]
|
||||||
for item in to_delete:
|
for item in to_delete:
|
||||||
path = item.get("Path", item.get("Name"))
|
to_delete_paths.append(item.get("Path", item.get("Name")))
|
||||||
full_remote_path = f"{remote}/{path}"
|
|
||||||
cmd = ["rclone", "deletefile", full_remote_path, "--drive-root-folder-id", "1ey-5aABgHDtirxeJarWy0ZmeqQPVTYtm"]
|
deleted_count = 0
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
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:
|
if result.returncode == 0:
|
||||||
deleted_count += 1
|
deleted_count = len(to_delete_paths)
|
||||||
else:
|
else:
|
||||||
errors.append(f"Failed to delete {path}: {result.stderr}")
|
errors.append(f"Failed to bulk delete: {result.stderr}")
|
||||||
|
|
||||||
# invalidate cache
|
# invalidate cache
|
||||||
global _stats_cache
|
global _stats_cache
|
||||||
@@ -316,7 +334,7 @@ def list_backups(backup_type):
|
|||||||
items = list_rclone_contents("gdrive", "Git")
|
items = list_rclone_contents("gdrive", "Git")
|
||||||
items = [i for i in items if "gitea-data" in i.get("Name", "")]
|
items = [i for i in items if "gitea-data" in i.get("Name", "")]
|
||||||
else:
|
else:
|
||||||
items = list_rclone_contents("gdrive", backup_type.capitalize())
|
items = list_rclone_contents("gdrive", remote.split(":")[1])
|
||||||
|
|
||||||
# Group repos by owner
|
# Group repos by owner
|
||||||
if backup_type == "repos":
|
if backup_type == "repos":
|
||||||
@@ -531,6 +549,13 @@ def run_backup():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e), "trace": traceback.format_exc()}), 500
|
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
|
# STATIC & LOGS
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|||||||
+23
-6
@@ -221,6 +221,7 @@ input,button,select{font-family:inherit}
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="modal-body"></div>
|
<div class="modal-body" id="modal-body"></div>
|
||||||
<div class="modal-footer" id="modal-footer">
|
<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" onclick="closeModal()">Cancelar</button>
|
||||||
<button class="btn btn-primary" id="modal-confirm" onclick="doRestore()">Restaurar</button>
|
<button class="btn btn-primary" id="modal-confirm" onclick="doRestore()">Restaurar</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,18 +360,21 @@ function renderBackupList(items) {
|
|||||||
list.innerHTML = '<div class="empty"><div class="empty-icon">📭</div>Nenhum backup encontrado</div>';
|
list.innerHTML = '<div class="empty"><div class="empty-icon">📭</div>Nenhum backup encontrado</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = items.map(item => `
|
list.innerHTML = items.map(item => {
|
||||||
|
const appName = getAppBaseName(item.name);
|
||||||
|
return `
|
||||||
<div class="backup-item">
|
<div class="backup-item">
|
||||||
<div class="bi-left">
|
<div class="bi-left" style="flex: 1; display: flex; align-items: center; gap: 12px; min-width: 0;">
|
||||||
<span class="bi-name">${item.name}</span>
|
<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-date">${formatDateTime(item.modified)}</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>
|
||||||
<div class="bi-right">
|
<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>
|
<button class="restore-btn" onclick="openRestoreModal('${currentTab}', '${item.name}', '${item.size}')">Restaurar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRepos(grouped) {
|
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>
|
<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-confirm').style.display = 'inline-block';
|
||||||
|
document.getElementById('modal-download').style.display = 'none';
|
||||||
document.getElementById('restore-modal').classList.add('show');
|
document.getElementById('restore-modal').classList.add('show');
|
||||||
|
|
||||||
// Start download
|
// Start download
|
||||||
@@ -539,6 +544,8 @@ function openRestoreModal(type, filename, size) {
|
|||||||
` cp -r ${d.extracted_to}/* /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/`;
|
` cp -r ${d.extracted_to}/* /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/`;
|
||||||
}
|
}
|
||||||
document.getElementById('restore-instructions').value = instructions || 'Pronto para uso.';
|
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 {
|
} else {
|
||||||
document.getElementById('restore-instructions').value = `Erro: ${d.error}`;
|
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>
|
<textarea id="restore-instructions" disabled>Baixando e preparando...</textarea>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
document.getElementById('modal-download').style.display = 'none';
|
||||||
document.getElementById('restore-modal').classList.add('show');
|
document.getElementById('restore-modal').classList.add('show');
|
||||||
|
|
||||||
fetch(`${API}/restore/repo`, {
|
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` +
|
` cp -r ${d.extracted_to}/* /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/git/repositories/${owner}/\n` +
|
||||||
` # Reinicie o Gitea\n` +
|
` # Reinicie o Gitea\n` +
|
||||||
` docker start <gitea-container>`;
|
` docker start <gitea-container>`;
|
||||||
|
|
||||||
|
document.getElementById('modal-download').href = `/api/download/${repoName}`;
|
||||||
|
document.getElementById('modal-download').style.display = 'inline-block';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('restore-instructions').value = `Erro: ${d.error}`;
|
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;
|
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
|
// Auto-refresh every 30s
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (document.getElementById('app').style.display !== 'none') {
|
if (document.getElementById('app').style.display !== 'none') {
|
||||||
|
|||||||
Reference in New Issue
Block a user