split uploaded and generated backups

This commit is contained in:
Julian Krauser 2025-02-03 11:03:38 +01:00
parent f8fb222ebb
commit 5cb68d92ce
11 changed files with 188 additions and 21 deletions

View file

@ -15,12 +15,17 @@
<option v-for="section in backupSections" :value="section">{{ section }}</option> <option v-for="section in backupSections" :value="section">{{ section }}</option>
</select> </select>
</div> </div>
<div v-if="!partial" class="flex flex-row items-center gap-2">
<input type="checkbox" id="overwrite" checked />
<label for="overwrite">Daten entfernen und importieren</label>
</div>
<br /> <br />
<p class="flex"> <p class="flex">
<InformationCircleIcon class="min-h-5 h-5 min-w-5 w-5" />Je nach Auswahl, werden die entsprechenden <InformationCircleIcon class="min-h-5 h-5 min-w-5 w-5" />Je nach Auswahl, werden die entsprechenden
Bestandsdaten ersetzt. Dadurch können Daten, die seit diesem Backup erstellt wurden, verloren gehen. Bestandsdaten ersetzt. Dadurch können Daten, die seit diesem Backup erstellt wurden, verloren gehen.
</p> </p>
<p class="flex">Das Laden eines vollständigen Backups wird zur Vermeidung von Inkonsistenzen empfohlen.</p>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'"> <button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">
@ -83,6 +88,7 @@ export default defineComponent({
include: Array.from(formData?.sections?.selectedOptions ?? []).map( include: Array.from(formData?.sections?.selectedOptions ?? []).map(
(t) => (t as HTMLOptionElement).value (t) => (t as HTMLOptionElement).value
) as Array<BackupSection>, ) as Array<BackupSection>,
overwrite: !formData?.overwrite.checked,
}; };
this.status = "loading"; this.status = "loading";
this.restoreBackup(restoreBackup) this.restoreBackup(restoreBackup)

View file

@ -18,7 +18,12 @@
type="file" type="file"
ref="fileSelect" ref="fileSelect"
accept="application/JSON" accept="application/JSON"
@change="(e) => uploadFile((e.target as HTMLInputElement)?.files?.[0])" @change="
(e) => {
uploadFile((e.target as HTMLInputElement)?.files?.[0]);
e.target.value = null;
}
"
multiple multiple
/> />
<button primary @click="openFileSelect">Datei auswählen</button> <button primary @click="openFileSelect">Datei auswählen</button>

19
src/router/backupGuard.ts Normal file
View file

@ -0,0 +1,19 @@
import { useBackupStore } from "../stores/admin/user/backup";
export async function setBackupPage(to: any, from: any, next: any) {
const backup = useBackupStore();
let uploadPage = to.name.includes("uploaded");
if (uploadPage) {
backup.page = "uploaded";
backup.backups = [];
} else {
backup.page = "generated";
backup.backups = [];
}
backup.fetchBackups();
next();
}

View file

@ -10,6 +10,7 @@ import { resetMemberStores, setMemberId } from "./memberGuard";
import { resetProtocolStores, setProtocolId } from "./protocolGuard"; import { resetProtocolStores, setProtocolId } from "./protocolGuard";
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard"; import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
import { config } from "../config"; import { config } from "../config";
import { setBackupPage } from "./backupGuard";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -636,10 +637,29 @@ const router = createRouter({
}, },
{ {
path: "backup", path: "backup",
name: "admin-user-backup", name: "admin-user-backup-route",
component: () => import("@/views/admin/user/backup/Backup.vue"), component: () => import("@/views/admin/user/backup/BackupRouting.vue"),
meta: { type: "read", section: "user", module: "backup" }, meta: { type: "read", section: "user", module: "backup" },
beforeEnter: [abilityAndNavUpdate], beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-user-backup",
redirect: { name: "admin-user-backup-generated" },
},
{
path: "generated",
name: "admin-user-backup-generated",
component: () => import("@/views/admin/user/backup/GeneratedBackup.vue"),
beforeEnter: [setBackupPage],
},
{
path: "uploads",
name: "admin-user-backup-uploaded",
component: () => import("@/views/admin/user/backup/UploadedBackup.vue"),
beforeEnter: [setBackupPage],
},
],
}, },
], ],
}, },

View file

@ -8,13 +8,14 @@ export const useBackupStore = defineStore("backup", {
return { return {
backups: [] as Array<string>, backups: [] as Array<string>,
loading: null as null | "loading" | "success" | "failed", loading: null as null | "loading" | "success" | "failed",
page: "generated" as "generated" | "uploaded",
}; };
}, },
actions: { actions: {
fetchBackups() { fetchBackups() {
this.loading = "loading"; this.loading = "loading";
http http
.get("/admin/backup") .get(`/admin/backup/${this.page}`)
.then((result) => { .then((result) => {
this.backups = result.data; this.backups = result.data;
this.loading = "success"; this.loading = "success";
@ -24,16 +25,16 @@ export const useBackupStore = defineStore("backup", {
}); });
}, },
fetchBackupById(filename: string): Promise<AxiosResponse<any, any>> { fetchBackupById(filename: string): Promise<AxiosResponse<any, any>> {
return http.get(`/admin/backup/${filename}`); return http.get(`/admin/backup/${this.page}/${filename}`);
},
async restoreBackup(backup: BackupRestoreViewModel): Promise<AxiosResponse<any, any>> {
return await http.post(`/admin/backup/${this.page}/restore`, backup);
}, },
async triggerBackupCreate(): Promise<AxiosResponse<any, any>> { async triggerBackupCreate(): Promise<AxiosResponse<any, any>> {
const result = await http.post("/admin/backup"); const result = await http.post("/admin/backup");
this.fetchBackups(); this.fetchBackups();
return result; return result;
}, },
async restoreBackup(backup: BackupRestoreViewModel): Promise<AxiosResponse<any, any>> {
return await http.post("/admin/backup/restore", backup);
},
async uploadBackup(file: File): Promise<AxiosResponse<any, any>> { async uploadBackup(file: File): Promise<AxiosResponse<any, any>> {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);

View file

@ -9,7 +9,8 @@ export interface MembershipViewModel {
export interface MembershipStatisticsViewModel { export interface MembershipStatisticsViewModel {
durationInDays: number; durationInDays: number;
durationInYears: string; durationInYears: number;
exactDuration: string;
status: string; status: string;
statusId: number; statusId: number;
memberId: string; memberId: string;

View file

@ -4,4 +4,5 @@ export interface BackupRestoreViewModel {
filename: string; filename: string;
partial: boolean; partial: boolean;
include: Array<BackupSection>; include: Array<BackupSection>;
overwrite: boolean;
} }

View file

@ -34,7 +34,7 @@
> >
<p> <p>
{{ stat.status }} für gesamt {{ stat.durationInDays }} Tage {{ stat.status }} für gesamt {{ stat.durationInDays }} Tage
<span class="whitespace-nowrap"> ~> {{ stat.durationInYears.replace("_", "") }} Jahre</span> <span class="whitespace-nowrap"> ~> {{ stat.exactDuration }}</span>
</p> </p>
</div> </div>
</div> </div>

View file

@ -6,17 +6,27 @@
</div> </div>
</template> </template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-2 grow px-7 overflow-hidden">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col grow gap-2 overflow-hidden">
<BackupListItem v-for="backup in backups" :key="backup" :backup="backup" /> <div class="w-full flex flex-row max-lg:flex-wrap justify-center">
<RouterLink
v-for="tab in tabs"
:key="tab.route"
v-slot="{ isActive }"
:to="{ name: tab.route }"
class="w-1/2 p-0.5 first:pl-0 last:pr-0"
>
<p
:class="[
'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-none',
isActive ? 'bg-red-200 shadow border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
]"
>
{{ tab.title }}
</p>
</RouterLink>
</div> </div>
<div class="flex flex-row gap-4"> <RouterView />
<button v-if="can('create', 'user', 'backup')" primary class="!w-fit" @click="openCreateModal">
Backup erstellen
</button>
<button v-if="can('create', 'user', 'backup')" primary class="!w-fit" @click="openUploadModal">
Backup hochladen
</button>
</div> </div>
</div> </div>
</template> </template>
@ -35,6 +45,14 @@ import { useAbilityStore } from "@/stores/ability";
<script lang="ts"> <script lang="ts">
export default defineComponent({ export default defineComponent({
data() {
return {
tabs: [
{ route: "admin-user-backup-generated", title: "Erstellt" },
{ route: "admin-user-backup-uploaded", title: "Uploads" },
],
};
},
computed: { computed: {
...mapState(useBackupStore, ["backups"]), ...mapState(useBackupStore, ["backups"]),
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),

View file

@ -0,0 +1,48 @@
<template>
<div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
<BackupListItem v-for="backup in backups" :key="backup" :backup="backup" />
</div>
<div class="flex flex-row gap-4">
<button v-if="can('create', 'user', 'backup')" primary class="!w-fit" @click="openCreateModal">
Backup erstellen
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapState, mapActions } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useBackupStore } from "@/stores/admin/user/backup";
import BackupListItem from "@/components/admin/user/backup/BackupListItem.vue";
import { useModalStore } from "@/stores/modal";
import { useAbilityStore } from "@/stores/ability";
</script>
<script lang="ts">
export default defineComponent({
computed: {
...mapState(useBackupStore, ["backups"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchBackups();
},
methods: {
...mapActions(useBackupStore, ["fetchBackups"]),
...mapActions(useModalStore, ["openModal"]),
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/user/backup/CreateBackupModal.vue")))
);
},
openUploadModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/user/backup/UploadBackupModal.vue")))
);
},
},
});
</script>

View file

@ -0,0 +1,48 @@
<template>
<div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
<BackupListItem v-for="backup in backups" :key="backup" :backup="backup" />
</div>
<div class="flex flex-row gap-4">
<button v-if="can('create', 'user', 'backup')" primary class="!w-fit" @click="openUploadModal">
Backup hochladen
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapState, mapActions } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useBackupStore } from "@/stores/admin/user/backup";
import BackupListItem from "@/components/admin/user/backup/BackupListItem.vue";
import { useModalStore } from "@/stores/modal";
import { useAbilityStore } from "@/stores/ability";
</script>
<script lang="ts">
export default defineComponent({
computed: {
...mapState(useBackupStore, ["backups"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchBackups();
},
methods: {
...mapActions(useBackupStore, ["fetchBackups"]),
...mapActions(useModalStore, ["openModal"]),
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/user/backup/CreateBackupModal.vue")))
);
},
openUploadModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/user/backup/UploadBackupModal.vue")))
);
},
},
});
</script>