mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-09-11 16:29:07 +02:00
🔨 Add cron setting menu
This commit is contained in:
parent
446e89a6c7
commit
1324c0b3d1
@ -13,7 +13,7 @@ const __dirname = path.dirname(__filename);
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: "http://localhost:5173"
|
||||
origin: true
|
||||
}))
|
||||
|
||||
app.set("port", process.env.PORT || 3000);
|
||||
|
@ -1,17 +1,34 @@
|
||||
import express from "express";
|
||||
import * as haOsService from "../services/homeAssistantService.js"
|
||||
import * as haOsService from "../services/homeAssistantService.js";
|
||||
|
||||
const homeAssistantRouter = express.Router();
|
||||
|
||||
homeAssistantRouter.get("/backups/", (req, res, next) => {
|
||||
haOsService.getBackups()
|
||||
.then((value)=>{
|
||||
haOsService
|
||||
.getBackups()
|
||||
.then((value) => {
|
||||
res.json(value.body.data.backups);
|
||||
}).catch((reason)=>{
|
||||
})
|
||||
.catch((reason) => {
|
||||
res.status(500);
|
||||
res.json(reason);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
homeAssistantRouter.get("/addons", (req, res, next) => {
|
||||
haOsService
|
||||
.getAddonList()
|
||||
.then((value) => {
|
||||
res.json(value.body.data);
|
||||
})
|
||||
.catch((reason) => {
|
||||
res.status(500);
|
||||
res.json(reason);
|
||||
});
|
||||
});
|
||||
|
||||
export default homeAssistantRouter;
|
||||
homeAssistantRouter.get("/folders", (req, res, next) => {
|
||||
res.json(haOsService.getFolderList());
|
||||
})
|
||||
|
||||
export default homeAssistantRouter;
|
||||
|
@ -86,30 +86,6 @@ function getAddonToBackup(addons: AddonModel[]) {
|
||||
return slugs;
|
||||
}
|
||||
|
||||
function getFolderList() {
|
||||
return [
|
||||
{
|
||||
name: "Home Assistant configuration",
|
||||
slug: "homeassistant",
|
||||
},
|
||||
{
|
||||
name: "SSL",
|
||||
slug: "ssl",
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
slug: "share",
|
||||
},
|
||||
{
|
||||
name: "Media",
|
||||
slug: "media",
|
||||
},
|
||||
{
|
||||
name: "Local add-ons",
|
||||
slug: "addons/local",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getFolderToBackup() {
|
||||
const excluded_folder = settingsTools.getSettings().exclude_folder;
|
||||
@ -434,6 +410,32 @@ function startAddons(addonSlugs: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getFolderList() {
|
||||
return [
|
||||
{
|
||||
name: "Home Assistant configuration",
|
||||
slug: "homeassistant",
|
||||
},
|
||||
{
|
||||
name: "SSL",
|
||||
slug: "ssl",
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
slug: "share",
|
||||
},
|
||||
{
|
||||
name: "Media",
|
||||
slug: "media",
|
||||
},
|
||||
{
|
||||
name: "Local add-ons",
|
||||
slug: "addons/local",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
function publish_state(state: Status) {
|
||||
// let data_error_sensor = {
|
||||
// state: state.status == "error" ? "on" : "off",
|
||||
@ -501,7 +503,6 @@ function publish_state(state: Status) {
|
||||
export {
|
||||
getVersion,
|
||||
getAddonList,
|
||||
getFolderList,
|
||||
getBackups,
|
||||
downloadSnapshot,
|
||||
createNewBackup,
|
||||
|
@ -59,6 +59,7 @@ export function getWebdavDefaultConfig(): WebdavConfig {
|
||||
password: "",
|
||||
backupDir: default_root,
|
||||
allowSelfSignedCerts: false,
|
||||
chunckedUpload: false,
|
||||
webdavEndpoint: {
|
||||
type: WebdavEndpointType.NEXTCLOUD,
|
||||
},
|
||||
|
@ -47,8 +47,8 @@ const backupConfigValidation = {
|
||||
webdav: Joi.object(AutoCleanConfig).required(),
|
||||
}).required(),
|
||||
exclude: Joi.object({
|
||||
addon: Joi.array().items(Joi.string().not().empty()),
|
||||
folder: Joi.array().items(Joi.string().not().empty()),
|
||||
addon: Joi.array().items(Joi.string().not().empty()).required(),
|
||||
folder: Joi.array().items(Joi.string().not().empty()).required(),
|
||||
}).required(),
|
||||
autoStopAddon: Joi.array().items(Joi.string().not().empty()),
|
||||
password: Joi.object({
|
||||
|
@ -10,6 +10,7 @@ export interface WebdavConfig {
|
||||
password: string;
|
||||
backupDir: string;
|
||||
allowSelfSignedCerts: boolean
|
||||
chunckedUpload: boolean
|
||||
webdavEndpoint: {
|
||||
type: WebdavEndpointType;
|
||||
customEndpoint?: string;
|
||||
|
@ -3,11 +3,12 @@ import { WebdavEndpointType } from "./webdavConfig.js";
|
||||
|
||||
|
||||
const WebdavConfigValidation = {
|
||||
url: Joi.string().not().empty().uri().required(),
|
||||
username: Joi.string().not().empty().required(),
|
||||
password: Joi.string().not().empty().required(),
|
||||
backupDir: Joi.string().required(),
|
||||
allowSelfSignedCerts: Joi.boolean().required(),
|
||||
url: Joi.string().not().empty().uri().required().label("Url"),
|
||||
username: Joi.string().not().empty().label("Username"),
|
||||
password: Joi.string().not().empty().label("Password"),
|
||||
backupDir: Joi.string().required().label("Backup directory"),
|
||||
allowSelfSignedCerts: Joi.boolean().label("Allow self signed certificate"),
|
||||
chunckedUpload: Joi.boolean().required().label("Chuncked upload"),
|
||||
webdavEndpoint: Joi.object({
|
||||
type: Joi.string().valid(WebdavEndpointType.CUSTOM, WebdavEndpointType.NEXTCLOUD).required(),
|
||||
customEndpoint: Joi.alternatives().conditional("type", {
|
||||
@ -15,7 +16,7 @@ const WebdavConfigValidation = {
|
||||
then: Joi.string().not().empty().required,
|
||||
otherwise: Joi.disallow()
|
||||
})
|
||||
}).required()
|
||||
}).required().label("Webdav endpoint"),
|
||||
}
|
||||
|
||||
export default WebdavConfigValidation;
|
@ -11,33 +11,35 @@
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "5.9.55",
|
||||
"@mdi/js": "^7.0.96",
|
||||
"@types/luxon": "^3.0.1",
|
||||
"@mdi/font": "7.1.96",
|
||||
"@mdi/js": "^7.1.96",
|
||||
"@types/luxon": "^3.2.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"ky": "^0.31.4",
|
||||
"luxon": "^3.0.4",
|
||||
"pinia": "^2.0.23",
|
||||
"luxon": "^3.2.1",
|
||||
"pinia": "^2.0.28",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.2.40",
|
||||
"vuetify": "3.0.1",
|
||||
"webfontloader": "^1.0.0"
|
||||
"uuid": "^9.0.0",
|
||||
"vue": "^3.2.45",
|
||||
"vuetify": "3.1.1",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@types/node": "^16.11.65",
|
||||
"@types/webfontloader": "^1.0.0",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@types/node": "^16.18.11",
|
||||
"@types/webfontloader": "^1.6.35",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-plugin-vue": "^9.6.0",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.2",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^3.1.7",
|
||||
"vite-plugin-vuetify": "^1.0.0-alpha.12",
|
||||
"vite": "^3.2.5",
|
||||
"vite-plugin-vuetify": "^1.0.1",
|
||||
"vue-tsc": "1.0.7"
|
||||
},
|
||||
"packageManager": "pnpm@7.12.1"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,8 @@
|
||||
<navbar-component></navbar-component>
|
||||
<message-bar></message-bar>
|
||||
<webdav-settings-menu></webdav-settings-menu>
|
||||
<v-main class="mx-12">
|
||||
<BackupConfigMenu></BackupConfigMenu>
|
||||
<v-main class="mx-12">
|
||||
<v-row>
|
||||
<v-col cols="6" offset="6">
|
||||
<cloud-list></cloud-list>
|
||||
@ -18,6 +19,7 @@ import NavbarComponent from "./components/NavbarComponent.vue";
|
||||
import MessageBar from "./components/MessageBar.vue";
|
||||
import WebdavSettingsMenu from "./components/settings/WebdavConfigMenu.vue";
|
||||
import CloudList from "./components/cloud/CloudList.vue";
|
||||
import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue";
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -6,11 +6,15 @@
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card variant="outlined" elevation="5" color="grey-darken-2">
|
||||
<v-card-title class="text-center text-white">Auto</v-card-title>
|
||||
<v-card variant="elevated" elevation="7" height="100%">
|
||||
<v-card-title
|
||||
class="text-center text-white bg-light-blue-darken-4"
|
||||
>
|
||||
Auto
|
||||
</v-card-title>
|
||||
<v-divider color="grey-darken-3"></v-divider>
|
||||
<v-card-text class="pa-0">
|
||||
<v-list variant="tonal" class="pa-0">
|
||||
<v-list class="pa-0">
|
||||
<v-list-item
|
||||
v-if="autoBackups.length == 0"
|
||||
class="text-center text-subtitle-2 text-disabled"
|
||||
@ -31,11 +35,14 @@
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card variant="outlined" elevation="5" color="grey-darken-2">
|
||||
<v-card-title class="text-center text-white">Manual</v-card-title>
|
||||
<v-card variant="elevated" elevation="7" height="100%">
|
||||
<v-card-title
|
||||
class="text-center text-white bg-light-blue-darken-4"
|
||||
>Manual</v-card-title
|
||||
>
|
||||
<v-divider color="grey-darken-3"></v-divider>
|
||||
<v-card-text class="pa-0">
|
||||
<v-list variant="tonal" class="pa-0">
|
||||
<v-list class="pa-0">
|
||||
<v-list-item
|
||||
v-if="manualBackups.length == 0"
|
||||
class="text-center text-subtitle-2 text-disabled"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-divider v-if="index != 0" color="grey-darken-3"></v-divider>
|
||||
<v-list-item>
|
||||
<v-list-item class="bg-grey-darken-3">
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-scroll-x-transition>
|
||||
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<v-card variant="elevated" elevation="7" height="100%">
|
||||
<v-card-title class="text-center text-white bg-light-blue-darken-4">
|
||||
Addons
|
||||
</v-card-title>
|
||||
<v-card-text class="text-white px-2 py-1">
|
||||
<div v-if="loading" class="d-flex justify-center">
|
||||
<v-progress-circular indeterminate color="orange"></v-progress-circular>
|
||||
</div>
|
||||
<v-checkbox
|
||||
v-else
|
||||
v-for="addon in addons"
|
||||
v-model="invertedAddons"
|
||||
:key="addon.slug"
|
||||
:label="addon.name"
|
||||
:value="addon.slug"
|
||||
:loading="loading"
|
||||
hide-details="auto"
|
||||
color="orange"
|
||||
density="compact"
|
||||
>
|
||||
</v-checkbox>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBackupConfigStore } from "@/stores/backupConfig";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { watch } from "vue";
|
||||
|
||||
defineProps<{ loading: boolean }>();
|
||||
const backupConfigStore = useBackupConfigStore();
|
||||
|
||||
const { data, addons, invertedAddons } = storeToRefs(backupConfigStore);
|
||||
watch(invertedAddons, () => {
|
||||
data.value.exclude.addon = [];
|
||||
for (const addon of addons.value) {
|
||||
if (!invertedAddons.value.includes(addon.slug)) {
|
||||
data.value.exclude.addon.push(addon.slug);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<v-card variant="elevated" elevation="7">
|
||||
<v-card-title class="bg-light-blue-darken-4">
|
||||
<v-row dense>
|
||||
<v-col cols="10" offset="1" class="text-center text-white">
|
||||
Auto Backup
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-btn
|
||||
class="float-right"
|
||||
size="32"
|
||||
color="green"
|
||||
rounded
|
||||
@click="backupConfigStore.addEmptyCron()"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0 bg-blue-grey-darken-4">
|
||||
<v-expansion-panels variant="inset" v-model="expansionPanelModel">
|
||||
<v-expansion-panel v-for="cron in data.cron" :key="cron.id">
|
||||
<v-expansion-panel-title>
|
||||
<template v-slot:default="{ expanded }">
|
||||
{{ CronModeFriendly[cron.mode] }}
|
||||
<v-spacer></v-spacer>
|
||||
<v-fade-transition leave-absolute>
|
||||
<v-chip
|
||||
v-if="!expanded && cron.monthDay != undefined"
|
||||
append-icon="mdi-calendar"
|
||||
size="small"
|
||||
>
|
||||
{{ cron.monthDay }}
|
||||
</v-chip>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition leave-absolute>
|
||||
<v-chip
|
||||
v-if="!expanded && cron.weekday != undefined"
|
||||
append-icon="mdi-calendar"
|
||||
size="small"
|
||||
class="ml-3"
|
||||
>
|
||||
{{ weekdayFriendly[cron.weekday] }}
|
||||
</v-chip>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition leave-absolute>
|
||||
<v-chip
|
||||
v-if="!expanded && cron.hour"
|
||||
append-icon="mdi-clock"
|
||||
size="small"
|
||||
class="mx-3"
|
||||
>
|
||||
{{ cron.hour }}
|
||||
</v-chip>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition leave-absolute>
|
||||
<v-chip
|
||||
v-if="!expanded && cron.custom"
|
||||
append-icon="mdi-clock-edit"
|
||||
size="small"
|
||||
class="mx-3"
|
||||
>{{ cron.custom }}</v-chip
|
||||
>
|
||||
</v-fade-transition>
|
||||
</template>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1 text-medium-emphasis">
|
||||
Mode <v-icon class="float-right">mdi-cog</v-icon>
|
||||
</div>
|
||||
<v-select
|
||||
:items="
|
||||
Object.entries(CronMode).map((value) => {
|
||||
return { title: value[0], value: value[1] };
|
||||
})
|
||||
"
|
||||
v-model="cron.mode"
|
||||
hide-details="auto"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
color="orange"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="cron.mode == CronMode.Weekly" dense>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1 text-medium-emphasis">
|
||||
Day of week <v-icon class="float-right">mdi-calendar</v-icon>
|
||||
</div>
|
||||
<v-select
|
||||
v-model="cron.weekday"
|
||||
:items="
|
||||
weekdayFriendly.map((value, index) => {
|
||||
return { title: value, value: index };
|
||||
})
|
||||
"
|
||||
hide-details="auto"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
color="orange"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="cron.mode == CronMode.Monthly" dense>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1 text-medium-emphasis">
|
||||
Day of month <v-icon class="float-right">mdi-calendar</v-icon>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="cron.monthDay"
|
||||
type="number"
|
||||
hide-details="auto"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
color="orange"
|
||||
min="1"
|
||||
max="28"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="cron.mode != CronMode.Custom" dense>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1 text-medium-emphasis">
|
||||
Hour <v-icon class="float-right">mdi-clock</v-icon>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="cron.hour"
|
||||
type="time"
|
||||
hide-details="auto"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
color="orange"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="cron.mode == CronMode.Custom" dense>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1 text-medium-emphasis">
|
||||
Custom CRON
|
||||
<v-icon class="float-right">mdi-clock-edit</v-icon>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="cron.custom"
|
||||
hide-details="auto"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
color="orange"
|
||||
min="1"
|
||||
max="28"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col align-self="center" class="text-center">
|
||||
<v-btn color="red" @click="removeCron(cron.id)"
|
||||
><v-icon>mdi-trash-can</v-icon></v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBackupConfigStore } from "@/stores/backupConfig";
|
||||
import {
|
||||
CronMode,
|
||||
CronModeFriendly,
|
||||
weekdayFriendly,
|
||||
} from "@/types/backupConfig";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
const expansionPanelModel = ref(undefined);
|
||||
|
||||
defineProps<{ loading: boolean }>();
|
||||
const backupConfigStore = useBackupConfigStore();
|
||||
|
||||
function removeCron(id: string) {
|
||||
expansionPanelModel.value = undefined;
|
||||
backupConfigStore.removeCron(id);
|
||||
}
|
||||
|
||||
const { data } = storeToRefs(backupConfigStore);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<v-card variant="elevated" elevation="7" height="100%">
|
||||
<v-card-title class="text-center text-white bg-light-blue-darken-4">
|
||||
Folders
|
||||
</v-card-title>
|
||||
<v-card-text class="text-white px-2 py-1">
|
||||
<div v-if="loading" class="d-flex justify-center">
|
||||
<v-progress-circular indeterminate color="orange"></v-progress-circular>
|
||||
</div>
|
||||
<v-checkbox
|
||||
v-else
|
||||
v-for="folder in folders"
|
||||
v-model="invertedFolders"
|
||||
:key="folder.slug"
|
||||
:label="folder.name"
|
||||
:value="folder.slug"
|
||||
:loading="loading"
|
||||
hide-details="auto"
|
||||
color="orange"
|
||||
density="compact"
|
||||
>
|
||||
</v-checkbox>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBackupConfigStore } from "@/stores/backupConfig";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { watch } from "vue";
|
||||
|
||||
defineProps<{ loading: boolean }>();
|
||||
const backupConfigStore = useBackupConfigStore();
|
||||
|
||||
const { data, folders, invertedFolders } = storeToRefs(backupConfigStore);
|
||||
watch(invertedFolders, () => {
|
||||
data.value.exclude.folder = [];
|
||||
for (const folder of folders.value) {
|
||||
if (!invertedFolders.value.includes(folder.slug)) {
|
||||
data.value.exclude.folder.push(folder.slug);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<v-form class="mx-4">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1 text-medium-emphasis">Naming template</div>
|
||||
<!-- <v-text-field
|
||||
placeholder="{type}-{ha_version}-{date}_{hour}"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-tag"
|
||||
hide-details="auto"
|
||||
v-model="data.config.nameTemplate"
|
||||
:error-messages="errors.nameTemplate"
|
||||
:loading="loading"
|
||||
color="orange"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="outlined"
|
||||
class="mt-n2"
|
||||
height="auto"
|
||||
href="https://github.com/Sebclem/hassio-nextcloud-backup/blob/master/nextcloud_backup/naming_template.md"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon icon="mdi-help-circle-outline"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field> -->
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<BackupConfigFolder :loading="loading"></BackupConfigFolder>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<BackupConfigAddon :loading="loading"></BackupConfigAddon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<v-sheet border elevation="5" rounded class="py-1">
|
||||
<h2>Automation</h2>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<BackupConfigAutoBackup :loading="loading"></BackupConfigAutoBackup>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
import { useConfigForm } from "@/composable/ConfigForm";
|
||||
import { saveBackupConfig } from "@/services/configService";
|
||||
import { useBackupConfigStore } from "@/stores/backupConfig";
|
||||
import { CronMode } from "@/types/backupConfig";
|
||||
import { storeToRefs } from "pinia";
|
||||
import BackupConfigAddon from "./BackupConfig/BackupConfigAddon.vue";
|
||||
import BackupConfigFolder from "./BackupConfig/BackupConfigFolder.vue";
|
||||
import BackupConfigAutoBackup from "./BackupConfig/BackupConfigAutoBackup.vue";
|
||||
|
||||
const backupConfigStore = useBackupConfigStore();
|
||||
const { data, folders, invertedFolders } = storeToRefs(backupConfigStore);
|
||||
const errors = ref({
|
||||
nameTemplate: [],
|
||||
username: [],
|
||||
password: [],
|
||||
backupDir: [],
|
||||
allowSelfSignedCerts: [],
|
||||
type: [],
|
||||
customEndpoint: [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "success"): void;
|
||||
(e: "fail"): void;
|
||||
(e: "loaded"): void;
|
||||
(e: "loading"): void;
|
||||
}>();
|
||||
|
||||
const { save, loading } = useConfigForm(
|
||||
saveBackupConfig,
|
||||
backupConfigStore.loadAll,
|
||||
data,
|
||||
errors,
|
||||
emit
|
||||
);
|
||||
defineExpose({ save });
|
||||
</script>
|
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialogStatusStore.backup"
|
||||
persistent
|
||||
:width="width"
|
||||
:fullscreen="isFullScreen"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-center">Backup Settings</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<backup-config-form
|
||||
ref="form"
|
||||
@fail="saving = false"
|
||||
@success="saved"
|
||||
@loaded="loading = false"
|
||||
@loading="loading = true"
|
||||
></backup-config-form>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="dialogStatusStore.backup = false"
|
||||
:disabled="saving"
|
||||
>Cancel</v-btn
|
||||
>
|
||||
<v-btn color="success" @click="save()" :loading="saveLoading"
|
||||
>Save</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialogStatusStore } from "@/stores/dialogStatus";
|
||||
import { computed, ref } from "vue";
|
||||
import { useMenuSize } from "@/composable/menuSize";
|
||||
import BackupConfigForm from "./BackupConfigForm.vue";
|
||||
|
||||
const dialogStatusStore = useDialogStatusStore();
|
||||
const form = ref<InstanceType<typeof BackupConfigForm> | null>(null);
|
||||
const { width, isFullScreen } = useMenuSize();
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
|
||||
let saveLoading = computed(() => {
|
||||
return saving.value || loading.value;
|
||||
});
|
||||
|
||||
function save() {
|
||||
saving.value = true;
|
||||
form.value?.save();
|
||||
}
|
||||
|
||||
function saved() {
|
||||
dialogStatusStore.webdav = false;
|
||||
saving.value = false;
|
||||
}
|
||||
</script>
|
@ -48,7 +48,7 @@
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-row class="mt-0">
|
||||
<v-col class="d-flex align-content-end">
|
||||
<v-switch
|
||||
label="Allow Self Signed Certificate"
|
||||
@ -62,7 +62,21 @@
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-row class="mt-0">
|
||||
<v-col class="d-flex align-content-end">
|
||||
<v-switch
|
||||
label="Chunked Upload (Beta)"
|
||||
v-model="data.chunckedUpload"
|
||||
hide-details="auto"
|
||||
density="compact"
|
||||
inset
|
||||
:error-messages="errors.chunckedUpload"
|
||||
:loading="loading"
|
||||
color="orange"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-0">
|
||||
<v-col><v-divider></v-divider></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
@ -133,18 +147,10 @@
|
||||
</v-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
WebdavEndpointType,
|
||||
type WebdavConfig,
|
||||
} from "../../types/webdavConfig";
|
||||
import {
|
||||
getWebdavConfig,
|
||||
saveWebdavConfig,
|
||||
} from "../../services/ConfigService";
|
||||
import { WebdavEndpointType, type WebdavConfig } from "@/types/webdavConfig";
|
||||
import { getWebdavConfig, saveWebdavConfig } from "@/services/configService";
|
||||
import { ref } from "vue";
|
||||
import { HTTPError } from "ky";
|
||||
|
||||
const loading = ref(true);
|
||||
import { useConfigForm } from "@/composable/ConfigForm";
|
||||
|
||||
const items = [
|
||||
{
|
||||
@ -165,6 +171,7 @@ const errors = ref({
|
||||
allowSelfSignedCerts: [],
|
||||
type: [],
|
||||
customEndpoint: [],
|
||||
chunckedUpload: [],
|
||||
});
|
||||
|
||||
const data = ref<WebdavConfig>({
|
||||
@ -173,6 +180,7 @@ const data = ref<WebdavConfig>({
|
||||
backupDir: "",
|
||||
username: "",
|
||||
password: "",
|
||||
chunckedUpload: false,
|
||||
webdavEndpoint: {
|
||||
type: WebdavEndpointType.NEXTCLOUD,
|
||||
},
|
||||
@ -185,44 +193,19 @@ const emit = defineEmits<{
|
||||
(e: "loading"): void;
|
||||
}>();
|
||||
|
||||
function save() {
|
||||
loading.value = true;
|
||||
clearErrors();
|
||||
saveWebdavConfig(data.value)
|
||||
.then(() => {
|
||||
emit("success");
|
||||
loading.value = false;
|
||||
})
|
||||
.catch(async (reason) => {
|
||||
if (reason instanceof HTTPError) {
|
||||
const response = await reason.response.json();
|
||||
if (Array.isArray(response)) {
|
||||
for (let elem of response) {
|
||||
errors.value[elem.context.key as keyof typeof errors.value] =
|
||||
elem.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
emit("fail");
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
for (let elem in errors.value) {
|
||||
errors.value[elem as keyof typeof errors.value] = [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
emit("loading");
|
||||
getWebdavConfig().then((value) => {
|
||||
return getWebdavConfig().then((value) => {
|
||||
data.value = value;
|
||||
emit("loaded");
|
||||
loading.value = false;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
loadData();
|
||||
const { save, loading } = useConfigForm(
|
||||
saveWebdavConfig,
|
||||
loadData,
|
||||
data,
|
||||
errors,
|
||||
emit
|
||||
);
|
||||
defineExpose({ save });
|
||||
</script>
|
||||
|
@ -3,7 +3,7 @@
|
||||
v-model="dialogStatusStore.webdav"
|
||||
persistent
|
||||
:width="width"
|
||||
:fullscreen="xs"
|
||||
:fullscreen="isFullScreen"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
@ -13,7 +13,7 @@
|
||||
<webdav-settings-form
|
||||
ref="form"
|
||||
@fail="saving = false"
|
||||
@success="dialogStatusStore.webdav = false"
|
||||
@success="saved"
|
||||
@loaded="loading = false"
|
||||
@loading="loading = true"
|
||||
></webdav-settings-form>
|
||||
@ -35,33 +35,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuSize } from "@/composable/menuSize";
|
||||
import { useDialogStatusStore } from "@/stores/dialogStatus";
|
||||
import { computed, ref } from "vue";
|
||||
import { useDisplay } from "vuetify/lib/framework.mjs";
|
||||
import WebdavSettingsForm from "./WebdavConfigForm.vue";
|
||||
|
||||
const dialogStatusStore = useDialogStatusStore();
|
||||
const form = ref<InstanceType<typeof WebdavSettingsForm> | null>(null);
|
||||
const { width, isFullScreen } = useMenuSize();
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
const { xs, mdAndDown } = useDisplay();
|
||||
|
||||
let saveLoading = computed(() => {
|
||||
return saving.value || loading.value;
|
||||
});
|
||||
|
||||
const width = computed(() => {
|
||||
if (xs.value) {
|
||||
return undefined;
|
||||
} else if (mdAndDown.value) {
|
||||
return "80%";
|
||||
} else {
|
||||
return "50%";
|
||||
}
|
||||
});
|
||||
|
||||
function save() {
|
||||
saving.value = true;
|
||||
form.value?.save();
|
||||
}
|
||||
|
||||
function saved() {
|
||||
dialogStatusStore.webdav = false;
|
||||
saving.value = false;
|
||||
}
|
||||
</script>
|
||||
|
58
nextcloud_backup/frontend/src/composable/ConfigForm.ts
Normal file
58
nextcloud_backup/frontend/src/composable/ConfigForm.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { HTTPError } from "ky";
|
||||
import { ref, type Ref } from "vue";
|
||||
|
||||
export function useConfigForm(
|
||||
saveService: (data: any) => Promise<unknown>,
|
||||
loadService: () => Promise<any>,
|
||||
dataRef: Ref,
|
||||
errorsRef: Ref,
|
||||
emit: {
|
||||
(e: "success"): void;
|
||||
(e: "fail"): void;
|
||||
(e: "loaded"): void;
|
||||
(e: "loading"): void;
|
||||
}
|
||||
) {
|
||||
const loading = ref(true);
|
||||
|
||||
function save() {
|
||||
loading.value = true;
|
||||
clearErrors();
|
||||
saveService(dataRef.value)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
emit("success");
|
||||
})
|
||||
.catch(async (reason) => {
|
||||
if (reason instanceof HTTPError) {
|
||||
const response = await reason.response.json();
|
||||
if (Array.isArray(response)) {
|
||||
for (const elem of response) {
|
||||
errorsRef.value[
|
||||
elem.context.key as keyof typeof errorsRef.value
|
||||
] = elem.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
loading.value = false;
|
||||
emit("fail");
|
||||
});
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
for (const elem in errorsRef.value) {
|
||||
errorsRef.value[elem as keyof typeof errorsRef.value] = [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
emit("loading");
|
||||
loadService().then(() => {
|
||||
loading.value = false;
|
||||
emit("loaded");
|
||||
});
|
||||
}
|
||||
|
||||
loadData();
|
||||
return { save, loading };
|
||||
}
|
17
nextcloud_backup/frontend/src/composable/menuSize.ts
Normal file
17
nextcloud_backup/frontend/src/composable/menuSize.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { computed } from "vue";
|
||||
import { useDisplay } from "vuetify/lib/framework.mjs";
|
||||
|
||||
export function useMenuSize() {
|
||||
const { xs, mdAndDown } = useDisplay();
|
||||
const width = computed(() => {
|
||||
if (xs.value) {
|
||||
return undefined;
|
||||
} else if (mdAndDown.value) {
|
||||
return "80%";
|
||||
} else {
|
||||
return "50%";
|
||||
}
|
||||
});
|
||||
const isFullScreen = xs;
|
||||
return { width, isFullScreen };
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import type { BackupConfig } from "@/types/backupConfig";
|
||||
import type { WebdavConfig } from "@/types/webdavConfig";
|
||||
import kyClient from "./kyClient";
|
||||
|
||||
@ -12,3 +13,15 @@ export function saveWebdavConfig(config: WebdavConfig) {
|
||||
})
|
||||
.json();
|
||||
}
|
||||
|
||||
export function getBackupConfig() {
|
||||
return kyClient.get("config/backup").json<BackupConfig>();
|
||||
}
|
||||
|
||||
export function saveBackupConfig(config: BackupConfig) {
|
||||
return kyClient
|
||||
.put("config/backup", {
|
||||
json: config,
|
||||
})
|
||||
.json();
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import type { AddonData, Folder } from "@/types/homeAssistant";
|
||||
import kyClient from "./kyClient";
|
||||
|
||||
export function getFolders() {
|
||||
return kyClient.get("homeAssistant/folders").json<Folder[]>();
|
||||
}
|
||||
|
||||
export function getAddons() {
|
||||
return kyClient.get("homeAssistant/addons").json<AddonData>();
|
||||
}
|
60
nextcloud_backup/frontend/src/stores/backupConfig.ts
Normal file
60
nextcloud_backup/frontend/src/stores/backupConfig.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { getBackupConfig } from "@/services/configService";
|
||||
import { getAddons, getFolders } from "@/services/homeAssistantService";
|
||||
import { CronMode, type BackupConfig } from "@/types/backupConfig";
|
||||
import type { Folder, AddonModel } from "@/types/homeAssistant";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export const useBackupConfigStore = defineStore("backupConfig", () => {
|
||||
const data = ref<BackupConfig>({} as BackupConfig);
|
||||
const addons = ref<AddonModel[]>([]);
|
||||
const folders = ref<Folder[]>([]);
|
||||
|
||||
// This represent the oposite of excluded => if prensent on this list, backup it
|
||||
const invertedFolders = ref<string[]>([]);
|
||||
const invertedAddons = ref<string[]>([]);
|
||||
|
||||
function loadAll() {
|
||||
const conf = getBackupConfig();
|
||||
const foldersProm = getFolders();
|
||||
const addonsProm = getAddons();
|
||||
return Promise.all([conf, foldersProm, addonsProm]).then((value) => {
|
||||
for (const folder of value[1]) {
|
||||
if (!value[0].exclude.folder.includes(folder.slug)) {
|
||||
invertedFolders.value.push(folder.slug);
|
||||
}
|
||||
}
|
||||
for (const addon of value[2].addons) {
|
||||
if (!value[0].exclude.addon.includes(addon.slug)) {
|
||||
invertedAddons.value.push(addon.slug);
|
||||
}
|
||||
}
|
||||
data.value = value[0];
|
||||
folders.value = value[1];
|
||||
addons.value = value[2].addons;
|
||||
});
|
||||
}
|
||||
|
||||
function addEmptyCron() {
|
||||
data.value.cron.push({
|
||||
id: uuidv4(),
|
||||
mode: CronMode.Daily,
|
||||
});
|
||||
}
|
||||
|
||||
function removeCron(id: string) {
|
||||
data.value.cron = data.value.cron.filter((value) => value.id != id);
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
addons,
|
||||
folders,
|
||||
invertedFolders,
|
||||
invertedAddons,
|
||||
loadAll,
|
||||
addEmptyCron,
|
||||
removeCron,
|
||||
};
|
||||
});
|
65
nextcloud_backup/frontend/src/types/backupConfig.ts
Normal file
65
nextcloud_backup/frontend/src/types/backupConfig.ts
Normal file
@ -0,0 +1,65 @@
|
||||
export enum CronMode {
|
||||
Daily = "DAILY",
|
||||
Weekly = "WEEKLY",
|
||||
Monthly = "MONTHLY",
|
||||
Custom = "CUSTOM",
|
||||
}
|
||||
|
||||
export enum CronModeFriendly {
|
||||
DAILY = "Daily",
|
||||
WEEKLY = "Weekly",
|
||||
MONTHLY = "Monthly",
|
||||
CUSTOM = "Custom",
|
||||
}
|
||||
|
||||
export enum Weekday {
|
||||
SUNDAY,
|
||||
MONDAY,
|
||||
TUESDAY,
|
||||
WEDNESDAY,
|
||||
THURSDAY,
|
||||
FRIDAY,
|
||||
SATURDAY,
|
||||
}
|
||||
|
||||
export const weekdayFriendly = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
|
||||
export interface BackupConfig {
|
||||
nameTemplate: string;
|
||||
cron: CronConfig[];
|
||||
autoClean: {
|
||||
homeAssistant: AutoCleanConfig;
|
||||
webdav: AutoCleanConfig;
|
||||
};
|
||||
exclude: {
|
||||
addon: string[];
|
||||
folder: string[];
|
||||
};
|
||||
autoStopAddon: string[];
|
||||
password: {
|
||||
enabled: boolean;
|
||||
value?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CronConfig {
|
||||
id: string;
|
||||
mode: CronMode;
|
||||
hour?: string;
|
||||
weekday?: Weekday;
|
||||
monthDay?: string;
|
||||
custom?: string;
|
||||
}
|
||||
|
||||
export interface AutoCleanConfig {
|
||||
enabled: boolean;
|
||||
nbrToKeep?: number;
|
||||
}
|
24
nextcloud_backup/frontend/src/types/homeAssistant.ts
Normal file
24
nextcloud_backup/frontend/src/types/homeAssistant.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export interface Folder {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface AddonData {
|
||||
addons: AddonModel[];
|
||||
}
|
||||
|
||||
export interface AddonModel {
|
||||
name: string;
|
||||
slug: string;
|
||||
advanced: boolean;
|
||||
description: string;
|
||||
repository: string;
|
||||
version: string;
|
||||
version_latest: string;
|
||||
update_available: boolean;
|
||||
installed: string;
|
||||
available: boolean;
|
||||
icon: boolean;
|
||||
logo: boolean;
|
||||
state: string;
|
||||
}
|
@ -9,6 +9,7 @@ export interface WebdavConfig {
|
||||
password: string;
|
||||
backupDir: string;
|
||||
allowSelfSignedCerts: boolean;
|
||||
chunckedUpload: boolean;
|
||||
webdavEndpoint: {
|
||||
type: WebdavEndpointType;
|
||||
customEndpoint?: string;
|
||||
|
@ -5,7 +5,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"lib": ["ES2017", "DOM", "DOM.Iterable"]
|
||||
},
|
||||
|
||||
"references": [
|
||||
|
@ -2,10 +2,14 @@ import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import dns from "dns";
|
||||
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
|
||||
import vuetify from "vite-plugin-vuetify";
|
||||
|
||||
// //Print localhost instead of 127.0.0.1
|
||||
// dns.setDefaultResultOrder("verbatim");
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vuetify({ autoImport: true })],
|
||||
|
Loading…
Reference in New Issue
Block a user