mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-26 10:56:55 +01:00
Compare commits
No commits in common. "446e89a6c7feedba1a7037650117468172253b8f" and "07235fda1d992fc6f57966a1edb9ed5a6fadc257" have entirely different histories.
446e89a6c7
...
07235fda1d
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "backend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "frontend"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {}
|
|
||||||
}
|
|
@ -27,7 +27,7 @@ app.set("port", process.env.PORT || 3000);
|
|||||||
// );
|
// );
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
morgan("dev", { stream: { write: (message) => logger.debug(message) } })
|
morgan("dev", { stream: { write: (message) => logger.info(message) } })
|
||||||
);
|
);
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
@ -1,66 +1,51 @@
|
|||||||
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(async () => {
|
.then(() => {
|
||||||
const value = await webdavService
|
webdavService
|
||||||
.getBackups(pathTools.auto, config, backupConf.nameTemplate);
|
.getBackups(pathTools.auto, config)
|
||||||
|
.then((value) => {
|
||||||
res.json(value);
|
res.json(value);
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
res.status(500);
|
res.status(500);
|
||||||
res.json(reason);
|
res.json(reason);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
res.status(500);
|
||||||
|
res.json(reason);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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(async () => {
|
.then(() => {
|
||||||
const value = await webdavService
|
webdavService
|
||||||
.getBackups(pathTools.manual, config, backupConf.nameTemplate);
|
.getBackups(pathTools.manual, config)
|
||||||
|
.then((value) => {
|
||||||
res.json(value);
|
res.json(value);
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
res.status(500);
|
res.status(500);
|
||||||
res.json(reason);
|
res.json(reason);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
res.status(500);
|
||||||
|
res.json(reason);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
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;
|
||||||
@ -37,7 +39,7 @@ export function getBackupDefaultConfig(): BackupConfig {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
webdav: {
|
webdav: {
|
||||||
enabled: false,
|
enabled: false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
exclude: {
|
exclude: {
|
||||||
@ -47,18 +49,6 @@ 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+)?)"
|
|
||||||
);
|
|
||||||
}
|
}
|
@ -2,19 +2,20 @@ 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,22 +35,16 @@ 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) {
|
||||||
endpoint = NEXTCLOUD_ENDPOINT.replace("$username", config.username);
|
return NEXTCLOUD_ENDPOINT.replace("$username", config.username);
|
||||||
} else if (config.webdavEndpoint.customEndpoint) {
|
} else if (config.webdavEndpoint.customEndpoint) {
|
||||||
endpoint = config.webdavEndpoint.customEndpoint.replace(
|
return 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 {
|
||||||
|
@ -8,7 +8,6 @@ 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 =
|
||||||
@ -95,11 +94,7 @@ function createDirectory(path: string, config: WebdavConfig) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBackups(
|
export function getBackups(folder: string, config: WebdavConfig) {
|
||||||
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,
|
||||||
@ -112,10 +107,7 @@ export function getBackups(
|
|||||||
body: PROPFIND_BODY,
|
body: PROPFIND_BODY,
|
||||||
}).then(
|
}).then(
|
||||||
(value) => {
|
(value) => {
|
||||||
const data = parseXmlBackupData(value.body, config).sort(
|
return parseXmlBackupData(value.body);
|
||||||
(a, b) => b.lastEdit.toMillis() - a.lastEdit.toMillis()
|
|
||||||
);
|
|
||||||
return extractBackupInfo(data, nameTemplate);
|
|
||||||
},
|
},
|
||||||
(reason) => {
|
(reason) => {
|
||||||
messageManager.error(
|
messageManager.error(
|
||||||
@ -128,59 +120,7 @@ export function getBackups(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractBackupInfo(backups: WebdavBackup[], template: string) {
|
function parseXmlBackupData(body: 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"];
|
||||||
@ -201,7 +141,6 @@ function parseXmlBackupData(body: string, config: WebdavConfig) {
|
|||||||
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), ""),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,4 @@ 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;
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import Joi from "joi";
|
import Joi, { not } from "joi";
|
||||||
import { WebdavEndpointType } from "./webdavConfig.js";
|
import { WebdavEndpointType } from "./webdavConfig.js";
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import Joi from "joi";
|
|
||||||
|
|
||||||
export const WebdavDeleteValidation = {
|
|
||||||
path: Joi.string().not().empty().required()
|
|
||||||
}
|
|
@ -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.1",
|
"vuetify": "3.0.0-beta.15",
|
||||||
"webfontloader": "^1.0.0"
|
"webfontloader": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -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.1
|
vuetify: 3.0.0-beta.15
|
||||||
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.1_vdkwhj2kz5kpysy3rwb432nm3y
|
vuetify: 3.0.0-beta.15_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_3jvrp5vztr3d3x64xppq7ybday
|
vite-plugin-vuetify: 1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy
|
||||||
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_vue@3.2.40+vuetify@3.0.1:
|
/@vuetify/loader-shared/1.6.0_l2xw63bflzffxrhpro723k3mhy:
|
||||||
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.1_vdkwhj2kz5kpysy3rwb432nm3y
|
vuetify: 3.0.0-beta.15_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_3jvrp5vztr3d3x64xppq7ybday:
|
/vite-plugin-vuetify/1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy:
|
||||||
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_vue@3.2.40+vuetify@3.0.1
|
'@vuetify/loader-shared': 1.6.0_l2xw63bflzffxrhpro723k3mhy
|
||||||
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.1_vdkwhj2kz5kpysy3rwb432nm3y
|
vuetify: 3.0.0-beta.15_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.1_vdkwhj2kz5kpysy3rwb432nm3y:
|
/vuetify/3.0.0-beta.15_vdkwhj2kz5kpysy3rwb432nm3y:
|
||||||
resolution: {integrity: sha512-Vl4wYB4mCm6GFK6Q9KZDK+HM3YKI7md7BoUPwbgqZj4bkofjQ/8NVSRQQpTcwk0YoQrgw6qj0QaOtP5zitkS1Q==}
|
resolution: {integrity: sha512-Tw4StO4JJxwzN7RIAJ+nBVTaftlk3TqfqCtMLwJY/SO/ciVru+sHtahOpXIbp8B43d/DIRql3mfnrz1ooTSXtQ==}
|
||||||
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_3jvrp5vztr3d3x64xppq7ybday
|
vite-plugin-vuetify: 1.0.0-alpha.17_g64smqxqrz57uhsly7clskj5fy
|
||||||
vue: 3.2.40
|
vue: 3.2.40
|
||||||
|
|
||||||
/webfontloader/1.6.28:
|
/webfontloader/1.6.28:
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
<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,7 +21,6 @@
|
|||||||
: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>
|
||||||
@ -46,7 +45,6 @@
|
|||||||
: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>
|
||||||
@ -56,22 +54,20 @@
|
|||||||
</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 { onBeforeUnmount, ref } from "vue";
|
import { 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 deleteDialog = ref<InstanceType<typeof CloudDeleteDialog> | null>(null);
|
const popup = ref(false);
|
||||||
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() {
|
||||||
@ -82,16 +78,5 @@ 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>
|
||||||
|
@ -1,94 +1,46 @@
|
|||||||
<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>{{ item.name }}</v-list-item-title>
|
<v-list-item-title class="text-deep-orange-darken-3">{{
|
||||||
|
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-1"
|
class="mr-2"
|
||||||
v-show="!detail"
|
v-show="!detail"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
DateTime.fromISO(item.lastEdit).toLocaleString(
|
DateTime.fromISO(item.lastEdit).toLocaleString(
|
||||||
DateTime.DATETIME_MED
|
DateTime.DATETIME_SHORT
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</v-scroll-x-transition>
|
</v-scroll-x-transition>
|
||||||
|
|
||||||
<v-btn variant="text" icon color="success" @click="detail = !detail">
|
<v-btn variant="text" icon color="secondary" @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>
|
<v-card-text class="d-flex justify-center">
|
||||||
<v-row>
|
<v-chip color="primary" variant="flat" class="mr-2" label>
|
||||||
<v-col class="d-flex justify-center">
|
|
||||||
<v-tooltip text="Creation" 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-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>
|
<v-icon start icon="mdi-pencil"></v-icon>
|
||||||
{{
|
{{
|
||||||
DateTime.fromISO(item.lastEdit).toLocaleString(
|
DateTime.fromISO(item.lastEdit).toLocaleString(
|
||||||
DateTime.DATETIME_MED
|
DateTime.DATETIME_SHORT
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</template>
|
<v-chip color="success" variant="flat" label>
|
||||||
</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>
|
<v-icon start icon="mdi-database"></v-icon>
|
||||||
{{ prettyBytes(item.size) }}
|
{{ prettyBytes(item.size) }}
|
||||||
</v-chip>
|
</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">
|
||||||
@ -99,7 +51,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<v-btn variant="outlined" color="red" @click="emits('delete', item)">
|
<v-btn variant="outlined" color="red">
|
||||||
<v-icon>mdi-trash-can</v-icon>
|
<v-icon>mdi-trash-can</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
@ -109,8 +61,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { WebdavBackup } from "@/types/webdav";
|
import type { WebdavBackup } from "@/types/webdav";
|
||||||
import { DateTime } from "luxon";
|
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const detail = ref(false);
|
const detail = ref(false);
|
||||||
@ -118,8 +70,4 @@ defineProps<{
|
|||||||
item: WebdavBackup;
|
item: WebdavBackup;
|
||||||
index: number;
|
index: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(e: "delete", item: WebdavBackup): void;
|
|
||||||
}>();
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,13 +8,3 @@ 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();
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
|
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;
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user