diff --git a/nextcloud_backup/backend/package.json b/nextcloud_backup/backend/package.json index d17255d..4cc590a 100644 --- a/nextcloud_backup/backend/package.json +++ b/nextcloud_backup/backend/package.json @@ -23,6 +23,7 @@ "form-data": "4.0.0", "got": "12.3.0", "http-errors": "2.0.0", + "joi": "^17.6.1", "jquery": "3.6.0", "kleur": "^4.1.5", "luxon": "3.0.1", diff --git a/nextcloud_backup/backend/pnpm-lock.yaml b/nextcloud_backup/backend/pnpm-lock.yaml index 87682d8..7afc773 100644 --- a/nextcloud_backup/backend/pnpm-lock.yaml +++ b/nextcloud_backup/backend/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: form-data: 4.0.0 got: 12.3.0 http-errors: 2.0.0 + joi: ^17.6.1 jquery: 3.6.0 kleur: ^4.1.5 luxon: 3.0.1 @@ -48,6 +49,7 @@ dependencies: form-data: 4.0.0 got: 12.3.0 http-errors: 2.0.0 + joi: 17.6.1 jquery: 3.6.0 kleur: 4.1.5 luxon: 3.0.1 @@ -131,6 +133,16 @@ packages: transitivePeerDependencies: - supports-color + /@hapi/hoek/9.3.0: + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + dev: false + + /@hapi/topo/5.1.0: + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + dependencies: + '@hapi/hoek': 9.3.0 + dev: false + /@jridgewell/resolve-uri/3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} @@ -165,6 +177,20 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.13.0 + /@sideway/address/4.1.4: + resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} + dependencies: + '@hapi/hoek': 9.3.0 + dev: false + + /@sideway/formula/3.0.0: + resolution: {integrity: sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==} + dev: false + + /@sideway/pinpoint/2.0.0: + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + dev: false + /@sindresorhus/is/5.3.0: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} @@ -1493,6 +1519,16 @@ packages: /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /joi/17.6.1: + resolution: {integrity: sha512-Hl7/iBklIX345OCM1TiFSCZRVaAOLDGlWCp0Df2vWYgBgjkezaR7Kvm3joBciBHQjZj5sxXs859r6eqsRSlG8w==} + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.4 + '@sideway/formula': 3.0.0 + '@sideway/pinpoint': 2.0.0 + dev: false + /jquery/3.6.0: resolution: {integrity: sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==} dev: false diff --git a/nextcloud_backup/backend/src/routes/apiV2.ts b/nextcloud_backup/backend/src/routes/apiV2.ts index 582dd7f..736df57 100644 --- a/nextcloud_backup/backend/src/routes/apiV2.ts +++ b/nextcloud_backup/backend/src/routes/apiV2.ts @@ -1,9 +1,11 @@ import express from "express" +import configRouter from "./config.js"; import homeAssistant from "./homeAssistant.js" const router = express.Router(); router.use("/homeAssistant", homeAssistant) +router.use("/config", configRouter); export default router; \ No newline at end of file diff --git a/nextcloud_backup/backend/src/routes/config.ts b/nextcloud_backup/backend/src/routes/config.ts new file mode 100644 index 0000000..cd28ab7 --- /dev/null +++ b/nextcloud_backup/backend/src/routes/config.ts @@ -0,0 +1,20 @@ +import { config } from "dotenv"; +import express from "express"; +import { saveBackupConfig, validateBackupConfig } from "../services/configService.js"; + +const configRouter = express.Router(); + +configRouter.put("/backup", (req, res, next) => { + validateBackupConfig(req.body) + .then(() => { + saveBackupConfig(req.body); + res.status(204); + res.send(); + }) + .catch((error) => { + res.status(400); + res.json(error.details); + }); +}); + +export default configRouter; diff --git a/nextcloud_backup/backend/src/routes/homeAssistant.ts b/nextcloud_backup/backend/src/routes/homeAssistant.ts index 2d503f1..ebe8c6a 100644 --- a/nextcloud_backup/backend/src/routes/homeAssistant.ts +++ b/nextcloud_backup/backend/src/routes/homeAssistant.ts @@ -1,9 +1,9 @@ import express from "express"; import * as haOsService from "../services/homeAssistantService.js" -const router = express.Router(); +const homeAssistantRouter = express.Router(); -router.get("/backups/", (req, res, next) => { +homeAssistantRouter.get("/backups/", (req, res, next) => { haOsService.getBackups() .then((value)=>{ res.json(value.body.data.backups); @@ -14,6 +14,5 @@ router.get("/backups/", (req, res, next) => { }); -router.get("") -export default router; \ No newline at end of file +export default homeAssistantRouter; \ No newline at end of file diff --git a/nextcloud_backup/backend/src/services/configService.ts b/nextcloud_backup/backend/src/services/configService.ts new file mode 100644 index 0000000..ec3c000 --- /dev/null +++ b/nextcloud_backup/backend/src/services/configService.ts @@ -0,0 +1,52 @@ +import fs from "fs"; +import Joi from "joi" +import logger from "../config/winston.js"; +import { backupConfigValidation } from "../types/services/backupConfigValidation.js" +import { BackupConfig } from "../types/services/backupConfig.js" + + +const backupConfigPath = "/data/backupConfigV2.json"; + + +export function validateBackupConfig(config: BackupConfig){ + const validator = Joi.object(backupConfigValidation); + return validator.validateAsync(config); +} + +export function saveBackupConfig(config: BackupConfig){ + fs.writeFileSync(backupConfigPath, JSON.stringify(config, undefined, 2)); +} + +export function getBackupConfig(): BackupConfig { + if (!fs.existsSync(backupConfigPath)) { + logger.warn("Config file not found, creating default one !") + const defaultConfig = getBackupDefaultConfig(); + saveBackupConfig(defaultConfig); + return defaultConfig; + } else { + return JSON.parse(fs.readFileSync(backupConfigPath).toString()); + } +} + +export function getBackupDefaultConfig(): BackupConfig { + return { + nameTemplate: "{type}-{ha_version}-{date}_{hour}", + cron: [], + autoClean: { + homeAssistant: { + enabled: false, + }, + webdav: { + enabled: false + }, + }, + exclude: { + addon: [], + folder: [], + }, + autoStopAddon: [], + password: { + enabled: false, + } + } +} \ No newline at end of file diff --git a/nextcloud_backup/backend/src/types/services/backupConfig.ts b/nextcloud_backup/backend/src/types/services/backupConfig.ts new file mode 100644 index 0000000..8705e59 --- /dev/null +++ b/nextcloud_backup/backend/src/types/services/backupConfig.ts @@ -0,0 +1,51 @@ +export enum CronMode { + DAILY = "DAILY", + WEEKLY = "WEEKLY", + MONTHLY = "MONTHLY", + CUSTOM = "CUSTOM" +} + +export enum Weekday { + 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; +} \ No newline at end of file diff --git a/nextcloud_backup/backend/src/types/services/backupConfigValidation.ts b/nextcloud_backup/backend/src/types/services/backupConfigValidation.ts new file mode 100644 index 0000000..cbaf304 --- /dev/null +++ b/nextcloud_backup/backend/src/types/services/backupConfigValidation.ts @@ -0,0 +1,62 @@ +import Joi from "joi"; +import { CronMode } from "./backupConfig.js"; + +const CronConfigValidation = { + id: Joi.string().required().not().empty(), + mode: Joi.string() + .required() + .valid(CronMode.CUSTOM, CronMode.DAILY, CronMode.MONTHLY, CronMode.WEEKLY), + hour: Joi.alternatives().conditional("mode", { + is: CronMode.CUSTOM, + then: Joi.forbidden(), + otherwise: Joi.string() + .pattern(/^\d{2}:\d{2}$/) + .required(), + }), + weekday: Joi.alternatives().conditional("mode", { + is: CronMode.WEEKLY, + then: Joi.number().min(0).max(6).required(), + otherwise: Joi.forbidden(), + }), + monthDay: Joi.alternatives().conditional("mode", { + is: CronMode.MONTHLY, + then: Joi.number().min(1).max(28).required(), + otherwise: Joi.forbidden(), + }), + custom: Joi.alternatives().conditional("mode", { + is: CronMode.CUSTOM, + then: Joi.string().required(), + otherwise: Joi.forbidden(), + }), +}; + +const AutoCleanConfig = { + enabled: Joi.boolean().required(), + nbrToKeep: Joi.alternatives().conditional("enabled", { + is: true, + then: Joi.number().required().min(0), + otherwise: Joi.forbidden(), + }), +}; + +export const backupConfigValidation = { + nameTemplate: Joi.string().required().not().empty(), + cron: Joi.array().items(CronConfigValidation).required(), + autoClean: Joi.object({ + homeAssistant: Joi.object(AutoCleanConfig).required(), + webdav: Joi.object(AutoCleanConfig).required(), + }).required(), + exclude: Joi.object({ + addon: Joi.array().items(Joi.string().not().empty()), + folder: Joi.array().items(Joi.string().not().empty()), + }).required(), + autoStopAddon: Joi.array().items(Joi.string().not().empty()), + password: Joi.object({ + enabled: Joi.boolean().required(), + value: Joi.alternatives().conditional("enabled", { + is: true, + then: Joi.string().required().not().empty(), + otherwise: Joi.forbidden(), + }), + }), +};