# 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
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,
+ }
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 {
const msg =
data?.detail?.message ||
data?.message ||
- "Download failed";
+ errorPrefix;
throw new Error(`${errorPrefix}: ${msg}`);
}
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);
+ }
+}
// 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
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) => {
},
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
// import api
-import { apiRequest, apiGet, apiPost, apiDownload } from './api.js';
+import { apiRequest, apiGet, apiPost, apiDownload, apiUpload } from './api.js';
// -------------------------------------------------------
// Check Abount
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
// -------------------------------------------------------
}
// -------------------------------------------------------
-// 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;
}
</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>