feat: add backups feature

This commit is contained in:
3252a8
2026-05-27 13:53:30 +03:00
parent e90988ea5c
commit 3aede8fe95
55 changed files with 3032 additions and 70 deletions
@@ -0,0 +1,93 @@
import { writable } from "svelte/store";
export function createBackupsStore({ api, onToast, at }) {
const state = writable({
archives: [],
backupDir: "",
backupsLoading: false,
backupsUploading: false,
backupsRestoring: false,
lastRestore: null,
});
async function loadArchives() {
state.update((s) => ({ ...s, backupsLoading: true }));
try {
const data = await api("/admin/backups");
if (data?.ok) {
state.update((s) => ({
...s,
archives: data.archives || [],
backupDir: data.backup_dir || "",
}));
} else {
onToast(data?.message || data?.error || at("backups_load_failed", {}, "Не удалось загрузить бэкапы"));
}
} finally {
state.update((s) => ({ ...s, backupsLoading: false }));
}
}
async function uploadArchive(file) {
if (!file) return null;
state.update((s) => ({ ...s, backupsUploading: true }));
try {
const body = new FormData();
body.append("file", file);
const data = await api("/admin/backups/upload", {
method: "POST",
body,
});
if (data?.ok) {
onToast(at("backups_upload_done", {}, "Архив загружен"));
await loadArchives();
return data.archive || null;
}
onToast(data?.message || data?.error || at("backups_upload_failed", {}, "Не удалось загрузить архив"));
return null;
} finally {
state.update((s) => ({ ...s, backupsUploading: false }));
}
}
async function restoreArchive({ archiveName, restoreDatabase, restoreCompose }) {
const archive_name = String(archiveName || "").trim();
if (!archive_name) {
onToast(at("backups_select_archive", {}, "Выберите архив"));
return false;
}
if (!restoreDatabase && !restoreCompose) {
onToast(at("backups_select_target", {}, "Выберите, что восстановить"));
return false;
}
state.update((s) => ({ ...s, backupsRestoring: true, lastRestore: null }));
try {
const data = await api("/admin/backups/restore", {
method: "POST",
body: JSON.stringify({
archive_name,
restore_database: Boolean(restoreDatabase),
restore_compose: Boolean(restoreCompose),
confirm: true,
}),
});
if (data?.ok) {
state.update((s) => ({ ...s, lastRestore: data.result || null }));
onToast(at("backups_restore_done", {}, "Восстановление завершено"));
return true;
}
onToast(data?.message || data?.error || at("backups_restore_failed", {}, "Не удалось восстановить"));
return false;
} finally {
state.update((s) => ({ ...s, backupsRestoring: false }));
}
}
return {
subscribe: state.subscribe,
loadArchives,
uploadArchive,
restoreArchive,
};
}