]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added possitiblity to upload backups
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 30 May 2026 22:16:14 +0000 (00:16 +0200)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 30 May 2026 22:16:14 +0000 (00:16 +0200)
backend/routes/backup.py
frontend/css/layout.css
frontend/js/api.js
frontend/js/index.js
frontend/js/services.js
frontend/modals.html

index 9166b3441fcb415dc352794e1675e185d01248a3..0fcf2b7ffa9820894c9b649c63a0c1ec093066e6 100644 (file)
@@ -1,11 +1,14 @@
 # backend/routes/backup.py
 
 # import standard modules
+from fastapi import APIRouter, HTTPException, status, UploadFile, File
 from fastapi.responses import Response, JSONResponse, FileResponse
 from pathlib import Path
 from pydantic import BaseModel
+import shutil
 import time
 from typing import Dict, Any
+import zipfile
 
 # Import local modules
 from backend.backup import backup_create, backup_list, backup_restore, backup_delete
@@ -251,3 +254,91 @@ def download_backup(backup_id: str):
         filename=zip_path.name,
         media_type="application/zip"
     )
+
+# ---------------------------------------------------------
+# API: Upload backup
+# ---------------------------------------------------------
+@router.post("/api/backup/upload")
+def upload_backup(file: UploadFile = File(...)):
+
+    # Initialization
+    start_ns = time.monotonic_ns()
+
+    if not file.filename.endswith(".zip"):
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail={
+                    "code": "BACKUP_UPLOAD_FAILED",
+                    "status": "failure",
+                    "message": "Only ZIP files allowed",
+                    "took_ms": took_ms,
+                },
+            )
+
+    backup_dir = Path(settings.BACKUP_PATH)
+    backup_dir.mkdir(parents=True, exist_ok=True)
+
+    # safe filename
+    safe_name = Path(file.filename).name
+    target_path = backup_dir / safe_name
+
+    # prevent overwrite
+    if target_path.exists():
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail={
+                    "code": "BACKUP_UPLOAD_FAILED",
+                    "status": "failure",
+                    "message": "Backup already exists",
+                    "took_ms": took_ms,
+                },
+            )
+
+    # validate ZIP
+    import zipfile
+    try:
+        with zipfile.ZipFile(file.file) as z:
+            if z.testzip() is not None:
+                took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+                raise HTTPException(
+                        status_code=status.HTTP_400_BAD_REQUEST,
+                        detail={
+                            "code": "BACKUP_UPLOAD_FAILED",
+                            "status": "failure",
+                            "message": "Corrupted ZIP file",
+                            "took_ms": took_ms,
+                        },
+                    )
+
+    except zipfile.BadZipFile:
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail={
+                    "code": "BACKUP_UPLOAD_FAILED",
+                    "status": "failure",
+                    "message": "Invalid ZIP file",
+                    "took_ms": took_ms,
+                },
+            )
+
+    # reset pointer after validation
+    file.file.seek(0)
+
+    # save file
+    with target_path.open("wb") as buffer:
+        shutil.copyfileobj(file.file, buffer)
+
+    backup_id = safe_name.replace(".zip", "")
+
+    took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+
+    return {
+        "code": "BACKUP_UPLOADED",
+        "status": "success",
+        "message": "Backup uploaded successfully",
+        "took_ms": took_ms,
+        "backup_id": backup_id,
+    }
index 8c11c6425dcf7ca4d75bcfa25fb23604e5233b7f..2f1df39050416219f2693592779ad851223da100 100644 (file)
@@ -294,13 +294,13 @@ select.form-select:focus,
    Icons
    ================================ */
 .icon {
-    color: var(--icon-color);
+    color: currentColor;
     display: inline-flex;
     align-items: center;
 }
 
 .icon:hover {
-    color: var(--icon-color-hover);
+    color: inherit;
 }
 
 .icon-static {
index bd644280b8e87fe16454377bbc4d4977cc567c59..4bd35b236105e1069dba2b2a8132d2450a912521 100644 (file)
@@ -119,7 +119,7 @@ export async function apiDownload(url, errorPrefix = 'Download error') {
         const msg =
             data?.detail?.message ||
             data?.message ||
-            "Download failed";
+            errorPrefix;
 
         throw new Error(`${errorPrefix}: ${msg}`);
     }
@@ -133,3 +133,40 @@ export async function apiDownload(url, errorPrefix = 'Download error') {
 
     return res;
 }
+
+// -------------------------------------------------------
+// API Upload (multipart/form-data)
+// -------------------------------------------------------
+export async function apiUpload(url, file, errorPrefix = "Upload error") {
+    try {
+        const formData = new FormData();
+        formData.append("file", file);
+
+        const res = await fetch(url, {
+            method: "POST",
+            body: formData
+        });
+
+        if (!res.ok) {
+            let err;
+            try {
+                err = await res.json();
+            } catch {
+                throw new Error(errorPrefix);
+            }
+
+            const msg =
+                err?.detail?.message ||
+                err?.message ||
+                errorPrefix;
+
+            throw new Error(msg);
+        }
+
+        return await res.json();
+
+    } catch (err) {
+        console.error(err);
+        throw new Error(err.message || errorPrefix);
+    }
+}
index 655ec1b92907c261ecbde317e7a1f58ae229aa13..72c024a3ae34fa6915ddc567bb24940784697cfa 100644 (file)
@@ -2,7 +2,7 @@
 // IMPORT
 // -------------------------------------------------------
 import { loadModals, showToast } from './common.js';
-import { serviceCheckAbount, serviceCheckHealth , serviceReloadDNS, serviceReloadDHCP, serviceBackupCreate, serviceBackupList, serviceBackupRestore, serviceDownloadBackup, serviceDeleteBackup} from './services.js';
+import { serviceCheckAbount, serviceCheckHealth , serviceReloadDNS, serviceReloadDHCP, serviceBackupCreate, serviceBackupList, serviceBackupRestore, serviceDeleteBackup, serviceDownloadBackup, serviceUploadBackup } from './services.js';
 
 // -------------------------------------------------------
 // BACKUP MODAL OPEN/CLOSE
@@ -323,27 +323,6 @@ 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) => {
 
@@ -375,16 +354,80 @@ const actionHandlers = {
     },
     refreshBackupList: async () => {
         try {
-            const data = await serviceBackupList();
+            const result = await serviceBackupList();
             const msg = (typeof result === 'object' && result?.message)
                         ? result.message
                         : 'Backup list refreshed successfully';
             showToast(msg, true);
-            renderBackupList(data);
+            renderBackupList(result);
         } catch (err) {
             showToast(err?.message || "Error refreshing backup list", 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);
+        }
+    },
+    // Upload Backup
+    uploadBackup: async (e, el) => {
+        const input = document.getElementById('backupUploadInput');
+        if (!input?.files?.length) {
+            showToast("Select a file first", false);
+            return;
+        }
+
+        const file = input.files[0];
+
+        const icon = el.querySelector('.icon');
+        const originalClass = icon?.className;
+
+        el.disabled = true;
+        if (icon) {
+            icon.className = "spinner-border spinner-border-sm icon";
+        }
+
+        try {
+            const result = await serviceUploadBackup(file);
+
+            const msg = (result?.message)
+                ? result.message
+                : 'Backup uploaded successfully';
+
+            showToast(msg, true);
+
+            console.log("Uploaded backup ID:", result?.backup_id);
+
+            input.value = '';
+
+            const data = await serviceBackupList();
+            renderBackupList(data);
+
+        } catch (err) {
+            showToast(err?.message || "Error uploading backup", false);
+        } finally {
+            if (icon && originalClass) {
+                icon.className = originalClass;
+            }
+            el.disabled = false;
+        }
+    },
     openBackupModal,       // managed by boostrap
     closeBackupModal,      // managed by boostrap
     // Reload DNS
index 7a366867ccd2a875af13986e9326b2ee99bf510a..9a508a4a0d7da2c0ef0f025d64353d14a835a461 100644 (file)
@@ -1,5 +1,5 @@
 // import api
-import { apiRequest, apiGet, apiPost, apiDownload } from './api.js';
+import { apiRequest, apiGet, apiPost, apiDownload, apiUpload } from './api.js';
 
 // -------------------------------------------------------
 // Check Abount
@@ -279,6 +279,19 @@ export async function serviceBackupRestore(id) {
     return false;
 }
 
+// -------------------------------------------------------
+// Delete a Backup
+// -------------------------------------------------------
+export async function serviceDeleteBackup(id) {
+    const data = await apiPost(
+        "/api/backup/delete",
+        { backup_id: id },
+        "Error performing delete"
+    );
+
+    return data?.message ? { message: data.message } : true;
+}
+
 // -------------------------------------------------------
 // Download a Backup
 // -------------------------------------------------------
@@ -303,14 +316,20 @@ export async function serviceDownloadBackup(id) {
 }
 
 // -------------------------------------------------------
-// Delete a Backup
+// Upload a Backup
 // -------------------------------------------------------
-export async function serviceDeleteBackup(id) {
-    const data = await apiPost(
-        "/api/backup/delete",
-        { backup_id: id },
-        "Error performing delete"
+export async function serviceUploadBackup(file) {
+    const data = await apiUpload(
+        "/api/backup/upload",
+        file,
+        "Error uploading backup"
     );
 
-    return data?.message ? { message: data.message } : true;
+    if (data?.status === 'success') {
+        return data?.message
+            ? { message: data.message, backup_id: data.backup_id }
+            : true;
+    }
+
+    return false;
 }
index e3ec26feeb8af66dc9beba2322365e3441b8aa3c..726224c4b0bd119a2396490a0e59889b8813dd7d 100644 (file)
                 </table>
             </div>
 
-            <!-- Hidden input opzionale se vuoi mantenere compatibilità -->
+            <!-- Hidden input opzionale -->
             <input type="hidden" id="restoreBackupId" />
 
             <small class="text-muted">Select one backup to restore.</small>
         </div>
 
+        <div class="modal-body">
+            <label class="form-label">Upload backup</label>
+            <div class="d-flex gap-2">
+                <input type="file" id="backupUploadInput" class="form-control" accept=".zip,.bak,.tar,.gz">
+                <button type="button" class="btn btn-primary" title="Upload backup file" aria-label="Upload backup file" data-action="uploadBackup">
+                    <i class="bi bi-upload icon"></i><span class="label"></span>
+                </button>
+            </div>
+            <small class="text-muted">Select a backup file to upload.</small>
+        </div>
+
         <div class="modal-footer modal-buttons">
             <button type="button" class="btn btn-primary" data-action="startBackup">
                 <i class="bi bi-cloud-upload me-1"></i><span class="label">Create Backup</span>