From 90424bff3a029cfc951c4236fec534d5aa004cf4 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Mon, 29 Jun 2026 23:15:19 +0200 Subject: [PATCH] feature: download exoscale storage --- .../hooks/exoscaleMaintenance.ts | 59 + src/app/admin/dev-management/page.tsx | 42 +- src/app/i18n/translations/kk.ts | 2144 +++++++++++++++++ src/app/i18n/translations/sl.ts | 2144 +++++++++++++++++ 4 files changed, 4387 insertions(+), 2 deletions(-) create mode 100644 src/app/i18n/translations/kk.ts create mode 100644 src/app/i18n/translations/sl.ts diff --git a/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts b/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts index 4f25d6b..bca5f46 100644 --- a/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts +++ b/src/app/admin/dev-management/hooks/exoscaleMaintenance.ts @@ -210,3 +210,62 @@ export async function listGhostDirectories(): Promise<{ ok: boolean; data?: Ghos return { ok: false, message: 'Network error while loading ghost directories.' }; } } + +export async function downloadExoscaleArchive(): Promise<{ ok: boolean; fileName?: string; message?: string }> { + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + const url = `${BASE_URL}/api/admin/dev/exoscale/download-archive`; + + log('🧪 Dev Exoscale: GET', url); + try { + const res = await authFetch(url, { method: 'GET', headers: { Accept: 'application/zip' } }); + + if (res.status === 401) { + return { ok: false, message: 'Unauthorized. Please log in.' }; + } + + let body: any = null; + if (res.status === 403 || !res.ok) { + try { + body = await res.clone().json(); + } catch { + try { + body = await res.text(); + } catch { + body = null; + } + } + } + + if (res.status === 403) { + return { + ok: false, + message: typeof body === 'string' ? body : body?.error || 'Forbidden. Admin access required.' + }; + } + + if (!res.ok) { + return { + ok: false, + message: typeof body === 'string' ? body : body?.error || 'Failed to download Exoscale archive.' + }; + } + + const blob = await res.blob(); + const contentDisposition = res.headers.get('content-disposition') || ''; + const fileNameMatch = contentDisposition.match(/filename="?([^";]+)"?/i); + const fileName = fileNameMatch?.[1] || `exoscale-storage-${Date.now()}.zip`; + const downloadUrl = window.URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = downloadUrl; + anchor.download = fileName; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + window.URL.revokeObjectURL(downloadUrl); + + return { ok: true, fileName }; + } catch (e: any) { + log('āŒ Dev Exoscale: archive download error', e?.message || e); + return { ok: false, message: 'Network error while downloading Exoscale archive.' }; + } +} diff --git a/src/app/admin/dev-management/page.tsx b/src/app/admin/dev-management/page.tsx index e565612..08e9100 100644 --- a/src/app/admin/dev-management/page.tsx +++ b/src/app/admin/dev-management/page.tsx @@ -7,11 +7,11 @@ import React from 'react' import Header from '../../components/nav/Header' import Footer from '../../components/Footer' import PageTransitionEffect from '../../components/animation/pageTransitionEffect' -import { CommandLineIcon, PlayIcon, TrashIcon, ExclamationTriangleIcon, ArrowUpTrayIcon, WrenchScrewdriverIcon, FolderOpenIcon, ArrowPathIcon } from '@heroicons/react/24/outline' +import { CommandLineIcon, PlayIcon, TrashIcon, ExclamationTriangleIcon, ArrowUpTrayIcon, WrenchScrewdriverIcon, FolderOpenIcon, ArrowPathIcon, ArrowDownTrayIcon } from '@heroicons/react/24/outline' import useAuthStore from '../../store/authStore' import { useRouter } from 'next/navigation' import { importSqlDump, SqlExecutionData, SqlExecutionMeta } from './hooks/executeSql' -import { createFolderStructure, listFolderStructureIssues, listLooseFiles, listGhostDirectories, moveLooseFilesToContract, FixResult, FolderStructureIssueUser, GhostDirectory, LooseFileUser } from './hooks/exoscaleMaintenance' +import { createFolderStructure, listFolderStructureIssues, listLooseFiles, listGhostDirectories, moveLooseFilesToContract, downloadExoscaleArchive, FixResult, FolderStructureIssueUser, GhostDirectory, LooseFileUser } from './hooks/exoscaleMaintenance' export default function DevManagementPage() { const { t } = useTranslation(); @@ -70,6 +70,8 @@ export default function DevManagementPage() { const [structureStatus, setStructureStatus] = React.useState('') const [looseStatus, setLooseStatus] = React.useState('') const [ghostStatus, setGhostStatus] = React.useState('') + const [archiveLoading, setArchiveLoading] = React.useState(false) + const [archiveStatus, setArchiveStatus] = React.useState('') const formatNow = () => new Date().toLocaleString() @@ -153,6 +155,23 @@ export default function DevManagementPage() { } } + const runArchiveDownload = async () => { + setExoscaleError('') + setArchiveLoading(true) + setArchiveStatus('Preparing ZIP export...') + try { + const res = await downloadExoscaleArchive() + if (!res.ok) { + setExoscaleError(res.message || 'Failed to download Exoscale archive.') + setArchiveStatus('') + return + } + setArchiveStatus(`Download started: ${res.fileName || formatNow()}`) + } finally { + setArchiveLoading(false) + } + } + const runCreateStructure = async (userId?: number) => { setExoscaleError('') if (userId) { @@ -283,6 +302,25 @@ export default function DevManagementPage() {
+
+
+
+

Exoscale Backup Export

+

Ladet alle Dateien aus dem Exoscale Storage und liefert sie als ZIP-Download aus.

+ {archiveStatus && ( +
{archiveStatus}
+ )} +
+ +
+
+