feat: manual backup button

This commit is contained in:
3252a8
2026-05-27 14:25:37 +03:00
parent 4bd547f06a
commit 4706be53ab
9 changed files with 205 additions and 14 deletions
@@ -10,6 +10,7 @@
import {
CheckCircle2,
Database,
Plus,
RefreshCw,
Server,
TriangleAlert,
@@ -29,6 +30,7 @@
$: ({
archives,
backupDir,
backupsCreating,
backupsLoading,
backupsUploading,
backupsRestoring,
@@ -39,13 +41,16 @@
selectedName = archives[0].name;
}
$: selectedArchive = (archives || []).find((item) => item.name === selectedName) || null;
$: if (selectedArchive && restoreDatabase && !selectedArchive.has_database) restoreDatabase = false;
$: if (selectedArchive && restoreDatabase && !selectedArchive.has_database)
restoreDatabase = false;
$: if (selectedArchive && restoreCompose && !selectedArchive.has_compose) restoreCompose = false;
$: if (selectedArchive && !restoreDatabase && !restoreCompose) {
if (selectedArchive.has_database) restoreDatabase = true;
else if (selectedArchive.has_compose) restoreCompose = true;
}
$: canRestore = Boolean(selectedArchive && (restoreDatabase || restoreCompose) && !backupsRestoring);
$: canRestore = Boolean(
selectedArchive && (restoreDatabase || restoreCompose) && !backupsRestoring && !backupsCreating
);
$: backupHeaders = [
"",
at("backups_col_archive", {}, "Архив"),
@@ -85,6 +90,11 @@
event.currentTarget.value = "";
}
async function createManualBackup() {
const archive = await backupsStore.createBackup();
if (archive?.name) selectedName = archive.name;
}
async function restoreSelected() {
if (!canRestore) return;
const confirmText = at(
@@ -114,6 +124,12 @@
<RefreshCw size={14} />
{at("btn_refresh", {}, "Обновить")}
</AdminButton>
<AdminButton onclick={createManualBackup} disabled={backupsCreating || backupsRestoring}>
<Plus size={14} />
{backupsCreating
? at("backups_creating", {}, "Создание...")
: at("backups_create", {}, "Создать бэкап")}
</AdminButton>
<AdminButton onclick={() => fileInput?.click()} disabled={backupsUploading}>
<Upload size={14} />
{backupsUploading
@@ -222,11 +238,18 @@
aria-label={archive.name}
/>
</td>
<td class="admin-cell-wrap backups-name" data-label={at("backups_col_archive", {}, "Архив")}>
<td
class="admin-cell-wrap backups-name"
data-label={at("backups_col_archive", {}, "Архив")}
>
{archive.name}
</td>
<td data-label={at("backups_col_created", {}, "Создан")}>{fmtDate(archiveDate(archive))}</td>
<td data-label={at("backups_col_size", {}, "Размер")}>{formatSize(archive.size_bytes)}</td>
<td data-label={at("backups_col_created", {}, "Создан")}
>{fmtDate(archiveDate(archive))}</td
>
<td data-label={at("backups_col_size", {}, "Размер")}
>{formatSize(archive.size_bytes)}</td
>
<td data-label={at("backups_col_contents", {}, "Состав")}>
<span class="backups-badges">
{#if archive.has_database}
+37 -3
View File
@@ -5,8 +5,10 @@ export function createBackupsStore({ api, onToast, at }) {
archives: [],
backupDir: "",
backupsLoading: false,
backupsCreating: false,
backupsUploading: false,
backupsRestoring: false,
lastCreated: null,
lastRestore: null,
});
@@ -21,13 +23,38 @@ export function createBackupsStore({ api, onToast, at }) {
backupDir: data.backup_dir || "",
}));
} else {
onToast(data?.message || data?.error || at("backups_load_failed", {}, "Не удалось загрузить бэкапы"));
onToast(
data?.message ||
data?.error ||
at("backups_load_failed", {}, "Не удалось загрузить бэкапы")
);
}
} finally {
state.update((s) => ({ ...s, backupsLoading: false }));
}
}
async function createBackup() {
state.update((s) => ({ ...s, backupsCreating: true, lastCreated: null }));
try {
const data = await api("/admin/backups/create", {
method: "POST",
});
if (data?.ok) {
state.update((s) => ({ ...s, lastCreated: data.result || null }));
onToast(at("backups_create_done", {}, "Бэкап создан"));
await loadArchives();
return data.archive || null;
}
onToast(
data?.message || data?.error || at("backups_create_failed", {}, "Не удалось создать бэкап")
);
return null;
} finally {
state.update((s) => ({ ...s, backupsCreating: false }));
}
}
async function uploadArchive(file) {
if (!file) return null;
state.update((s) => ({ ...s, backupsUploading: true }));
@@ -43,7 +70,11 @@ export function createBackupsStore({ api, onToast, at }) {
await loadArchives();
return data.archive || null;
}
onToast(data?.message || data?.error || at("backups_upload_failed", {}, "Не удалось загрузить архив"));
onToast(
data?.message ||
data?.error ||
at("backups_upload_failed", {}, "Не удалось загрузить архив")
);
return null;
} finally {
state.update((s) => ({ ...s, backupsUploading: false }));
@@ -77,7 +108,9 @@ export function createBackupsStore({ api, onToast, at }) {
onToast(at("backups_restore_done", {}, "Восстановление завершено"));
return true;
}
onToast(data?.message || data?.error || at("backups_restore_failed", {}, "Не удалось восстановить"));
onToast(
data?.message || data?.error || at("backups_restore_failed", {}, "Не удалось восстановить")
);
return false;
} finally {
state.update((s) => ({ ...s, backupsRestoring: false }));
@@ -87,6 +120,7 @@ export function createBackupsStore({ api, onToast, at }) {
return {
subscribe: state.subscribe,
loadArchives,
createBackup,
uploadArchive,
restoreArchive,
};
+29
View File
@@ -475,6 +475,35 @@ export async function mockApi(path, options = {}, context = {}) {
archives: clone(mockBackups),
};
}
if (path === "/admin/backups/create") {
const createdAt = new Date();
const stamp = createdAt
.toISOString()
.replace(/[-:]/g, "")
.replace("T", "-")
.replace(/\.\d{3}Z$/, "+0000");
const archive = {
...mockBackups[0],
name: `remnawave-minishop-backup-${stamp}.zip`,
modified_at: createdAt.toISOString(),
created_at: createdAt.toISOString(),
created_at_local: createdAt.toISOString(),
};
return {
ok: true,
archive,
result: {
archive_name: archive.name,
archive_path: `data/backups/${archive.name}`,
started_at: createdAt.toISOString(),
completed_at: createdAt.toISOString(),
db_dump_included: true,
compose_files_count: archive.compose_files_count,
size_bytes: archive.size_bytes,
warnings: [],
},
};
}
if (path === "/admin/backups/upload") {
return {
ok: true,