mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-12-23 22:46:44 +01:00
🔨 Add delete for cloud
This commit is contained in:
parent
84d45f6236
commit
b12e6f5111
11
nextcloud_backup/backend.code-workspace
Normal file
11
nextcloud_backup/backend.code-workspace
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "backend"
|
||||
},
|
||||
{
|
||||
"path": "frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
@ -1,26 +1,23 @@
|
||||
import express from "express";
|
||||
import Joi from "joi";
|
||||
import {
|
||||
getWebdavConfig,
|
||||
validateWebdavConfig,
|
||||
} from "../services/webdavConfigService.js";
|
||||
import * as webdavService from "../services/webdavService.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();
|
||||
|
||||
webdavRouter.get("/backup/auto", (req, res, next) => {
|
||||
const config = getWebdavConfig();
|
||||
validateWebdavConfig(config)
|
||||
.then(() => {
|
||||
webdavService
|
||||
.getBackups(pathTools.auto, config)
|
||||
.then((value) => {
|
||||
res.json(value);
|
||||
})
|
||||
.catch((reason) => {
|
||||
res.status(500);
|
||||
res.json(reason);
|
||||
});
|
||||
.then(async () => {
|
||||
const value = await webdavService
|
||||
.getBackups(pathTools.auto, config);
|
||||
res.json(value);
|
||||
})
|
||||
.catch((reason) => {
|
||||
res.status(500);
|
||||
@ -31,16 +28,10 @@ webdavRouter.get("/backup/auto", (req, res, next) => {
|
||||
webdavRouter.get("/backup/manual", (req, res, next) => {
|
||||
const config = getWebdavConfig();
|
||||
validateWebdavConfig(config)
|
||||
.then(() => {
|
||||
webdavService
|
||||
.getBackups(pathTools.manual, config)
|
||||
.then((value) => {
|
||||
res.json(value);
|
||||
})
|
||||
.catch((reason) => {
|
||||
res.status(500);
|
||||
res.json(reason);
|
||||
});
|
||||
.then(async () => {
|
||||
const value = await webdavService
|
||||
.getBackups(pathTools.manual, config);
|
||||
res.json(value);
|
||||
})
|
||||
.catch((reason) => {
|
||||
res.status(500);
|
||||
@ -48,4 +39,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;
|
||||
|
@ -2,20 +2,19 @@ import fs from "fs";
|
||||
import Joi from "joi";
|
||||
import logger from "../config/winston.js";
|
||||
import { default_root } from "../tools/pathTools.js";
|
||||
import WebdavConfigValidation from "../types/services/webdavConfigValidation.js";
|
||||
import {
|
||||
WebdavConfig,
|
||||
WebdavEndpointType,
|
||||
WebdavEndpointType
|
||||
} from "../types/services/webdavConfig.js";
|
||||
import WebdavConfigValidation from "../types/services/webdavConfigValidation.js";
|
||||
|
||||
const webdavConfigPath = "/data/webdavConfigV2.json";
|
||||
|
||||
const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username";
|
||||
|
||||
export function validateWebdavConfig(config: WebdavConfig) {
|
||||
const validator = Joi.object(WebdavConfigValidation);
|
||||
return validator.validateAsync(config, {
|
||||
abortEarly: false
|
||||
abortEarly: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -35,16 +34,22 @@ export function getWebdavConfig(): WebdavConfig {
|
||||
}
|
||||
|
||||
export function getEndpoint(config: WebdavConfig) {
|
||||
let endpoint: string;
|
||||
|
||||
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) {
|
||||
return config.webdavEndpoint.customEndpoint.replace(
|
||||
endpoint = config.webdavEndpoint.customEndpoint.replace(
|
||||
"$username",
|
||||
config.username
|
||||
);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
if (endpoint.endsWith("/")) {
|
||||
return endpoint.slice(0, -1);
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
export function getWebdavDefaultConfig(): WebdavConfig {
|
||||
|
@ -107,7 +107,7 @@ export function getBackups(folder: string, config: WebdavConfig) {
|
||||
body: PROPFIND_BODY,
|
||||
}).then(
|
||||
(value) => {
|
||||
return parseXmlBackupData(value.body);
|
||||
return parseXmlBackupData(value.body, config);
|
||||
},
|
||||
(reason) => {
|
||||
messageManager.error(
|
||||
@ -120,7 +120,28 @@ export function getBackups(folder: string, config: WebdavConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
function parseXmlBackupData(body: string) {
|
||||
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 data = parser.parse(body);
|
||||
const multistatus = data["d:multistatus"];
|
||||
@ -141,6 +162,7 @@ function parseXmlBackupData(body: string) {
|
||||
lastEdit: lastEdit,
|
||||
size: propstat["d:prop"]["d:getcontentlength"],
|
||||
name: name,
|
||||
path: href.replace(getEndpoint(config), "")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5,4 +5,10 @@ export interface WebdavBackup {
|
||||
name: string;
|
||||
size: number;
|
||||
lastEdit: DateTime;
|
||||
path: string;
|
||||
}
|
||||
|
||||
|
||||
export interface WebdavDelete {
|
||||
path: string;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import Joi, { not } from "joi";
|
||||
import Joi from "joi";
|
||||
import { WebdavEndpointType } from "./webdavConfig.js";
|
||||
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
import Joi from "joi";
|
||||
|
||||
export const WebdavDeleteValidation = {
|
||||
path: Joi.string().not().empty().required()
|
||||
}
|
@ -20,7 +20,7 @@
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.2.40",
|
||||
"vuetify": "3.0.0-beta.15",
|
||||
"vuetify": "3.0.0",
|
||||
"webfontloader": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
22
nextcloud_backup/frontend/pnpm-lock.yaml
generated
22
nextcloud_backup/frontend/pnpm-lock.yaml
generated
@ -25,7 +25,7 @@ specifiers:
|
||||
vite-plugin-vuetify: ^1.0.0-alpha.12
|
||||
vue: ^3.2.40
|
||||
vue-tsc: 1.0.7
|
||||
vuetify: 3.0.0-beta.15
|
||||
vuetify: 3.0.0
|
||||
webfontloader: ^1.0.0
|
||||
|
||||
dependencies:
|
||||
@ -38,7 +38,7 @@ dependencies:
|
||||
pretty-bytes: 6.0.0
|
||||
roboto-fontface: 0.10.0
|
||||
vue: 3.2.40
|
||||
vuetify: 3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y
|
||||
vuetify: 3.0.0_vdkwhj2kz5kpysy3rwb432nm3y
|
||||
webfontloader: 1.6.28
|
||||
|
||||
devDependencies:
|
||||
@ -55,7 +55,7 @@ devDependencies:
|
||||
prettier: 2.7.1
|
||||
typescript: 4.7.4
|
||||
vite: 3.1.7
|
||||
vite-plugin-vuetify: 1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy
|
||||
vite-plugin-vuetify: 1.0.0-alpha.17_al4u2thft73kbehmgonig46c5m
|
||||
vue-tsc: 1.0.7_typescript@4.7.4
|
||||
|
||||
packages:
|
||||
@ -484,7 +484,7 @@ packages:
|
||||
'@types/node': 16.11.65
|
||||
dev: true
|
||||
|
||||
/@vuetify/loader-shared/1.6.0_l2xw63bflzffxrhpro723k3mhy:
|
||||
/@vuetify/loader-shared/1.6.0_vue@3.2.40+vuetify@3.0.0:
|
||||
resolution: {integrity: sha512-mRvswe5SIecagmKkL1c0UQx5V9ACKLyEGegOOe5vC3ccFH8rMbyI4AVZaAKm/EnGXKirWJ1umL8RQFcGd+C0tw==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
@ -493,7 +493,7 @@ packages:
|
||||
find-cache-dir: 3.3.2
|
||||
upath: 2.0.1
|
||||
vue: 3.2.40
|
||||
vuetify: 3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y
|
||||
vuetify: 3.0.0_vdkwhj2kz5kpysy3rwb432nm3y
|
||||
|
||||
/acorn-jsx/5.3.2_acorn@8.8.0:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
@ -2157,18 +2157,18 @@ packages:
|
||||
spdx-expression-parse: 3.0.1
|
||||
dev: true
|
||||
|
||||
/vite-plugin-vuetify/1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy:
|
||||
/vite-plugin-vuetify/1.0.0-alpha.17_al4u2thft73kbehmgonig46c5m:
|
||||
resolution: {integrity: sha512-lP4vG+Z3LnIEjI8nSE8/MJwDRQK5OnS2i4zHDu36yyD4E7wmPKyIkWJdkfBIsx/O+PU7dGc/3MeLARgr+RErEQ==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
vite: ^2.7.0 || ^3.0.0
|
||||
vuetify: ^3.0.0-beta.4
|
||||
dependencies:
|
||||
'@vuetify/loader-shared': 1.6.0_l2xw63bflzffxrhpro723k3mhy
|
||||
'@vuetify/loader-shared': 1.6.0_vue@3.2.40+vuetify@3.0.0
|
||||
debug: 4.3.4
|
||||
upath: 2.0.1
|
||||
vite: 3.1.7
|
||||
vuetify: 3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y
|
||||
vuetify: 3.0.0_vdkwhj2kz5kpysy3rwb432nm3y
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- vue
|
||||
@ -2259,8 +2259,8 @@ packages:
|
||||
'@vue/server-renderer': 3.2.40_vue@3.2.40
|
||||
'@vue/shared': 3.2.40
|
||||
|
||||
/vuetify/3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y:
|
||||
resolution: {integrity: sha512-Tw4StO4JJxwzN7RIAJ+nBVTaftlk3TqfqCtMLwJY/SO/ciVru+sHtahOpXIbp8B43d/DIRql3mfnrz1ooTSXtQ==}
|
||||
/vuetify/3.0.0_vdkwhj2kz5kpysy3rwb432nm3y:
|
||||
resolution: {integrity: sha512-0olLmKWb+oTaebGTM02+1fWaAgnGDuIt86K3jDv2LnALQ2atSzhDS1SSASJsJCZ2PgFBdDAeJVLcNWWavRYAlQ==}
|
||||
engines: {node: ^12.20 || >=14.13}
|
||||
peerDependencies:
|
||||
vite-plugin-vuetify: ^1.0.0-alpha.12
|
||||
@ -2275,7 +2275,7 @@ packages:
|
||||
webpack-plugin-vuetify:
|
||||
optional: true
|
||||
dependencies:
|
||||
vite-plugin-vuetify: 1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy
|
||||
vite-plugin-vuetify: 1.0.0-alpha.17_al4u2thft73kbehmgonig46c5m
|
||||
vue: 3.2.40
|
||||
|
||||
/webfontloader/1.6.28:
|
||||
|
@ -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>
|
@ -21,6 +21,7 @@
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:index="index"
|
||||
@delete="deleteBackup"
|
||||
>
|
||||
</cloud-list-item>
|
||||
</v-list>
|
||||
@ -45,6 +46,7 @@
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:index="index"
|
||||
@delete="deleteBackup"
|
||||
>
|
||||
</cloud-list-item>
|
||||
</v-list>
|
||||
@ -54,20 +56,22 @@
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<cloud-delete-dialog ref="deleteDialog"></cloud-delete-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { onBeforeUnmount, ref } from "vue";
|
||||
import type { WebdavBackup } from "@/types/webdav";
|
||||
import {
|
||||
getAutoBackupList,
|
||||
getManualBackupList,
|
||||
} from "@/services/webdavService";
|
||||
|
||||
import CloudDeleteDialog from "./CloudDeleteDialog.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 manualBackups = ref<WebdavBackup[]>([]);
|
||||
function refreshBackup() {
|
||||
@ -78,5 +82,16 @@ function refreshBackup() {
|
||||
manualBackups.value = value;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteBackup(item: WebdavBackup) {
|
||||
deleteItem.value = item;
|
||||
deleteDialog.value?.open(item);
|
||||
}
|
||||
refreshBackup();
|
||||
|
||||
const interval = setInterval(refreshBackup, 2000);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
@ -1,27 +1,25 @@
|
||||
<template>
|
||||
<v-divider v-if="index != 0" color="grey-darken-3"></v-divider>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="text-deep-orange-darken-3">{{
|
||||
item.name
|
||||
}}</v-list-item-title>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-scroll-x-transition>
|
||||
<v-chip
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
class="mr-1"
|
||||
v-show="!detail"
|
||||
>
|
||||
{{
|
||||
DateTime.fromISO(item.lastEdit).toLocaleString(
|
||||
DateTime.DATETIME_SHORT
|
||||
DateTime.DATETIME_MED
|
||||
)
|
||||
}}
|
||||
</v-chip>
|
||||
</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-btn>
|
||||
</template>
|
||||
@ -33,7 +31,7 @@
|
||||
<v-icon start icon="mdi-pencil"></v-icon>
|
||||
{{
|
||||
DateTime.fromISO(item.lastEdit).toLocaleString(
|
||||
DateTime.DATETIME_SHORT
|
||||
DateTime.DATETIME_MED
|
||||
)
|
||||
}}
|
||||
</v-chip>
|
||||
@ -51,7 +49,7 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
</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-btn>
|
||||
</v-card-actions>
|
||||
@ -61,8 +59,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { WebdavBackup } from "@/types/webdav";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { DateTime } from "luxon";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { ref } from "vue";
|
||||
|
||||
const detail = ref(false);
|
||||
@ -70,4 +68,8 @@ defineProps<{
|
||||
item: WebdavBackup;
|
||||
index: number;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "delete", item: WebdavBackup): void;
|
||||
}>();
|
||||
</script>
|
||||
|
@ -8,3 +8,13 @@ export function getAutoBackupList() {
|
||||
export function getManualBackupList() {
|
||||
return kyClient.get("webdav/backup/manual").json<WebdavBackup[]>();
|
||||
}
|
||||
|
||||
export function deleteWebdabBackup(path: string) {
|
||||
return kyClient
|
||||
.delete("webdav", {
|
||||
json: {
|
||||
path: path,
|
||||
},
|
||||
})
|
||||
.text();
|
||||
}
|
||||
|
@ -5,4 +5,9 @@ export interface WebdavBackup {
|
||||
name: string;
|
||||
size: number;
|
||||
lastEdit: string;
|
||||
}
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface WebdavDeletePayload {
|
||||
path: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user