# backend/routes/backup.py
# import standard modules
-from fastapi import APIRouter, HTTPException, status
-from fastapi.responses import Response, JSONResponse
+from fastapi.responses import Response, JSONResponse, FileResponse
+from pathlib import Path
from pydantic import BaseModel
import time
from typing import Dict, Any
# Import local modules
from backend.backup import backup_create, backup_list, backup_restore, backup_delete
+# Import Settings
+from backend.settings.settings import settings
# Import Logging
from backend.log.log import get_logger
}
# ---------------------------------------------------------
-# API ENDPOINTS
+# API: Create Backup
# ---------------------------------------------------------
@router.post("/api/backup/create")
async def api_backup_create():
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"code": "BACKUP_DELETE_ERROR", "message": "Internal error"},
)
+
+# ---------------------------------------------------------
+# API: Download backup
+# ---------------------------------------------------------
+@router.get("/api/backup/download/{backup_id}")
+def download_backup(backup_id: str):
+ backup_dir = Path(settings.BACKUP_PATH)
+
+ zip_path = backup_dir / f"{backup_id}"
+
+ if not zip_path.exists():
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={
+ "code": "BACKUP_NOT_FOUND",
+ "status": "failure",
+ "message": "Backup not found",
+ },
+ )
+
+ return FileResponse(
+ path=zip_path,
+ filename=zip_path.name,
+ media_type="application/zip"
+ )
</a>
<!-- Backup & Restore -->
- <div class="tile">
+ <div class="tile" data-action="openBackupModal" role="button">
<div class="tile-icon"><i class="bi bi-arrow-counterclockwise"></i></div>
<h3>Backup & Restore</h3>
<p>Esecuzione backup e gestione archivi.</p>
<button class="btn btn-primary btn-sm" title="Start Backup" aria-label="Start Backup" data-action="startBackup">
<i class="bi bi-cloud-upload me-1"></i><span class="label">Create Backup</span>
</button>
- <button class="btn btn-primary btn-sm" title="Open Restore" aria-label="Open Restore" data-action="openBackupModal">
+ <button class="btn btn-primary btn-sm" title="Backup Management" aria-label="Backup Management" data-action="openBackupModal">
<i class="bi bi-cloud-download me-1"></i><span class="label">Backup Management</span>
</button>
</div>
return data;
}
+// -------------------------------------------------------
+// API Get
+// -------------------------------------------------------
export function apiGet(url, errorPrefix = 'Fetch error') {
return apiRequest(url, { method: 'GET' }, errorPrefix);
}
+// -------------------------------------------------------
+// API Post
+// -------------------------------------------------------
export function apiPost(url, payload, errorPrefix = 'Request error') {
return apiRequest(
url,
errorPrefix
);
}
+
+// -------------------------------------------------------
+// API Download
+// -------------------------------------------------------
+export async function apiDownload(url, errorPrefix = 'Download error') {
+ let res;
+
+ try {
+ res = await fetch(url);
+ } catch (err) {
+ throw new Error(`${errorPrefix}: network error`);
+ }
+
+ const contentType = res.headers.get("content-type") || "";
+
+ // JSON message (error)
+ if (contentType.includes("application/json")) {
+ let data;
+
+ try {
+ data = await res.json();
+ } catch {
+ throw new Error(`${errorPrefix}: Invalid error payload`);
+ }
+
+ const msg =
+ data?.detail?.message ||
+ data?.message ||
+ "Download failed";
+
+ throw new Error(`${errorPrefix}: ${msg}`);
+ }
+
+ // File
+ if (!res.ok) {
+ throw new Error(
+ `${errorPrefix}: ${res.status} ${res.statusText || ''}`.trim()
+ );
+ }
+
+ return res;
+}
\ No newline at end of file
// IMPORT
// -------------------------------------------------------
import { loadModals, showToast } from './common.js';
-import { serviceCheckAbount, serviceCheckHealth , serviceReloadDNS, serviceReloadDHCP, serviceBackupCreate, serviceBackupList, serviceBackupRestore, deleteBackup} from './services.js';
+import { serviceCheckAbount, serviceCheckHealth , serviceReloadDNS, serviceReloadDHCP, serviceBackupCreate, serviceBackupList, serviceBackupRestore, serviceDownloadBackup, serviceDeleteBackup} from './services.js';
// -------------------------------------------------------
// BACKUP MODAL OPEN/CLOSE
tdSize.textContent = formattedSize;
tdSize.classList.add("text-end");
- // actions (DELETE)
+ // actions
const tdActions = document.createElement("td");
tdActions.classList.add("text-end");
+ // download button
+ const downloadBtn = document.createElement("button");
+ downloadBtn.className = "btn btn-sm btn-outline-primary me-2";
+ downloadBtn.title = "Download backup";
+ downloadBtn.innerHTML = `<i class="bi bi-download text-primary"></i>`;
+ downloadBtn.setAttribute("data-action", "downloadBackup");
+ downloadBtn.setAttribute("data-id", b.name);
+ tdActions.appendChild(downloadBtn);
+ // delete button
const deleteBtn = document.createElement("button");
deleteBtn.className = "btn btn-sm btn-outline-danger";
deleteBtn.title = "Delete backup";
? result.message
: 'Backup completed successfully';
showToast(msg, !result?.partial);
- // Close modal
- if (modal) modal.style.display = 'none';
+ // reload list
+ const data = await serviceBackupList();
+ renderBackupList(data);
} catch (err) {
showToast(err?.message || "Error performing backup", false);
} finally {
: 'Restore completed successfully';
showToast(msg, !result?.partial);
// Close modal
- if (modal) modal.style.display = 'none';
+ //if (modal) modal.style.display = 'none';
} catch (err) {
showToast(err?.message || "Error performing restore", false);
} finally {
btn.disabled = false;
}
},
+ // Download Backup
+ downloadBackup: async (e, el) => {
+ e.stopPropagation();
+
+ const id = el.dataset.id;
+ if (!id) return;
+
+ try {
+ const result = await serviceDownloadBackup(id);
+
+ const msg = (typeof result === 'object' && result?.message)
+ ? result.message
+ : 'Backup downloaded successfully';
+
+ showToast(msg, true);
+
+ } catch (err) {
+ console.error(err);
+ showToast(err?.message || "Error downloading backup", false);
+ }
+ },
// Delete Backup
deleteBackup: async (e, el) => {
if (!confirmDelete) return;
try {
- const result = await deleteBackup(id);
+ const result = await serviceDeleteBackup(id);
const msg = (typeof result === 'object' && result?.message)
? result.message
showToast(err?.message || "Error deleting backup", false);
}
},
-
+ refreshBackupList: async () => {
+ try {
+ const data = await serviceBackupList();
+ const msg = (typeof result === 'object' && result?.message)
+ ? result.message
+ : 'Backup list refreshed successfully';
+ showToast(msg, true);
+ renderBackupList(data);
+ } catch (err) {
+ showToast(err?.message || "Error refreshing backup list", false);
+ }
+ },
openBackupModal, // managed by boostrap
closeBackupModal, // managed by boostrap
// Reload DNS
// import api
-import { apiRequest, apiGet, apiPost } from './api.js';
+import { apiRequest, apiGet, apiPost, apiDownload } from './api.js';
// -------------------------------------------------------
// Check Abount
return false;
}
+// -------------------------------------------------------
+// Download a Backup
+// -------------------------------------------------------
+export async function serviceDownloadBackup(id) {
+ const res = await apiDownload(
+ `/api/backup/download/${encodeURIComponent(id)}`,
+ "Error downloading backup"
+ );
+
+ const blob = await res.blob();
+
+ const url = window.URL.createObjectURL(blob);
+
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = id;
+ document.body.appendChild(a);
+ a.click();
+
+ a.remove();
+ window.URL.revokeObjectURL(url);
+}
+
// -------------------------------------------------------
// Delete a Backup
// -------------------------------------------------------
-export async function deleteBackup(id) {
+export async function serviceDeleteBackup(id) {
const data = await apiPost(
"/api/backup/delete",
{ backup_id: id },
<th>Name</th>
<th>Date</th>
<th class="text-end">Size</th>
- <th style="width: 60px;" class="text-end">Actions</th>
+ <th style="width: 120px;" class="text-end">Actions</th>
</tr>
</thead>
<tbody id="backupList">
<button type="button" class="btn btn-primary" data-action="startRestore">
<i class="bi bi-cloud-download me-1"></i><span class="label">Restore Restore</span>
</button>
+ <button type="button" class="btn btn-primary" data-action="refreshBackupList">
+ <i class="bi bi-arrow-clockwise me-1"></i><span class="label">Refresh List</span>
+ </button>
<button type="button" class="btn btn-outline-primary" data-action="closeBackupModal">
<i class="bi bi-x-circle me-1"></i><span class="label">Cancel</span>
</button>
fastapi
itsdangerous
uvicorn[standard]
+python-multipart