diff --git a/nextcloud_backup/backend.code-workspace b/nextcloud_backup/backend.code-workspace new file mode 100644 index 0000000..0cb4068 --- /dev/null +++ b/nextcloud_backup/backend.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "backend" + }, + { + "path": "frontend" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/nextcloud_backup/backend/src/routes/webdav.ts b/nextcloud_backup/backend/src/routes/webdav.ts index c261b2e..8f55d3f 100644 --- a/nextcloud_backup/backend/src/routes/webdav.ts +++ b/nextcloud_backup/backend/src/routes/webdav.ts @@ -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; diff --git a/nextcloud_backup/backend/src/services/webdavConfigService.ts b/nextcloud_backup/backend/src/services/webdavConfigService.ts index 3ed380c..4050a81 100644 --- a/nextcloud_backup/backend/src/services/webdavConfigService.ts +++ b/nextcloud_backup/backend/src/services/webdavConfigService.ts @@ -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 { diff --git a/nextcloud_backup/backend/src/services/webdavService.ts b/nextcloud_backup/backend/src/services/webdavService.ts index 2eb71c5..8c7d7f6 100644 --- a/nextcloud_backup/backend/src/services/webdavService.ts +++ b/nextcloud_backup/backend/src/services/webdavService.ts @@ -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), "") }); } } diff --git a/nextcloud_backup/backend/src/types/services/webdav.ts b/nextcloud_backup/backend/src/types/services/webdav.ts index 413ddea..e837a3e 100644 --- a/nextcloud_backup/backend/src/types/services/webdav.ts +++ b/nextcloud_backup/backend/src/types/services/webdav.ts @@ -5,4 +5,10 @@ export interface WebdavBackup { name: string; size: number; lastEdit: DateTime; + path: string; +} + + +export interface WebdavDelete { + path: string; } \ No newline at end of file diff --git a/nextcloud_backup/backend/src/types/services/webdavConfigValidation.ts b/nextcloud_backup/backend/src/types/services/webdavConfigValidation.ts index a1b8004..f4946a7 100644 --- a/nextcloud_backup/backend/src/types/services/webdavConfigValidation.ts +++ b/nextcloud_backup/backend/src/types/services/webdavConfigValidation.ts @@ -1,4 +1,4 @@ -import Joi, { not } from "joi"; +import Joi from "joi"; import { WebdavEndpointType } from "./webdavConfig.js"; diff --git a/nextcloud_backup/backend/src/types/services/webdavValidation.ts b/nextcloud_backup/backend/src/types/services/webdavValidation.ts new file mode 100644 index 0000000..4e1fa99 --- /dev/null +++ b/nextcloud_backup/backend/src/types/services/webdavValidation.ts @@ -0,0 +1,5 @@ +import Joi from "joi"; + +export const WebdavDeleteValidation = { + path: Joi.string().not().empty().required() +} \ No newline at end of file diff --git a/nextcloud_backup/frontend/package.json b/nextcloud_backup/frontend/package.json index 1066fff..c68d45b 100644 --- a/nextcloud_backup/frontend/package.json +++ b/nextcloud_backup/frontend/package.json @@ -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": { diff --git a/nextcloud_backup/frontend/pnpm-lock.yaml b/nextcloud_backup/frontend/pnpm-lock.yaml index 4cf5d0e..0d04b62 100644 --- a/nextcloud_backup/frontend/pnpm-lock.yaml +++ b/nextcloud_backup/frontend/pnpm-lock.yaml @@ -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: diff --git a/nextcloud_backup/frontend/src/components/cloud/CloudDeleteDialog.vue b/nextcloud_backup/frontend/src/components/cloud/CloudDeleteDialog.vue new file mode 100644 index 0000000..2fb2dec --- /dev/null +++ b/nextcloud_backup/frontend/src/components/cloud/CloudDeleteDialog.vue @@ -0,0 +1,72 @@ + + + diff --git a/nextcloud_backup/frontend/src/components/cloud/CloudList.vue b/nextcloud_backup/frontend/src/components/cloud/CloudList.vue index db5d57c..2381e8c 100644 --- a/nextcloud_backup/frontend/src/components/cloud/CloudList.vue +++ b/nextcloud_backup/frontend/src/components/cloud/CloudList.vue @@ -21,6 +21,7 @@ :key="item.id" :item="item" :index="index" + @delete="deleteBackup" > @@ -45,6 +46,7 @@ :key="item.id" :item="item" :index="index" + @delete="deleteBackup" > @@ -54,20 +56,22 @@ + diff --git a/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue b/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue index 4dfa6a3..f15ba5e 100644 --- a/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue +++ b/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue @@ -1,27 +1,25 @@ - + mdi-trash-can @@ -61,8 +59,8 @@ diff --git a/nextcloud_backup/frontend/src/services/webdavService.ts b/nextcloud_backup/frontend/src/services/webdavService.ts index 5f07076..c22e195 100644 --- a/nextcloud_backup/frontend/src/services/webdavService.ts +++ b/nextcloud_backup/frontend/src/services/webdavService.ts @@ -8,3 +8,13 @@ export function getAutoBackupList() { export function getManualBackupList() { return kyClient.get("webdav/backup/manual").json(); } + +export function deleteWebdabBackup(path: string) { + return kyClient + .delete("webdav", { + json: { + path: path, + }, + }) + .text(); +} diff --git a/nextcloud_backup/frontend/src/types/webdav.ts b/nextcloud_backup/frontend/src/types/webdav.ts index a361bb0..e946cc1 100644 --- a/nextcloud_backup/frontend/src/types/webdav.ts +++ b/nextcloud_backup/frontend/src/types/webdav.ts @@ -5,4 +5,9 @@ export interface WebdavBackup { name: string; size: number; lastEdit: string; -} \ No newline at end of file + path: string; +} + +export interface WebdavDeletePayload { + path: string; +}