mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-26 19:06:55 +01:00
Compare commits
3 Commits
446e89a6c7
...
9a02bfc6b8
Author | SHA1 | Date | |
---|---|---|---|
9a02bfc6b8 | |||
a10868da54 | |||
1324c0b3d1 |
@ -13,7 +13,7 @@ const __dirname = path.dirname(__filename);
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: "http://localhost:5173"
|
origin: true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
app.set("port", process.env.PORT || 3000);
|
app.set("port", process.env.PORT || 3000);
|
||||||
|
@ -1,17 +1,34 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import * as haOsService from "../services/homeAssistantService.js"
|
import * as haOsService from "../services/homeAssistantService.js";
|
||||||
|
|
||||||
const homeAssistantRouter = express.Router();
|
const homeAssistantRouter = express.Router();
|
||||||
|
|
||||||
homeAssistantRouter.get("/backups/", (req, res, next) => {
|
homeAssistantRouter.get("/backups/", (req, res, next) => {
|
||||||
haOsService.getBackups()
|
haOsService
|
||||||
.then((value)=>{
|
.getBackups()
|
||||||
|
.then((value) => {
|
||||||
res.json(value.body.data.backups);
|
res.json(value.body.data.backups);
|
||||||
}).catch((reason)=>{
|
})
|
||||||
|
.catch((reason) => {
|
||||||
res.status(500);
|
res.status(500);
|
||||||
res.json(reason);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
homeAssistantRouter.get("/folders", (req, res, next) => {
|
||||||
|
res.json(haOsService.getFolderList());
|
||||||
|
})
|
||||||
|
|
||||||
export default homeAssistantRouter;
|
export default homeAssistantRouter;
|
@ -86,30 +86,6 @@ function getAddonToBackup(addons: AddonModel[]) {
|
|||||||
return slugs;
|
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() {
|
function getFolderToBackup() {
|
||||||
const excluded_folder = settingsTools.getSettings().exclude_folder;
|
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) {
|
function publish_state(state: Status) {
|
||||||
// let data_error_sensor = {
|
// let data_error_sensor = {
|
||||||
// state: state.status == "error" ? "on" : "off",
|
// state: state.status == "error" ? "on" : "off",
|
||||||
@ -501,7 +503,6 @@ function publish_state(state: Status) {
|
|||||||
export {
|
export {
|
||||||
getVersion,
|
getVersion,
|
||||||
getAddonList,
|
getAddonList,
|
||||||
getFolderList,
|
|
||||||
getBackups,
|
getBackups,
|
||||||
downloadSnapshot,
|
downloadSnapshot,
|
||||||
createNewBackup,
|
createNewBackup,
|
||||||
|
@ -59,6 +59,7 @@ export function getWebdavDefaultConfig(): WebdavConfig {
|
|||||||
password: "",
|
password: "",
|
||||||
backupDir: default_root,
|
backupDir: default_root,
|
||||||
allowSelfSignedCerts: false,
|
allowSelfSignedCerts: false,
|
||||||
|
chunckedUpload: false,
|
||||||
webdavEndpoint: {
|
webdavEndpoint: {
|
||||||
type: WebdavEndpointType.NEXTCLOUD,
|
type: WebdavEndpointType.NEXTCLOUD,
|
||||||
},
|
},
|
||||||
|
@ -47,8 +47,8 @@ const backupConfigValidation = {
|
|||||||
webdav: Joi.object(AutoCleanConfig).required(),
|
webdav: Joi.object(AutoCleanConfig).required(),
|
||||||
}).required(),
|
}).required(),
|
||||||
exclude: Joi.object({
|
exclude: Joi.object({
|
||||||
addon: Joi.array().items(Joi.string().not().empty()),
|
addon: Joi.array().items(Joi.string().not().empty()).required(),
|
||||||
folder: Joi.array().items(Joi.string().not().empty()),
|
folder: Joi.array().items(Joi.string().not().empty()).required(),
|
||||||
}).required(),
|
}).required(),
|
||||||
autoStopAddon: Joi.array().items(Joi.string().not().empty()),
|
autoStopAddon: Joi.array().items(Joi.string().not().empty()),
|
||||||
password: Joi.object({
|
password: Joi.object({
|
||||||
|
@ -10,6 +10,7 @@ export interface WebdavConfig {
|
|||||||
password: string;
|
password: string;
|
||||||
backupDir: string;
|
backupDir: string;
|
||||||
allowSelfSignedCerts: boolean
|
allowSelfSignedCerts: boolean
|
||||||
|
chunckedUpload: boolean
|
||||||
webdavEndpoint: {
|
webdavEndpoint: {
|
||||||
type: WebdavEndpointType;
|
type: WebdavEndpointType;
|
||||||
customEndpoint?: string;
|
customEndpoint?: string;
|
||||||
|
@ -3,11 +3,12 @@ import { WebdavEndpointType } from "./webdavConfig.js";
|
|||||||
|
|
||||||
|
|
||||||
const WebdavConfigValidation = {
|
const WebdavConfigValidation = {
|
||||||
url: Joi.string().not().empty().uri().required(),
|
url: Joi.string().not().empty().uri().required().label("Url"),
|
||||||
username: Joi.string().not().empty().required(),
|
username: Joi.string().not().empty().label("Username"),
|
||||||
password: Joi.string().not().empty().required(),
|
password: Joi.string().not().empty().label("Password"),
|
||||||
backupDir: Joi.string().required(),
|
backupDir: Joi.string().required().label("Backup directory"),
|
||||||
allowSelfSignedCerts: Joi.boolean().required(),
|
allowSelfSignedCerts: Joi.boolean().label("Allow self signed certificate"),
|
||||||
|
chunckedUpload: Joi.boolean().required().label("Chuncked upload"),
|
||||||
webdavEndpoint: Joi.object({
|
webdavEndpoint: Joi.object({
|
||||||
type: Joi.string().valid(WebdavEndpointType.CUSTOM, WebdavEndpointType.NEXTCLOUD).required(),
|
type: Joi.string().valid(WebdavEndpointType.CUSTOM, WebdavEndpointType.NEXTCLOUD).required(),
|
||||||
customEndpoint: Joi.alternatives().conditional("type", {
|
customEndpoint: Joi.alternatives().conditional("type", {
|
||||||
@ -15,7 +16,7 @@ const WebdavConfigValidation = {
|
|||||||
then: Joi.string().not().empty().required,
|
then: Joi.string().not().empty().required,
|
||||||
otherwise: Joi.disallow()
|
otherwise: Joi.disallow()
|
||||||
})
|
})
|
||||||
}).required()
|
}).required().label("Webdav endpoint"),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WebdavConfigValidation;
|
export default WebdavConfigValidation;
|
@ -11,33 +11,35 @@
|
|||||||
"type-check": "vue-tsc --noEmit"
|
"type-check": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "5.9.55",
|
"@mdi/font": "7.1.96",
|
||||||
"@mdi/js": "^7.0.96",
|
"@mdi/js": "^7.1.96",
|
||||||
"@types/luxon": "^3.0.1",
|
"@types/luxon": "^3.2.0",
|
||||||
|
"@types/uuid": "^9.0.0",
|
||||||
"ky": "^0.31.4",
|
"ky": "^0.31.4",
|
||||||
"luxon": "^3.0.4",
|
"luxon": "^3.2.1",
|
||||||
"pinia": "^2.0.23",
|
"pinia": "^2.0.28",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.0.0",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"vue": "^3.2.40",
|
"uuid": "^9.0.0",
|
||||||
"vuetify": "3.0.1",
|
"vue": "^3.2.45",
|
||||||
"webfontloader": "^1.0.0"
|
"vuetify": "3.1.1",
|
||||||
|
"webfontloader": "^1.6.28"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
"@types/node": "^16.11.65",
|
"@types/node": "^16.18.11",
|
||||||
"@types/webfontloader": "^1.0.0",
|
"@types/webfontloader": "^1.6.35",
|
||||||
"@vitejs/plugin-vue": "^3.1.2",
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
"@vue/eslint-config-prettier": "^7.0.0",
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.2",
|
"@vue/eslint-config-typescript": "^11.0.2",
|
||||||
"@vue/tsconfig": "^0.1.3",
|
"@vue/tsconfig": "^0.1.3",
|
||||||
"eslint": "^8.25.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-plugin-vue": "^9.6.0",
|
"eslint-plugin-vue": "^9.8.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.2",
|
||||||
"typescript": "~4.7.4",
|
"typescript": "~4.7.4",
|
||||||
"vite": "^3.1.7",
|
"vite": "^3.2.5",
|
||||||
"vite-plugin-vuetify": "^1.0.0-alpha.12",
|
"vite-plugin-vuetify": "^1.0.1",
|
||||||
"vue-tsc": "1.0.7"
|
"vue-tsc": "1.0.7"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.12.1"
|
"packageManager": "pnpm@7.12.1"
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
|||||||
<navbar-component></navbar-component>
|
<navbar-component></navbar-component>
|
||||||
<message-bar></message-bar>
|
<message-bar></message-bar>
|
||||||
<webdav-settings-menu></webdav-settings-menu>
|
<webdav-settings-menu></webdav-settings-menu>
|
||||||
|
<BackupConfigMenu></BackupConfigMenu>
|
||||||
<v-main class="mx-12">
|
<v-main class="mx-12">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="6" offset="6">
|
<v-col cols="6" offset="6">
|
||||||
@ -18,6 +19,7 @@ import NavbarComponent from "./components/NavbarComponent.vue";
|
|||||||
import MessageBar from "./components/MessageBar.vue";
|
import MessageBar from "./components/MessageBar.vue";
|
||||||
import WebdavSettingsMenu from "./components/settings/WebdavConfigMenu.vue";
|
import WebdavSettingsMenu from "./components/settings/WebdavConfigMenu.vue";
|
||||||
import CloudList from "./components/cloud/CloudList.vue";
|
import CloudList from "./components/cloud/CloudList.vue";
|
||||||
|
import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -6,11 +6,15 @@
|
|||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-card variant="outlined" elevation="5" color="grey-darken-2">
|
<v-card variant="elevated" elevation="7" height="100%">
|
||||||
<v-card-title class="text-center text-white">Auto</v-card-title>
|
<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-divider color="grey-darken-3"></v-divider>
|
||||||
<v-card-text class="pa-0">
|
<v-card-text class="pa-0">
|
||||||
<v-list variant="tonal" class="pa-0">
|
<v-list class="pa-0">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-if="autoBackups.length == 0"
|
v-if="autoBackups.length == 0"
|
||||||
class="text-center text-subtitle-2 text-disabled"
|
class="text-center text-subtitle-2 text-disabled"
|
||||||
@ -31,11 +35,14 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-card variant="outlined" elevation="5" color="grey-darken-2">
|
<v-card variant="elevated" elevation="7" height="100%">
|
||||||
<v-card-title class="text-center text-white">Manual</v-card-title>
|
<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-divider color="grey-darken-3"></v-divider>
|
||||||
<v-card-text class="pa-0">
|
<v-card-text class="pa-0">
|
||||||
<v-list variant="tonal" class="pa-0">
|
<v-list class="pa-0">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-if="manualBackups.length == 0"
|
v-if="manualBackups.length == 0"
|
||||||
class="text-center text-subtitle-2 text-disabled"
|
class="text-center text-subtitle-2 text-disabled"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-divider v-if="index != 0" color="grey-darken-3"></v-divider>
|
<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>
|
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-scroll-x-transition>
|
<v-scroll-x-transition>
|
||||||
@ -59,7 +59,7 @@
|
|||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row>
|
<v-row dense>
|
||||||
<v-col class="d-flex justify-center">
|
<v-col class="d-flex justify-center">
|
||||||
<v-tooltip text="Last edit" location="top">
|
<v-tooltip text="Last edit" location="top">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
|
@ -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,200 @@
|
|||||||
|
<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>
|
||||||
|
<v-chip
|
||||||
|
v-if="!expanded && cron.monthDay != undefined"
|
||||||
|
append-icon="mdi-calendar"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ cron.monthDay }}
|
||||||
|
</v-chip>
|
||||||
|
</v-fade-transition>
|
||||||
|
<v-fade-transition>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<div
|
||||||
|
v-if="data.cron?.length == 0"
|
||||||
|
class="my-3 text-subtitle-2 text-medium-emphasis"
|
||||||
|
>
|
||||||
|
No auto backup configured
|
||||||
|
</div>
|
||||||
|
</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,85 @@
|
|||||||
|
<template>
|
||||||
|
<v-card variant="elevated" elevation="7">
|
||||||
|
<v-card-title class="bg-light-blue-darken-4 text-center">
|
||||||
|
Auto Clean
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row class="mt-0" v-if="!loading">
|
||||||
|
<v-col class="" cols="12" md="6">
|
||||||
|
<v-row dense>
|
||||||
|
<v-col>
|
||||||
|
<v-switch
|
||||||
|
label="Auto clean Home Assistant Backups"
|
||||||
|
v-model="data.autoClean.homeAssistant.enabled"
|
||||||
|
hide-details="auto"
|
||||||
|
density="compact"
|
||||||
|
inset
|
||||||
|
:loading="loading"
|
||||||
|
color="orange"
|
||||||
|
></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-fade-transition>
|
||||||
|
<v-row dense v-if="data.autoClean.homeAssistant.enabled">
|
||||||
|
<v-col>
|
||||||
|
<div class="text-subtitle-1 text-medium-emphasis">
|
||||||
|
Number of backup to keep
|
||||||
|
</div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.autoClean.homeAssistant.nbrToKeep"
|
||||||
|
type="number"
|
||||||
|
hide-details="auto"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
color="orange"
|
||||||
|
min="1"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-fade-transition>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-row dense>
|
||||||
|
<v-col>
|
||||||
|
<v-switch
|
||||||
|
label="Auto clean Cloud Backups"
|
||||||
|
v-model="data.autoClean.webdav.enabled"
|
||||||
|
hide-details="auto"
|
||||||
|
density="compact"
|
||||||
|
inset
|
||||||
|
:loading="loading"
|
||||||
|
color="orange"
|
||||||
|
></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-fade-transition>
|
||||||
|
<v-row dense v-if="data.autoClean.webdav.enabled">
|
||||||
|
<v-col>
|
||||||
|
<div class="text-subtitle-1 text-medium-emphasis">
|
||||||
|
Number of backup to keep
|
||||||
|
</div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="data.autoClean.webdav.nbrToKeep"
|
||||||
|
type="number"
|
||||||
|
hide-details="auto"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
color="orange"
|
||||||
|
min="1"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-fade-transition>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBackupConfigStore } from "@/stores/backupConfig";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
|
defineProps<{ loading: boolean }>();
|
||||||
|
const backupConfigStore = useBackupConfigStore();
|
||||||
|
const { data } = storeToRefs(backupConfigStore);
|
||||||
|
</script>
|
@ -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,99 @@
|
|||||||
|
<template>
|
||||||
|
<v-form class="mx-4" @submit.prevent>
|
||||||
|
<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.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 dense>
|
||||||
|
<v-col>
|
||||||
|
<BackupConfigAutoBackup :loading="loading"></BackupConfigAutoBackup>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col>
|
||||||
|
<BackupConfigAutoClean :loading="loading"></BackupConfigAutoClean>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
import { useConfigForm } from "@/composable/ConfigForm";
|
||||||
|
import { saveBackupConfig } from "@/services/configService";
|
||||||
|
import { useBackupConfigStore } from "@/stores/backupConfig";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import BackupConfigAddon from "./BackupConfig/BackupConfigAddon.vue";
|
||||||
|
import BackupConfigAutoBackup from "./BackupConfig/BackupConfigAutoBackup.vue";
|
||||||
|
import BackupConfigFolder from "./BackupConfig/BackupConfigFolder.vue";
|
||||||
|
import BackupConfigAutoClean from "./BackupConfig/BackupConfigAutoClean.vue";
|
||||||
|
|
||||||
|
const backupConfigStore = useBackupConfigStore();
|
||||||
|
const { data } = 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-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row>
|
<v-row class="mt-0">
|
||||||
<v-col class="d-flex align-content-end">
|
<v-col class="d-flex align-content-end">
|
||||||
<v-switch
|
<v-switch
|
||||||
label="Allow Self Signed Certificate"
|
label="Allow Self Signed Certificate"
|
||||||
@ -62,7 +62,21 @@
|
|||||||
></v-switch>
|
></v-switch>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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-col><v-divider></v-divider></v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row>
|
<v-row>
|
||||||
@ -133,18 +147,10 @@
|
|||||||
</v-form>
|
</v-form>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { WebdavEndpointType, type WebdavConfig } from "@/types/webdavConfig";
|
||||||
WebdavEndpointType,
|
import { getWebdavConfig, saveWebdavConfig } from "@/services/configService";
|
||||||
type WebdavConfig,
|
|
||||||
} from "../../types/webdavConfig";
|
|
||||||
import {
|
|
||||||
getWebdavConfig,
|
|
||||||
saveWebdavConfig,
|
|
||||||
} from "../../services/ConfigService";
|
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { HTTPError } from "ky";
|
import { useConfigForm } from "@/composable/ConfigForm";
|
||||||
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@ -165,6 +171,7 @@ const errors = ref({
|
|||||||
allowSelfSignedCerts: [],
|
allowSelfSignedCerts: [],
|
||||||
type: [],
|
type: [],
|
||||||
customEndpoint: [],
|
customEndpoint: [],
|
||||||
|
chunckedUpload: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = ref<WebdavConfig>({
|
const data = ref<WebdavConfig>({
|
||||||
@ -173,6 +180,7 @@ const data = ref<WebdavConfig>({
|
|||||||
backupDir: "",
|
backupDir: "",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
chunckedUpload: false,
|
||||||
webdavEndpoint: {
|
webdavEndpoint: {
|
||||||
type: WebdavEndpointType.NEXTCLOUD,
|
type: WebdavEndpointType.NEXTCLOUD,
|
||||||
},
|
},
|
||||||
@ -185,44 +193,19 @@ const emit = defineEmits<{
|
|||||||
(e: "loading"): void;
|
(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() {
|
function loadData() {
|
||||||
emit("loading");
|
return getWebdavConfig().then((value) => {
|
||||||
getWebdavConfig().then((value) => {
|
|
||||||
data.value = value;
|
data.value = value;
|
||||||
emit("loaded");
|
return data;
|
||||||
loading.value = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData();
|
const { save, loading } = useConfigForm(
|
||||||
|
saveWebdavConfig,
|
||||||
|
loadData,
|
||||||
|
data,
|
||||||
|
errors,
|
||||||
|
emit
|
||||||
|
);
|
||||||
defineExpose({ save });
|
defineExpose({ save });
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
v-model="dialogStatusStore.webdav"
|
v-model="dialogStatusStore.webdav"
|
||||||
persistent
|
persistent
|
||||||
:width="width"
|
:width="width"
|
||||||
:fullscreen="xs"
|
:fullscreen="isFullScreen"
|
||||||
scrollable
|
scrollable
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<webdav-settings-form
|
<webdav-settings-form
|
||||||
ref="form"
|
ref="form"
|
||||||
@fail="saving = false"
|
@fail="saving = false"
|
||||||
@success="dialogStatusStore.webdav = false"
|
@success="saved"
|
||||||
@loaded="loading = false"
|
@loaded="loading = false"
|
||||||
@loading="loading = true"
|
@loading="loading = true"
|
||||||
></webdav-settings-form>
|
></webdav-settings-form>
|
||||||
@ -35,33 +35,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useMenuSize } from "@/composable/menuSize";
|
||||||
import { useDialogStatusStore } from "@/stores/dialogStatus";
|
import { useDialogStatusStore } from "@/stores/dialogStatus";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useDisplay } from "vuetify/lib/framework.mjs";
|
|
||||||
import WebdavSettingsForm from "./WebdavConfigForm.vue";
|
import WebdavSettingsForm from "./WebdavConfigForm.vue";
|
||||||
|
|
||||||
const dialogStatusStore = useDialogStatusStore();
|
const dialogStatusStore = useDialogStatusStore();
|
||||||
const form = ref<InstanceType<typeof WebdavSettingsForm> | null>(null);
|
const form = ref<InstanceType<typeof WebdavSettingsForm> | null>(null);
|
||||||
|
const { width, isFullScreen } = useMenuSize();
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const { xs, mdAndDown } = useDisplay();
|
|
||||||
|
|
||||||
let saveLoading = computed(() => {
|
let saveLoading = computed(() => {
|
||||||
return saving.value || loading.value;
|
return saving.value || loading.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const width = computed(() => {
|
|
||||||
if (xs.value) {
|
|
||||||
return undefined;
|
|
||||||
} else if (mdAndDown.value) {
|
|
||||||
return "80%";
|
|
||||||
} else {
|
|
||||||
return "50%";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
form.value?.save();
|
form.value?.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saved() {
|
||||||
|
dialogStatusStore.webdav = false;
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
</script>
|
</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,14 +0,0 @@
|
|||||||
import type { WebdavConfig } from "@/types/webdavConfig";
|
|
||||||
import kyClient from "./kyClient";
|
|
||||||
|
|
||||||
export function getWebdavConfig() {
|
|
||||||
return kyClient.get("config/webdav").json<WebdavConfig>();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveWebdavConfig(config: WebdavConfig) {
|
|
||||||
return kyClient
|
|
||||||
.put("config/webdav", {
|
|
||||||
json: config,
|
|
||||||
})
|
|
||||||
.json();
|
|
||||||
}
|
|
37
nextcloud_backup/frontend/src/services/configService.ts
Normal file
37
nextcloud_backup/frontend/src/services/configService.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { BackupConfig } from "@/types/backupConfig";
|
||||||
|
import type { WebdavConfig } from "@/types/webdavConfig";
|
||||||
|
import kyClient from "./kyClient";
|
||||||
|
|
||||||
|
export function getWebdavConfig() {
|
||||||
|
return kyClient.get("config/webdav").json<WebdavConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveWebdavConfig(config: WebdavConfig) {
|
||||||
|
return kyClient
|
||||||
|
.put("config/webdav", {
|
||||||
|
json: config,
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackupConfig() {
|
||||||
|
return kyClient.get("config/backup").json<BackupConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveBackupConfig(config: BackupConfig) {
|
||||||
|
return kyClient
|
||||||
|
.put("config/backup", {
|
||||||
|
json: cleanupConfig(config),
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupConfig(config: BackupConfig) {
|
||||||
|
if (!config.autoClean.homeAssistant.enabled) {
|
||||||
|
config.autoClean.homeAssistant.nbrToKeep = undefined;
|
||||||
|
}
|
||||||
|
if (!config.autoClean.webdav.enabled) {
|
||||||
|
config.autoClean.webdav.nbrToKeep = undefined;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
@ -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;
|
password: string;
|
||||||
backupDir: string;
|
backupDir: string;
|
||||||
allowSelfSignedCerts: boolean;
|
allowSelfSignedCerts: boolean;
|
||||||
|
chunckedUpload: boolean;
|
||||||
webdavEndpoint: {
|
webdavEndpoint: {
|
||||||
type: WebdavEndpointType;
|
type: WebdavEndpointType;
|
||||||
customEndpoint?: string;
|
customEndpoint?: string;
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"lib": ["ES2017", "DOM", "DOM.Iterable"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"references": [
|
"references": [
|
||||||
|
@ -2,10 +2,14 @@ import { fileURLToPath, URL } from "node:url";
|
|||||||
|
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import dns from "dns";
|
||||||
|
|
||||||
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
|
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
|
||||||
import vuetify from "vite-plugin-vuetify";
|
import vuetify from "vite-plugin-vuetify";
|
||||||
|
|
||||||
|
// //Print localhost instead of 127.0.0.1
|
||||||
|
// dns.setDefaultResultOrder("verbatim");
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), vuetify({ autoImport: true })],
|
plugins: [vue(), vuetify({ autoImport: true })],
|
||||||
|
Loading…
Reference in New Issue
Block a user