]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added button to download backup
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 30 May 2026 17:40:17 +0000 (19:40 +0200)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 30 May 2026 17:40:17 +0000 (19:40 +0200)
backend/routes/backup.py
frontend/index.html
frontend/js/api.js
frontend/js/index.js
frontend/js/services.js
frontend/modals.html
requirements.txt

index 9d457e83fda727cf6faaa371f06bf15df3abfb45..9166b3441fcb415dc352794e1675e185d01248a3 100644 (file)
@@ -1,8 +1,8 @@
 # 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
@@ -10,6 +10,8 @@ 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
 
@@ -87,7 +89,7 @@ def build_operation_response(
     }
 
 # ---------------------------------------------------------
-# API ENDPOINTS
+# API: Create Backup
 # ---------------------------------------------------------
 @router.post("/api/backup/create")
 async def api_backup_create():
@@ -224,3 +226,28 @@ async def api_backup_delete(payload: BackupDeleteRequest):
             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"
+    )
index 3d1544f7cc77c88c00914ebeda72757d9c9c3091..226eddcacee4c03020c9a4be69254e77268761c7 100644 (file)
             </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 &amp; 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>
index 34a1e75a523eeb50b279bc393343989154ddbff6..f28a3b5a4ef6e707e91a5348e07557a5b2ad8e78 100644 (file)
@@ -70,10 +70,16 @@ export async function apiRequest(
     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,
@@ -85,3 +91,45 @@ export function apiPost(url, payload, errorPrefix = 'Request error') {
         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
index 249c2b1fe53bf5ece5f555b8d2664a1834fab34f..655ec1b92907c261ecbde317e7a1f58ae229aa13 100644 (file)
@@ -2,7 +2,7 @@
 // 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
@@ -93,9 +93,18 @@ function renderBackupList(data) {
         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";
@@ -272,8 +281,9 @@ const actionHandlers = {
                         ? 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 {
@@ -305,7 +315,7 @@ const actionHandlers = {
                         : '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 {
@@ -313,6 +323,27 @@ const actionHandlers = {
             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) => {
 
@@ -325,7 +356,7 @@ const actionHandlers = {
         if (!confirmDelete) return;
 
         try {
-            const result = await deleteBackup(id);
+            const result = await serviceDeleteBackup(id);
 
             const msg = (typeof result === 'object' && result?.message)
                 ? result.message
@@ -342,7 +373,18 @@ const actionHandlers = {
             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
index e34a17bedd2f81c6d427e2630a49cef2b6f2c7a8..7a366867ccd2a875af13986e9326b2ee99bf510a 100644 (file)
@@ -1,5 +1,5 @@
 // import api
-import { apiRequest, apiGet, apiPost } from './api.js';
+import { apiRequest, apiGet, apiPost, apiDownload } from './api.js';
 
 // -------------------------------------------------------
 // Check Abount
@@ -279,10 +279,33 @@ export async function serviceBackupRestore(id) {
     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 },
index 0247a83ca236b770009a1434b9ffc8cf0d3513ec..e3ec26feeb8af66dc9beba2322365e3441b8aa3c 100644 (file)
                             <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>
index 1e40b78f890f1b5f8460cd90fe601146f19bbeec..523cb9d51c53863bc883a0cd84630701df05dcd9 100644 (file)
@@ -2,3 +2,4 @@ bcrypt
 fastapi
 itsdangerous
 uvicorn[standard]
+python-multipart