feat: manual backup button
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user