Compare commits

...

3 Commits

Author SHA1 Message Date
446e89a6c7
Update vuetify 2022-11-14 16:24:55 +01:00
5be8c9e426
🔨 Extract data from cloud file name 2022-11-14 16:12:05 +01:00
bff494252b
🔨 Add delete for cloud 2022-11-14 14:23:15 +01:00
17 changed files with 352 additions and 83 deletions

View File

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "backend"
},
{
"path": "frontend"
}
],
"settings": {}
}

View File

@ -27,7 +27,7 @@ app.set("port", process.env.PORT || 3000);
// ); // );
app.use( app.use(
morgan("dev", { stream: { write: (message) => logger.info(message) } }) morgan("dev", { stream: { write: (message) => logger.debug(message) } })
); );
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));

View File

@ -1,26 +1,25 @@
import express from "express"; import express from "express";
import Joi from "joi";
import { getBackupConfig } from "../services/backupConfigService.js";
import { import {
getWebdavConfig, getWebdavConfig,
validateWebdavConfig, validateWebdavConfig,
} from "../services/webdavConfigService.js"; } from "../services/webdavConfigService.js";
import * as webdavService from "../services/webdavService.js"; import * as webdavService from "../services/webdavService.js";
import * as pathTools from "../tools/pathTools.js"; import * as pathTools from "../tools/pathTools.js";
import type { WebdavDelete } from "../types/services/webdav.js";
import { WebdavDeleteValidation } from "../types/services/webdavValidation.js";
const webdavRouter = express.Router(); const webdavRouter = express.Router();
webdavRouter.get("/backup/auto", (req, res, next) => { webdavRouter.get("/backup/auto", (req, res, next) => {
const config = getWebdavConfig(); const config = getWebdavConfig();
const backupConf = getBackupConfig();
validateWebdavConfig(config) validateWebdavConfig(config)
.then(() => { .then(async () => {
webdavService const value = await webdavService
.getBackups(pathTools.auto, config) .getBackups(pathTools.auto, config, backupConf.nameTemplate);
.then((value) => { res.json(value);
res.json(value);
})
.catch((reason) => {
res.status(500);
res.json(reason);
});
}) })
.catch((reason) => { .catch((reason) => {
res.status(500); res.status(500);
@ -30,17 +29,12 @@ webdavRouter.get("/backup/auto", (req, res, next) => {
webdavRouter.get("/backup/manual", (req, res, next) => { webdavRouter.get("/backup/manual", (req, res, next) => {
const config = getWebdavConfig(); const config = getWebdavConfig();
const backupConf = getBackupConfig();
validateWebdavConfig(config) validateWebdavConfig(config)
.then(() => { .then(async () => {
webdavService const value = await webdavService
.getBackups(pathTools.manual, config) .getBackups(pathTools.manual, config, backupConf.nameTemplate);
.then((value) => { res.json(value);
res.json(value);
})
.catch((reason) => {
res.status(500);
res.json(reason);
});
}) })
.catch((reason) => { .catch((reason) => {
res.status(500); res.status(500);
@ -48,4 +42,25 @@ webdavRouter.get("/backup/manual", (req, res, next) => {
}); });
}); });
webdavRouter.delete("/", (req, res, next) => {
const body: WebdavDelete = req.body;
const validator = Joi.object(WebdavDeleteValidation);
const config = getWebdavConfig();
validateWebdavConfig(config).then(() => {
validator
.validateAsync(body)
.then(() => {
webdavService.deleteBackup(body.path, config)
.then(()=>{
res.status(201).send();
}).catch((reason)=>{
res.status(500).json(reason);
});
})
.catch((reason) => {
res.status(400).json(reason);
});
});
});
export default webdavRouter; export default webdavRouter;

View File

@ -1,27 +1,25 @@
import fs from "fs"; import fs from "fs";
import Joi from "joi" import Joi from "joi";
import logger from "../config/winston.js"; import logger from "../config/winston.js";
import type { BackupConfig } from "../types/services/backupConfig.js" import type { BackupConfig } from "../types/services/backupConfig.js";
import backupConfigValidation from "../types/services/backupConfigValidation.js"; import backupConfigValidation from "../types/services/backupConfigValidation.js";
const backupConfigPath = "/data/backupConfigV2.json"; const backupConfigPath = "/data/backupConfigV2.json";
export function validateBackupConfig(config: BackupConfig) {
export function validateBackupConfig(config: BackupConfig){
const validator = Joi.object(backupConfigValidation); const validator = Joi.object(backupConfigValidation);
return validator.validateAsync(config, { return validator.validateAsync(config, {
abortEarly: false abortEarly: false,
}); });
} }
export function saveBackupConfig(config: BackupConfig){ export function saveBackupConfig(config: BackupConfig) {
fs.writeFileSync(backupConfigPath, JSON.stringify(config, undefined, 2)); fs.writeFileSync(backupConfigPath, JSON.stringify(config, undefined, 2));
} }
export function getBackupConfig(): BackupConfig { export function getBackupConfig(): BackupConfig {
if (!fs.existsSync(backupConfigPath)) { if (!fs.existsSync(backupConfigPath)) {
logger.warn("Config file not found, creating default one !") logger.warn("Config file not found, creating default one !");
const defaultConfig = getBackupDefaultConfig(); const defaultConfig = getBackupDefaultConfig();
saveBackupConfig(defaultConfig); saveBackupConfig(defaultConfig);
return defaultConfig; return defaultConfig;
@ -39,7 +37,7 @@ export function getBackupDefaultConfig(): BackupConfig {
enabled: false, enabled: false,
}, },
webdav: { webdav: {
enabled: false enabled: false,
}, },
}, },
exclude: { exclude: {
@ -49,6 +47,18 @@ export function getBackupDefaultConfig(): BackupConfig {
autoStopAddon: [], autoStopAddon: [],
password: { password: {
enabled: false, enabled: false,
} },
} };
} }
export function templateToRegexp(template: string) {
let regexp = template.replace("{date}", "(?<date>\\d{4}-\\d{2}-\\d{2})");
regexp = regexp.replace("{hour}", "(?<hour>\\d{4})");
regexp = regexp.replace("{hour_12}", "(?<hour12>\\d{4}(AM|PM))");
regexp = regexp.replace("{type}", "(?<type>Auto|Manual|)")
regexp = regexp.replace("{type_low}", "(?<type>auto|manual|)")
return regexp.replace(
"{ha_version}",
"(?<version>\\d+\\.\\d+\\.\\d+(b\\d+)?)"
);
}

View File

@ -2,20 +2,19 @@ import fs from "fs";
import Joi from "joi"; import Joi from "joi";
import logger from "../config/winston.js"; import logger from "../config/winston.js";
import { default_root } from "../tools/pathTools.js"; import { default_root } from "../tools/pathTools.js";
import WebdavConfigValidation from "../types/services/webdavConfigValidation.js";
import { import {
WebdavConfig, WebdavConfig,
WebdavEndpointType, WebdavEndpointType
} from "../types/services/webdavConfig.js"; } from "../types/services/webdavConfig.js";
import WebdavConfigValidation from "../types/services/webdavConfigValidation.js";
const webdavConfigPath = "/data/webdavConfigV2.json"; const webdavConfigPath = "/data/webdavConfigV2.json";
const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username"; const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username";
export function validateWebdavConfig(config: WebdavConfig) { export function validateWebdavConfig(config: WebdavConfig) {
const validator = Joi.object(WebdavConfigValidation); const validator = Joi.object(WebdavConfigValidation);
return validator.validateAsync(config, { return validator.validateAsync(config, {
abortEarly: false abortEarly: false,
}); });
} }
@ -35,16 +34,22 @@ export function getWebdavConfig(): WebdavConfig {
} }
export function getEndpoint(config: WebdavConfig) { export function getEndpoint(config: WebdavConfig) {
let endpoint: string;
if (config.webdavEndpoint.type == WebdavEndpointType.NEXTCLOUD) { if (config.webdavEndpoint.type == WebdavEndpointType.NEXTCLOUD) {
return NEXTCLOUD_ENDPOINT.replace("$username", config.username); endpoint = NEXTCLOUD_ENDPOINT.replace("$username", config.username);
} else if (config.webdavEndpoint.customEndpoint) { } else if (config.webdavEndpoint.customEndpoint) {
return config.webdavEndpoint.customEndpoint.replace( endpoint = config.webdavEndpoint.customEndpoint.replace(
"$username", "$username",
config.username config.username
); );
} else { } else {
return ""; return "";
} }
if (endpoint.endsWith("/")) {
return endpoint.slice(0, -1);
}
return endpoint;
} }
export function getWebdavDefaultConfig(): WebdavConfig { export function getWebdavDefaultConfig(): WebdavConfig {

View File

@ -8,6 +8,7 @@ import * as pathTools from "../tools/pathTools.js";
import * as statusTools from "../tools/status.js"; import * as statusTools from "../tools/status.js";
import type { WebdavBackup } from "../types/services/webdav.js"; import type { WebdavBackup } from "../types/services/webdav.js";
import type { WebdavConfig } from "../types/services/webdavConfig.js"; import type { WebdavConfig } from "../types/services/webdavConfig.js";
import { templateToRegexp } from "./backupConfigService.js";
import { getEndpoint } from "./webdavConfigService.js"; import { getEndpoint } from "./webdavConfigService.js";
const PROPFIND_BODY = const PROPFIND_BODY =
@ -94,7 +95,11 @@ function createDirectory(path: string, config: WebdavConfig) {
}); });
} }
export function getBackups(folder: string, config: WebdavConfig) { export function getBackups(
folder: string,
config: WebdavConfig,
nameTemplate: string
) {
const endpoint = getEndpoint(config); const endpoint = getEndpoint(config);
return got(config.url + endpoint + config.backupDir + folder, { return got(config.url + endpoint + config.backupDir + folder, {
method: "PROPFIND" as Method, method: "PROPFIND" as Method,
@ -107,7 +112,10 @@ export function getBackups(folder: string, config: WebdavConfig) {
body: PROPFIND_BODY, body: PROPFIND_BODY,
}).then( }).then(
(value) => { (value) => {
return parseXmlBackupData(value.body); const data = parseXmlBackupData(value.body, config).sort(
(a, b) => b.lastEdit.toMillis() - a.lastEdit.toMillis()
);
return extractBackupInfo(data, nameTemplate);
}, },
(reason) => { (reason) => {
messageManager.error( messageManager.error(
@ -120,7 +128,59 @@ export function getBackups(folder: string, config: WebdavConfig) {
); );
} }
function parseXmlBackupData(body: string) { function extractBackupInfo(backups: WebdavBackup[], template: string) {
const regex = new RegExp(templateToRegexp(template));
for (const elem of backups) {
const match = elem.name.match(regex);
if (match?.groups?.date) {
let format = "yyyy-LL-dd";
let date = match.groups.date;
if (match.groups.hour) {
format += "+HHmm";
date += `+${match.groups.hour}`;
} else if (match.groups.hour_12) {
format += "+hhmma";
date += `+${match.groups.hour_12}`;
}
elem.creationDate = DateTime.fromFormat(date, format);
}
if(match?.groups?.version){
elem.haVersion = match.groups.version
}
}
return backups;
}
export function deleteBackup(path: string, config: WebdavConfig) {
const endpoint = getEndpoint(config);
return got
.delete(config.url + endpoint + path, {
headers: {
authorization:
"Basic " +
Buffer.from(config.username + ":" + config.password).toString(
"base64"
),
},
})
.then(
(response) => {
return response;
},
(reason) => {
messageManager.error(
"Fail to delete backup in webdav",
reason?.message
);
logger.error(`Fail to delete backup in Cloud`);
logger.error(reason);
return Promise.reject(reason);
}
);
}
function parseXmlBackupData(body: string, config: WebdavConfig) {
const parser = new XMLParser(); const parser = new XMLParser();
const data = parser.parse(body); const data = parser.parse(body);
const multistatus = data["d:multistatus"]; const multistatus = data["d:multistatus"];
@ -141,6 +201,7 @@ function parseXmlBackupData(body: string) {
lastEdit: lastEdit, lastEdit: lastEdit,
size: propstat["d:prop"]["d:getcontentlength"], size: propstat["d:prop"]["d:getcontentlength"],
name: name, name: name,
path: href.replace(getEndpoint(config), ""),
}); });
} }
} }

View File

@ -48,4 +48,4 @@ export interface CronConfig {
export interface AutoCleanConfig { export interface AutoCleanConfig {
enabled: boolean; enabled: boolean;
nbrToKeep?: number; nbrToKeep?: number;
} }

View File

@ -5,4 +5,12 @@ export interface WebdavBackup {
name: string; name: string;
size: number; size: number;
lastEdit: DateTime; lastEdit: DateTime;
path: string;
haVersion?: string;
creationDate?: DateTime;
}
export interface WebdavDelete {
path: string;
} }

View File

@ -1,4 +1,4 @@
import Joi, { not } from "joi"; import Joi from "joi";
import { WebdavEndpointType } from "./webdavConfig.js"; import { WebdavEndpointType } from "./webdavConfig.js";

View File

@ -0,0 +1,5 @@
import Joi from "joi";
export const WebdavDeleteValidation = {
path: Joi.string().not().empty().required()
}

View File

@ -20,7 +20,7 @@
"pretty-bytes": "^6.0.0", "pretty-bytes": "^6.0.0",
"roboto-fontface": "*", "roboto-fontface": "*",
"vue": "^3.2.40", "vue": "^3.2.40",
"vuetify": "3.0.0-beta.15", "vuetify": "3.0.1",
"webfontloader": "^1.0.0" "webfontloader": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -25,7 +25,7 @@ specifiers:
vite-plugin-vuetify: ^1.0.0-alpha.12 vite-plugin-vuetify: ^1.0.0-alpha.12
vue: ^3.2.40 vue: ^3.2.40
vue-tsc: 1.0.7 vue-tsc: 1.0.7
vuetify: 3.0.0-beta.15 vuetify: 3.0.1
webfontloader: ^1.0.0 webfontloader: ^1.0.0
dependencies: dependencies:
@ -38,7 +38,7 @@ dependencies:
pretty-bytes: 6.0.0 pretty-bytes: 6.0.0
roboto-fontface: 0.10.0 roboto-fontface: 0.10.0
vue: 3.2.40 vue: 3.2.40
vuetify: 3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y vuetify: 3.0.1_vdkwhj2kz5kpysy3rwb432nm3y
webfontloader: 1.6.28 webfontloader: 1.6.28
devDependencies: devDependencies:
@ -55,7 +55,7 @@ devDependencies:
prettier: 2.7.1 prettier: 2.7.1
typescript: 4.7.4 typescript: 4.7.4
vite: 3.1.7 vite: 3.1.7
vite-plugin-vuetify: 1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy vite-plugin-vuetify: 1.0.0-alpha.17_3jvrp5vztr3d3x64xppq7ybday
vue-tsc: 1.0.7_typescript@4.7.4 vue-tsc: 1.0.7_typescript@4.7.4
packages: packages:
@ -484,7 +484,7 @@ packages:
'@types/node': 16.11.65 '@types/node': 16.11.65
dev: true dev: true
/@vuetify/loader-shared/1.6.0_l2xw63bflzffxrhpro723k3mhy: /@vuetify/loader-shared/1.6.0_vue@3.2.40+vuetify@3.0.1:
resolution: {integrity: sha512-mRvswe5SIecagmKkL1c0UQx5V9ACKLyEGegOOe5vC3ccFH8rMbyI4AVZaAKm/EnGXKirWJ1umL8RQFcGd+C0tw==} resolution: {integrity: sha512-mRvswe5SIecagmKkL1c0UQx5V9ACKLyEGegOOe5vC3ccFH8rMbyI4AVZaAKm/EnGXKirWJ1umL8RQFcGd+C0tw==}
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
@ -493,7 +493,7 @@ packages:
find-cache-dir: 3.3.2 find-cache-dir: 3.3.2
upath: 2.0.1 upath: 2.0.1
vue: 3.2.40 vue: 3.2.40
vuetify: 3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y vuetify: 3.0.1_vdkwhj2kz5kpysy3rwb432nm3y
/acorn-jsx/5.3.2_acorn@8.8.0: /acorn-jsx/5.3.2_acorn@8.8.0:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -2157,18 +2157,18 @@ packages:
spdx-expression-parse: 3.0.1 spdx-expression-parse: 3.0.1
dev: true dev: true
/vite-plugin-vuetify/1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy: /vite-plugin-vuetify/1.0.0-alpha.17_3jvrp5vztr3d3x64xppq7ybday:
resolution: {integrity: sha512-lP4vG+Z3LnIEjI8nSE8/MJwDRQK5OnS2i4zHDu36yyD4E7wmPKyIkWJdkfBIsx/O+PU7dGc/3MeLARgr+RErEQ==} resolution: {integrity: sha512-lP4vG+Z3LnIEjI8nSE8/MJwDRQK5OnS2i4zHDu36yyD4E7wmPKyIkWJdkfBIsx/O+PU7dGc/3MeLARgr+RErEQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
peerDependencies: peerDependencies:
vite: ^2.7.0 || ^3.0.0 vite: ^2.7.0 || ^3.0.0
vuetify: ^3.0.0-beta.4 vuetify: ^3.0.0-beta.4
dependencies: dependencies:
'@vuetify/loader-shared': 1.6.0_l2xw63bflzffxrhpro723k3mhy '@vuetify/loader-shared': 1.6.0_vue@3.2.40+vuetify@3.0.1
debug: 4.3.4 debug: 4.3.4
upath: 2.0.1 upath: 2.0.1
vite: 3.1.7 vite: 3.1.7
vuetify: 3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y vuetify: 3.0.1_vdkwhj2kz5kpysy3rwb432nm3y
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- vue - vue
@ -2259,8 +2259,8 @@ packages:
'@vue/server-renderer': 3.2.40_vue@3.2.40 '@vue/server-renderer': 3.2.40_vue@3.2.40
'@vue/shared': 3.2.40 '@vue/shared': 3.2.40
/vuetify/3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y: /vuetify/3.0.1_vdkwhj2kz5kpysy3rwb432nm3y:
resolution: {integrity: sha512-Tw4StO4JJxwzN7RIAJ+nBVTaftlk3TqfqCtMLwJY/SO/ciVru+sHtahOpXIbp8B43d/DIRql3mfnrz1ooTSXtQ==} resolution: {integrity: sha512-Vl4wYB4mCm6GFK6Q9KZDK+HM3YKI7md7BoUPwbgqZj4bkofjQ/8NVSRQQpTcwk0YoQrgw6qj0QaOtP5zitkS1Q==}
engines: {node: ^12.20 || >=14.13} engines: {node: ^12.20 || >=14.13}
peerDependencies: peerDependencies:
vite-plugin-vuetify: ^1.0.0-alpha.12 vite-plugin-vuetify: ^1.0.0-alpha.12
@ -2275,7 +2275,7 @@ packages:
webpack-plugin-vuetify: webpack-plugin-vuetify:
optional: true optional: true
dependencies: dependencies:
vite-plugin-vuetify: 1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy vite-plugin-vuetify: 1.0.0-alpha.17_3jvrp5vztr3d3x64xppq7ybday
vue: 3.2.40 vue: 3.2.40
/webfontloader/1.6.28: /webfontloader/1.6.28:

View File

@ -0,0 +1,72 @@
<template>
<div>
<v-dialog
v-model="dialog"
:width="width"
:fullscreen="xs"
:persistent="loading"
>
<v-card>
<v-card-title class="d-flex align-center">
<v-icon color="red" class="mr-2">mdi-trash-can</v-icon> Delete Cloud
backup
</v-card-title>
<v-divider></v-divider>
<v-card-text>
Delete <v-code tag="code">{{ item?.name }}</v-code> backup in cloud ?
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="justify-end">
<v-btn color="success" @click="dialog = false" :disabled="loading">
Close
</v-btn>
<v-btn color="red" @click="confirm()" :loading="loading">
<v-icon>mdi-trash-can</v-icon> Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
import { deleteWebdabBackup } from "@/services/webdavService";
import type { WebdavBackup } from "@/types/webdav";
import { computed, ref } from "vue";
import { useDisplay } from "vuetify/dist/vuetify";
const { xs, mdAndDown } = useDisplay();
const dialog = ref(false);
const loading = ref(false);
const item = ref<WebdavBackup | null>(null);
const width = computed(() => {
if (xs.value) {
return undefined;
} else if (mdAndDown.value) {
return "80%";
} else {
return "50%";
}
});
function confirm() {
loading.value = true;
if (item.value) {
deleteWebdabBackup(item.value?.path)
.then(() => {
loading.value = false;
dialog.value = false;
})
.catch(() => {
loading.value = false;
});
}
}
function open(value: WebdavBackup) {
item.value = value;
dialog.value = true;
}
defineExpose({ open });
</script>

View File

@ -21,6 +21,7 @@
:key="item.id" :key="item.id"
:item="item" :item="item"
:index="index" :index="index"
@delete="deleteBackup"
> >
</cloud-list-item> </cloud-list-item>
</v-list> </v-list>
@ -45,6 +46,7 @@
:key="item.id" :key="item.id"
:item="item" :item="item"
:index="index" :index="index"
@delete="deleteBackup"
> >
</cloud-list-item> </cloud-list-item>
</v-list> </v-list>
@ -54,20 +56,22 @@
</v-row> </v-row>
</v-card-text> </v-card-text>
</v-card> </v-card>
<cloud-delete-dialog ref="deleteDialog"></cloud-delete-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { onBeforeUnmount, ref } from "vue";
import type { WebdavBackup } from "@/types/webdav"; import type { WebdavBackup } from "@/types/webdav";
import { import {
getAutoBackupList, getAutoBackupList,
getManualBackupList, getManualBackupList,
} from "@/services/webdavService"; } from "@/services/webdavService";
import CloudDeleteDialog from "./CloudDeleteDialog.vue";
import CloudListItem from "./CloudListItem.vue"; import CloudListItem from "./CloudListItem.vue";
const popup = ref(false); const deleteDialog = ref<InstanceType<typeof CloudDeleteDialog> | null>(null);
const deleteItem = ref<WebdavBackup | null>(null);
const autoBackups = ref<WebdavBackup[]>([]); const autoBackups = ref<WebdavBackup[]>([]);
const manualBackups = ref<WebdavBackup[]>([]); const manualBackups = ref<WebdavBackup[]>([]);
function refreshBackup() { function refreshBackup() {
@ -78,5 +82,16 @@ function refreshBackup() {
manualBackups.value = value; manualBackups.value = value;
}); });
} }
function deleteBackup(item: WebdavBackup) {
deleteItem.value = item;
deleteDialog.value?.open(item);
}
refreshBackup(); refreshBackup();
const interval = setInterval(refreshBackup, 2000);
onBeforeUnmount(() => {
clearInterval(interval);
});
</script> </script>

View File

@ -1,46 +1,94 @@
<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>
<v-list-item-title class="text-deep-orange-darken-3">{{ <v-list-item-title>{{ item.name }}</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>
<v-chip <v-chip
color="primary" color="primary"
variant="flat" variant="flat"
size="small" size="small"
class="mr-2" class="mr-1"
v-show="!detail" v-show="!detail"
> >
{{ {{
DateTime.fromISO(item.lastEdit).toLocaleString( DateTime.fromISO(item.lastEdit).toLocaleString(
DateTime.DATETIME_SHORT DateTime.DATETIME_MED
) )
}} }}
</v-chip> </v-chip>
</v-scroll-x-transition> </v-scroll-x-transition>
<v-btn variant="text" icon color="secondary" @click="detail = !detail"> <v-btn variant="text" icon color="success" @click="detail = !detail">
<v-icon>{{ detail ? "mdi-chevron-up" : "mdi-information" }}</v-icon> <v-icon>{{ detail ? "mdi-chevron-up" : "mdi-information" }}</v-icon>
</v-btn> </v-btn>
</template> </template>
</v-list-item> </v-list-item>
<v-expand-transition> <v-expand-transition>
<v-card v-show="detail" variant="tonal" color="secondary" rounded="0"> <v-card v-show="detail" variant="tonal" color="secondary" rounded="0">
<v-card-text class="d-flex justify-center"> <v-card-text>
<v-chip color="primary" variant="flat" class="mr-2" label> <v-row>
<v-icon start icon="mdi-pencil"></v-icon> <v-col class="d-flex justify-center">
{{ <v-tooltip text="Creation" location="top">
DateTime.fromISO(item.lastEdit).toLocaleString( <template v-slot:activator="{ props }">
DateTime.DATETIME_SHORT <v-chip
) color="primary"
}} variant="flat"
</v-chip> class="mr-2"
<v-chip color="success" variant="flat" label> label
<v-icon start icon="mdi-database"></v-icon> v-bind="props"
{{ prettyBytes(item.size) }} >
</v-chip> <v-icon start icon="mdi-folder-plus"></v-icon>
{{
item.creationDate
? DateTime.fromISO(item.creationDate).toLocaleString(
DateTime.DATETIME_MED
)
: "UNKNOWN"
}}
</v-chip>
</template>
</v-tooltip>
<v-tooltip text="Home Assistant Version" location="top">
<template v-slot:activator="{ props }">
<v-chip color="success" variant="flat" label v-bind="props">
<v-icon start icon="mdi-home-assistant"></v-icon>
{{ item.haVersion ? item.haVersion : "UNKNOWN" }}
</v-chip>
</template>
</v-tooltip>
</v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-center">
<v-tooltip text="Last edit" location="top">
<template v-slot:activator="{ props }">
<v-chip
color="primary"
variant="flat"
class="mr-2"
label
v-bind="props"
>
<v-icon start icon="mdi-pencil"></v-icon>
{{
DateTime.fromISO(item.lastEdit).toLocaleString(
DateTime.DATETIME_MED
)
}}
</v-chip>
</template>
</v-tooltip>
<v-tooltip text="Size" location="top">
<template v-slot:activator="{ props }">
<v-chip color="success" variant="flat" label v-bind="props">
<v-icon start icon="mdi-database"></v-icon>
{{ prettyBytes(item.size) }}
</v-chip>
</template>
</v-tooltip>
</v-col>
</v-row>
</v-card-text> </v-card-text>
<v-divider class="mx-4"></v-divider> <v-divider class="mx-4"></v-divider>
<v-card-actions class="justify-center"> <v-card-actions class="justify-center">
@ -51,7 +99,7 @@
</v-btn> </v-btn>
</template> </template>
</v-tooltip> </v-tooltip>
<v-btn variant="outlined" color="red"> <v-btn variant="outlined" color="red" @click="emits('delete', item)">
<v-icon>mdi-trash-can</v-icon> <v-icon>mdi-trash-can</v-icon>
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -61,8 +109,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { WebdavBackup } from "@/types/webdav"; import type { WebdavBackup } from "@/types/webdav";
import prettyBytes from "pretty-bytes";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import prettyBytes from "pretty-bytes";
import { ref } from "vue"; import { ref } from "vue";
const detail = ref(false); const detail = ref(false);
@ -70,4 +118,8 @@ defineProps<{
item: WebdavBackup; item: WebdavBackup;
index: number; index: number;
}>(); }>();
const emits = defineEmits<{
(e: "delete", item: WebdavBackup): void;
}>();
</script> </script>

View File

@ -8,3 +8,13 @@ export function getAutoBackupList() {
export function getManualBackupList() { export function getManualBackupList() {
return kyClient.get("webdav/backup/manual").json<WebdavBackup[]>(); return kyClient.get("webdav/backup/manual").json<WebdavBackup[]>();
} }
export function deleteWebdabBackup(path: string) {
return kyClient
.delete("webdav", {
json: {
path: path,
},
})
.text();
}

View File

@ -1,8 +1,13 @@
import type { DateTime } from "luxon";
export interface WebdavBackup { export interface WebdavBackup {
id: string; id: string;
name: string; name: string;
size: number; size: number;
lastEdit: string; lastEdit: string;
} path: string;
haVersion?: string;
creationDate?: string;
}
export interface WebdavDeletePayload {
path: string;
}