diff --git a/nextcloud_backup/backend/package.json b/nextcloud_backup/backend/package.json index 859090d..b8d7852 100644 --- a/nextcloud_backup/backend/package.json +++ b/nextcloud_backup/backend/package.json @@ -15,14 +15,11 @@ "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"pnpm run watch-ts\" \"pnpm run watch-node\"" }, "dependencies": { - "@fortawesome/fontawesome-free": "6.1.2", "@typescript-eslint/eslint-plugin": "^5.38.0", "app-root-path": "3.0.0", - "bootstrap": "5.2.0", "cookie-parser": "1.4.6", "cron": "2.1.0", "debug": "4.3.4", - "ejs": "3.1.8", "errorhandler": "^1.5.1", "express": "4.18.1", "form-data": "4.0.0", @@ -31,6 +28,7 @@ "jquery": "3.6.0", "luxon": "3.0.1", "morgan": "1.10.0", + "swagger-ui-express": "^4.5.0", "webdav": "4.10.0", "winston": "3.8.1" }, diff --git a/nextcloud_backup/backend/pnpm-lock.yaml b/nextcloud_backup/backend/pnpm-lock.yaml index 5d8acaa..519bc79 100644 --- a/nextcloud_backup/backend/pnpm-lock.yaml +++ b/nextcloud_backup/backend/pnpm-lock.yaml @@ -30,6 +30,7 @@ specifiers: luxon: 3.0.1 morgan: 1.10.0 nodemon: ^2.0.7 + swagger-ui-express: ^4.5.0 ts-node: ^10.9.1 typescript: ^4.8.3 webdav: 4.10.0 @@ -52,6 +53,7 @@ dependencies: jquery: 3.6.0 luxon: 3.0.1 morgan: 1.10.0 + swagger-ui-express: 4.5.0_express@4.18.1 webdav: 4.10.0_debug@4.3.4 winston: 3.8.1 @@ -2222,6 +2224,20 @@ packages: engines: {node: '>= 0.4'} dev: true + /swagger-ui-dist/4.14.0: + resolution: {integrity: sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw==} + dev: false + + /swagger-ui-express/4.5.0_express@4.18.1: + resolution: {integrity: sha512-DHk3zFvsxrkcnurGvQlAcLuTDacAVN1JHKDgcba/gr2NFRE4HGwP1YeHIXMiGznkWR4AeS7X5vEblNn4QljuNA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0' + dependencies: + express: 4.18.1 + swagger-ui-dist: 4.14.0 + dev: false + /table/6.8.0: resolution: {integrity: sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==} engines: {node: '>=10.0.0'} diff --git a/nextcloud_backup/backend/src/app.ts b/nextcloud_backup/backend/src/app.ts index 5a17b30..bde3457 100644 --- a/nextcloud_backup/backend/src/app.ts +++ b/nextcloud_backup/backend/src/app.ts @@ -5,10 +5,9 @@ import cookieParser from "cookie-parser"; import fs from "fs" import newlog from "./config/winston.js" import * as statusTools from "./tools/status.js" -import * as hassioApiTools from "./tools/hassioApiTools.js" import * as settingsTools from "./tools/settingsTools.js" import cronTools from "./tools/cronTools.js" -import webdav from "./tools/webdavTools.js"; +import webdav from "./services/webdavService.js"; import apiRouter from "./routes/api.js" import { fileURLToPath } from "url"; @@ -71,15 +70,17 @@ if (!fs.existsSync("/data")) fs.mkdirSync("/data"); statusTools.init(); newlog.info("Satus : \x1b[32mGo !\x1b[0m"); -hassioApiTools.getSnapshots().then( - () => { - newlog.info("Hassio API : \x1b[32mGo !\x1b[0m"); - }, - (err) => { - newlog.error("Hassio API : \x1b[31;1mFAIL !\x1b[0m"); - newlog.error("... " + err); - } -); + +// TODO Change this +// hassioApiTools.getSnapshots().then( +// () => { +// newlog.info("Hassio API : \x1b[32mGo !\x1b[0m"); +// }, +// (err) => { +// newlog.error("Hassio API : \x1b[31;1mFAIL !\x1b[0m"); +// newlog.error("... " + err); +// } +// ); webdav.confIsValid().then( () => { diff --git a/nextcloud_backup/backend/src/services/haOsService.ts b/nextcloud_backup/backend/src/services/haOsService.ts new file mode 100644 index 0000000..75ef2be --- /dev/null +++ b/nextcloud_backup/backend/src/services/haOsService.ts @@ -0,0 +1,503 @@ +import fs from "fs"; + +import FormData from "form-data"; +import got, { OptionsOfJSONResponseBody, Response } from "got"; +import stream from "stream"; +import { promisify } from "util"; +import logger from "../config/winston.js"; +import messageManager from "../tools/messageManager.js"; +import * as settingsTools from "../tools/settingsTools.js"; +import * as statusTools from "../tools/status.js"; +import { NewPartialBackupPayload } from "../types/services/ha_os_payload.js"; +import { + AddonModel, + BackupDetailModel, + BackupModel, + CoreInfoBody +} from "../types/services/ha_os_response.js"; +import { Status } from "../types/status.js"; + +const pipeline = promisify(stream.pipeline); + +const token = process.env.SUPERVISOR_TOKEN; + +// Default timeout to 90min +const create_snap_timeout = process.env.CREATE_BACKUP_TIMEOUT + ? parseInt(process.env.CREATE_BACKUP_TIMEOUT) + : 90 * 60 * 1000; + +function getVersion(): Promise> { + return got("http://hassio/core/info", { + headers: { authorization: `Bearer ${token}` }, + responseType: "json", + }).then( + (result) => { + return result; + }, + (error) => { + messageManager.error( + "Fail to fetch Home Assistant version", + error?.message + ); + logger.error(`Fail to fetch Home Assistant version`); + logger.error(error); + return error; + } + ); +} + +function getAddonList(): Promise> { + const option: OptionsOfJSONResponseBody = { + headers: { authorization: `Bearer ${token}` }, + responseType: "json", + }; + return got<{ addons: AddonModel[] }>("http://hassio/addons", option).then( + (result) => { + result.body.addons.sort((a, b) => { + const textA = a.name.toUpperCase(); + const textB = b.name.toUpperCase(); + return textA < textB ? -1 : textA > textB ? 1 : 0; + }); + return result; + }, + (error) => { + messageManager.error("Fail to fetch addons list", error?.message); + logger.error(`Fail to fetch addons list (${error?.message})`); + logger.error(error); + return error; + } + ); +} + +function getAddonToBackup() { + const excluded_addon = settingsTools.getSettings().exclude_addon; + return getAddonList().then((response) => { + const slugs: string[] = []; + for (const addon of response.body.addons) { + if (!excluded_addon.includes(addon.slug)) slugs.push(addon.slug); + } + logger.debug("Addon to backup:"); + logger.debug(slugs); + return slugs; + }); +} + +function getFolderList() { + return [ + { + name: "Home Assistant configuration", + slug: "homeassistant", + }, + { + name: "SSL", + slug: "ssl", + }, + { + name: "Share", + slug: "share", + }, + { + name: "Media", + slug: "media", + }, + { + name: "Local add-ons", + slug: "addons/local", + }, + ]; +} + +function getFolderToBackup() { + const excluded_folder = settingsTools.getSettings().exclude_folder; + const all_folder = getFolderList(); + const slugs = []; + for (const folder of all_folder) { + if (!excluded_folder.includes(folder.slug)) slugs.push(folder.slug); + } + logger.debug("Folders to backup:"); + logger.debug(slugs); + return slugs; +} + +function getBackups(): Promise> { + const option: OptionsOfJSONResponseBody = { + headers: { authorization: `Bearer ${token}` }, + responseType: "json", + }; + return got<{ backups: BackupModel[] }>("http://hassio/backups", option).then( + (result) => { + return result; + }, + (error) => { + messageManager.error("Fail to fetch Hassio backups", error?.message); + return error; + } + ); +} + +function downloadSnapshot(id: string): Promise { + logger.info(`Downloading snapshot ${id}...`); + if (!fs.existsSync("./temp/")) { + fs.mkdirSync("./temp/"); + } + const tmp_file = `./temp/${id}.tar`; + const stream = fs.createWriteStream(tmp_file); + const status = statusTools.getStatus(); + status.status = "download"; + status.progress = 0; + statusTools.setStatus(status); + const option = { + headers: { Authorization: `Bearer ${token}` }, + }; + + return pipeline( + got.stream + .get(`http://hassio/backups/${id}/download`, option) + .on("downloadProgress", (e) => { + const percent = Math.round(e.percent * 100) / 100; + if (status.progress !== percent) { + status.progress = percent; + statusTools.setStatus(status); + } + }), + stream + ).then( + () => { + logger.info("Download success !"); + status.progress = 1; + statusTools.setStatus(status); + logger.debug( + "Snapshot dl size : " + fs.statSync(tmp_file).size / 1024 / 1024 + ); + return tmp_file; + }, + (reason) => { + fs.unlinkSync(tmp_file); + messageManager.error( + "Fail to download Home Assistant backup", + reason.message + ); + return reason; + } + ); +} + +function delSnap(id: string): Promise> { + const option = { + headers: { authorization: `Bearer ${token}` }, + }; + return got.delete(`http://hassio/backups/${id}`, option).then( + (result) => { + return result; + }, + (reason) => { + messageManager.error( + "Fail to delete Homme assistant backup detail.", + reason.message + ); + logger.error("Fail to retrive Homme assistant backup detail."); + logger.error(reason); + return reason; + } + ); +} + +function checkSnap(id: string): Promise> { + const option: OptionsOfJSONResponseBody = { + headers: { authorization: `Bearer ${token}` }, + responseType: "json", + }; + return got( + `http://hassio/backups/${id}/info`, + option + ).then( + (result) => { + logger.info("Backup found !"); + logger.debug(`Backup size: ${result.body.size}`); + return result; + }, + (reason) => { + messageManager.error( + "Fail to retrive Homme assistant backup detail.", + reason.message + ); + logger.error("Fail to retrive Homme assistant backup detail"); + logger.error(reason); + return reason; + } + ); +} + +function createNewBackup( + name: string, + addonSlugs: string[], + folders: string[] +): Promise> { + const status = statusTools.getStatus(); + status.status = "creating"; + status.progress = -1; + statusTools.setStatus(status); + logger.info("Creating new snapshot..."); + const body: NewPartialBackupPayload = { + name: name, + addons: addonSlugs, + folders: folders, + }; + + const password_protected = settingsTools.getSettings().password_protected; + logger.debug(`Is password protected ? ${password_protected}`); + if (password_protected === "true") { + body.password = settingsTools.getSettings().password_protect_value; + } + const option: OptionsOfJSONResponseBody = { + headers: { authorization: `Bearer ${token}` }, + responseType: "json", + timeout: { + response: create_snap_timeout, + }, + json: body, + }; + return got + .post<{ slug: string }>(`http://hassio/backups/new/partial`, option) + .then( + (result) => { + logger.info(`Snapshot created with id ${result.body.slug}`); + return result; + }, + (reason) => { + messageManager.error("Fail to create new backup.", reason.message); + logger.error("Fail to create new backup"); + logger.error(reason); + return reason; + } + ); +} + +function clean(backups: BackupModel[]) { + const promises = []; + let limit = settingsTools.getSettings().auto_clean_local_keep; + if (limit == null) { + limit = 5; + } + if (backups.length < limit) { + return; + } + backups.sort((a, b) => { + return a.date < b.date ? 1 : -1; + }); + const toDel = backups.slice(limit); + for (const i of toDel) { + promises.push(delSnap(i.slug)); + } + logger.info("Local clean done."); + return Promise.allSettled(promises).then((values) => { + let errors = false; + for (const val of values) { + if (val.status == "rejected") { + messageManager.error("Fail to delete backup", val.reason); + logger.error("Fail to delete backup"); + logger.error(val.reason); + errors = true; + } + } + if (errors) { + messageManager.error("Fail to clean backups in Home Assistant"); + logger.error("Fail to clean backups in Home Assistant"); + return Promise.reject(); + } + }); +} + +function uploadSnapshot(path: string) { + return new Promise((resolve, reject) => { + const status = statusTools.getStatus(); + status.progress = 0; + statusTools.setStatus(status); + logger.info("Uploading backup..."); + const stream = fs.createReadStream(path); + const form = new FormData(); + form.append("file", stream); + + const options = { + body: form, + headers: { authorization: `Bearer ${token}` }, + }; + got.stream + .post(`http://hassio/backups/new/upload`, options) + .on("uploadProgress", (e) => { + const percent = e.percent; + if (status.progress !== percent) { + status.progress = percent; + statusTools.setStatus(status); + } + if (percent >= 1) { + logger.info("Upload done..."); + } + }) + .on("response", (res) => { + if (res.statusCode !== 200) { + logger.error(status.message); + fs.unlinkSync(path); + reject(status.message); + } else { + logger.info(`...Upload finish ! (status: ${res.statusCode})`); + fs.unlinkSync(path); + resolve(res); + } + }) + .on("error", (err) => { + fs.unlinkSync(path); + messageManager.error( + "Fail to upload backup to Home Assistant", + err?.message + ); + logger.error("Fail to upload backup to Home Assistant"); + logger.error(err); + reject(err); + }); + }); +} + +function stopAddons(addonSlugs: string[]) { + logger.info("Stopping addons..."); + const promises = []; + const option: OptionsOfJSONResponseBody = { + headers: { authorization: `Bearer ${token}` }, + responseType: "json", + }; + for (const addon of addonSlugs) { + if (addon !== "") { + logger.debug(`... Stopping addon ${addon}`); + promises.push(got.post(`http://hassio/addons/${addon}/stop`, option)); + } + } + return Promise.allSettled(promises).then((values) => { + let errors = false; + for (const val of values) { + if (val.status == "rejected") { + messageManager.error("Fail to stop addon", val.reason); + logger.error("Fail to stop addon"); + logger.error(val.reason); + errors = true; + } + } + if (errors) { + messageManager.error("Fail to stop addon"); + logger.error("Fail to stop addon"); + return Promise.reject(); + } + }); +} + +function startAddons(addonSlugs: string[]) { + logger.info("Starting addons..."); + const status = statusTools.getStatus(); + status.status = "starting"; + status.progress = -1; + statusTools.setStatus(status); + const promises = []; + const option: OptionsOfJSONResponseBody = { + headers: { authorization: `Bearer ${token}` }, + responseType: "json", + }; + for (const addon of addonSlugs) { + if (addon !== "") { + logger.debug(`... Starting addon ${addon}`); + promises.push(got.post(`http://hassio/addons/${addon}/start`, option)); + } + } + return Promise.allSettled(promises).then((values) => { + let errors = false; + for (const val of values) { + if (val.status == "rejected") { + messageManager.error("Fail to start addon", val.reason); + logger.error("Fail to start addon"); + logger.error(val.reason); + errors = true; + } + } + if (errors) { + messageManager.error("Fail to start addon"); + logger.error("Fail to start addon"); + return Promise.reject(); + } + }); +} + +function publish_state(state: Status) { + // let data_error_sensor = { + // state: state.status == "error" ? "on" : "off", + // attributes: { + // friendly_name: "Nexcloud Backup Error", + // device_class: "problem", + // error_code: state.error_code, + // message: state.message, + // icon: state.status == "error" ? "mdi:cloud-alert" : "mdi:cloud-check" + // }, + // } + // let option = { + // headers: { "authorization": `Bearer ${token}` }, + // responseType: "json", + // json: data_error_sensor + // }; + // got.post(`http://hassio/core/api/states/binary_sensor.nextcloud_backup_error`, option) + // .then((result) => { + // logger.debug('Home assistant sensor updated (error status)'); + // }) + // .catch((error) => { + // logger.error(error); + // }); + // let icon = "" + // switch(state.status){ + // case "error": + // icon = "mdi:cloud-alert"; + // break; + // case "download": + // case "download-b": + // icon = "mdi:cloud-download"; + // break; + // case "upload": + // case "upload-b": + // icon = "mdi:cloud-upload"; + // break; + // case "idle": + // icon = "mdi:cloud-check"; + // break; + // default: + // icon = "mdi:cloud-sync"; + // break; + // } + // let data_state_sensor = { + // state: state.status, + // attributes: { + // friendly_name: "Nexcloud Backup Status", + // error_code: state.error_code, + // message: state.message, + // icon: icon, + // last_backup: state.last_backup == null || state.last_backup == "" ? "" : new Date(state.last_backup).toISOString(), + // next_backup: state.next_backup == null || state.next_backup == "" ? "" : new Date(state.next_backup).toISOString() + // }, + // } + // option.json = data_state_sensor + // got.post(`http://hassio/core/api/states/sensor.nextcloud_backup_status`, option) + // .then((result) => { + // logger.debug('Home assistant sensor updated (status)'); + // }) + // .catch((error) => { + // logger.error(error); + // }); +} + +export { + getVersion, + getAddonList, + getFolderList, + getBackups, + downloadSnapshot, + createNewBackup, + uploadSnapshot, + stopAddons, + startAddons, + clean, + publish_state, +}; diff --git a/nextcloud_backup/backend/src/tools/cronTools.ts b/nextcloud_backup/backend/src/tools/cronTools.ts index 45956fd..c14e331 100644 --- a/nextcloud_backup/backend/src/tools/cronTools.ts +++ b/nextcloud_backup/backend/src/tools/cronTools.ts @@ -3,7 +3,7 @@ import * as settingsTools from "./settingsTools.js"; import * as hassioApiTools from "./hassioApiTools.js"; import * as statusTools from "./status.js"; import * as pathTools from "./pathTools.js"; -import webdav from "./webdavTools.js"; +import webdav from "../services/webdavService.js"; import logger from "../config/winston.js"; diff --git a/nextcloud_backup/backend/src/tools/hassioApiTools.ts b/nextcloud_backup/backend/src/tools/hassioApiTools.ts deleted file mode 100644 index 6f3d666..0000000 --- a/nextcloud_backup/backend/src/tools/hassioApiTools.ts +++ /dev/null @@ -1,545 +0,0 @@ -import fs from "fs"; - -import stream from "stream"; -import { promisify } from "util"; -import got, { OptionsOfJSONResponseBody } from "got"; -import FormData from "form-data"; -import * as statusTools from "./status.js"; -import * as settingsTools from "./settingsTools.js"; - -import logger from "../config/winston.js"; -import { Status } from "../types/status.js"; -import { AddonModel, BackupDetailModel, BackupModel, CoreInfoBody } from "../types/services/ha_os_response.js"; - -const pipeline = promisify(stream.pipeline); - -const token = process.env.SUPERVISOR_TOKEN; - -// Default timeout to 90min -const create_snap_timeout = process.env.CREATE_BACKUP_TIMEOUT - ? parseInt(process.env.CREATE_BACKUP_TIMEOUT) - : 90 * 60 * 1000; - -function getVersion() { - return new Promise((resolve, reject) => { - const status = statusTools.getStatus(); - got("http://hassio/core/info", { - headers: { authorization: `Bearer ${token}` }, - responseType: "json", - }) - .then((result) => { - if (status.error_code == 1) { - status.status = "idle"; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - } - const version = result.body.version; - resolve(version); - }) - .catch((error) => { - statusTools.setError(`Fail to fetch HA Version (${error.message})`, 1); - reject(`Fail to fetch HA Version (${error.message})`); - }); - }); -} - -function getAddonList() { - return new Promise((resolve, reject) => { - const status = statusTools.getStatus(); - const option: OptionsOfJSONResponseBody = { - headers: { authorization: `Bearer ${token}` }, - responseType: "json", - }; - - got<{ addons: AddonModel[] }>("http://hassio/addons", option) - .then((result) => { - if (status.error_code === 1) { - status.status = "idle"; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - } - const addons = result.body.addons; - addons.sort((a, b) => { - const textA = a.name.toUpperCase(); - const textB = b.name.toUpperCase(); - return textA < textB ? -1 : textA > textB ? 1 : 0; - }); - resolve(addons); - }) - .catch((error) => { - statusTools.setError(`Fail to fetch addons list (${error.message})`, 1); - reject(`Fail to fetch addons list (${error.message})`); - }); - }); -} - -function getAddonToBackup() { - return new Promise((resolve, reject) => { - const excluded_addon = settingsTools.getSettings().exclude_addon; - getAddonList() - .then((all_addon) => { - const slugs = []; - for (const addon of all_addon) { - if (!excluded_addon.includes(addon.slug)) slugs.push(addon.slug); - } - logger.debug("Addon to backup:"); - logger.debug(slugs); - resolve(slugs); - }) - .catch(() => reject()); - }); -} - -function getFolderList() { - return [ - { - name: "Home Assistant configuration", - slug: "homeassistant", - }, - { - name: "SSL", - slug: "ssl", - }, - { - name: "Share", - slug: "share", - }, - { - name: "Media", - slug: "media", - }, - { - name: "Local add-ons", - slug: "addons/local", - }, - ]; -} - -function getFolderToBackup() { - const excluded_folder = settingsTools.getSettings().exclude_folder; - const all_folder = getFolderList(); - const slugs = []; - for (const folder of all_folder) { - if (!excluded_folder.includes(folder.slug)) slugs.push(folder.slug); - } - logger.debug("Folders to backup:"); - logger.debug(slugs); - return slugs; -} - -function getSnapshots() { - return new Promise((resolve, reject) => { - const status = statusTools.getStatus(); - const option: OptionsOfJSONResponseBody = { - headers: { authorization: `Bearer ${token}` }, - responseType: "json", - }; - - got<{ backups: BackupModel[] }>("http://hassio/backups", option) - .then((result) => { - if (status.error_code === 1) { - status.status = "idle"; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - } - const snaps = result.body.backups; - resolve(snaps); - }) - .catch((error) => { - statusTools.setError( - `Fail to fetch Hassio backups (${error.message})`, - 1 - ); - reject(`Fail to fetch Hassio backups (${error.message})`); - }); - }); -} - -function downloadSnapshot(id: string) { - return new Promise((resolve, reject) => { - logger.info(`Downloading snapshot ${id}...`); - if (!fs.existsSync("./temp/")) fs.mkdirSync("./temp/"); - const tmp_file = `./temp/${id}.tar`; - const stream = fs.createWriteStream(tmp_file); - const status = statusTools.getStatus(); - checkSnap(id) - .then(() => { - status.status = "download"; - status.progress = 0; - statusTools.setStatus(status); - const option = { - headers: { Authorization: `Bearer ${token}` }, - }; - - pipeline( - got.stream - .get(`http://hassio/backups/${id}/download`, option) - .on("downloadProgress", (e) => { - const percent = Math.round(e.percent * 100) / 100; - if (status.progress !== percent) { - status.progress = percent; - statusTools.setStatus(status); - } - }), - stream - ) - .then(() => { - logger.info("Download success !"); - status.progress = 1; - statusTools.setStatus(status); - logger.debug( - "Snapshot dl size : " + fs.statSync(tmp_file).size / 1024 / 1024 - ); - resolve(undefined); - }) - .catch((error) => { - fs.unlinkSync(tmp_file); - statusTools.setError( - `Fail to download Hassio backup (${error.message})`, - 7 - ); - reject(`Fail to download Hassio backup (${error.message})`); - }); - }) - .catch(() => { - statusTools.setError("Fail to download Hassio backup. Not found ?", 7); - reject(); - }); - }); -} - -function dellSnap(id: string) { - return new Promise((resolve, reject) => { - checkSnap(id) - .then(() => { - const option = { - headers: { authorization: `Bearer ${token}` }, - }; - - got - .delete(`http://hassio/backups/${id}`, option) - .then(() => resolve(undefined)) - .catch((e) => { - logger.error(e); - reject(); - }); - }) - .catch(() => { - reject(); - }); - }); -} - -function checkSnap(id: string) { - return new Promise((resolve, reject) => { - const option: OptionsOfJSONResponseBody = { - headers: { authorization: `Bearer ${token}` }, - responseType: "json", - }; - - got(`http://hassio/backups/${id}/info`, option) - .then((result) => { - logger.debug(`Snapshot size: ${result.body.size}`); - resolve(undefined); - }) - .catch(() => reject()); - }); -} - -function createNewBackup(name: string) { - return new Promise((resolve, reject) => { - const status = statusTools.getStatus(); - status.status = "creating"; - status.progress = -1; - statusTools.setStatus(status); - logger.info("Creating new snapshot..."); - getAddonToBackup() - .then((addons) => { - const folders = getFolderToBackup(); - - const body: NewPartialBackupPayload = { - name: name, - addons: addons, - folders: folders, - } - - const password_protected = settingsTools.getSettings().password_protected; - logger.debug(`Is password protected ? ${password_protected}`); - if (password_protected === "true") { - body.password = - settingsTools.getSettings().password_protect_value; - } - - const option: OptionsOfJSONResponseBody = { - headers: { authorization: `Bearer ${token}` }, - responseType: "json", - timeout: { - response: create_snap_timeout, - }, - json: body - }; - - got - .post<{ slug: string }>(`http://hassio/backups/new/partial`, option) - .then((result) => { - logger.info(`Snapshot created with id ${result.body.slug}`); - resolve(result.body.slug); - }) - .catch((error) => { - statusTools.setError( - `Can't create new snapshot (${error.message})`, - 5 - ); - reject(`Can't create new snapshot (${error.message})`); - }); - }) - .catch(reject); - }); -} - -function clean() { - let limit = settingsTools.getSettings().auto_clean_local_keep; - if (limit == null) limit = 5; - return new Promise((resolve, reject) => { - getSnapshots() - .then(async (snaps) => { - if (snaps.length < limit) { - resolve(undefined); - return; - } - snaps.sort((a, b) => { - return a.date < b.date ? 1 : -1; - }); - const toDel = snaps.slice(limit); - for (const i of toDel) { - await dellSnap(i.slug); - } - logger.info("Local clean done."); - resolve(undefined); - }) - .catch((e) => { - statusTools.setError(`Fail to clean backups (${e}) !`, 6); - reject(`Fail to clean backups (${e}) !`); - }); - }); -} - -function uploadSnapshot(path: string) { - return new Promise((resolve, reject) => { - const status = statusTools.getStatus(); - status.status = "upload-b"; - status.progress = 0; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - logger.info("Uploading backup..."); - const stream = fs.createReadStream(path); - const form = new FormData(); - form.append("file", stream); - - const options = { - body: form, - headers: { authorization: `Bearer ${token}` }, - }; - - got.stream - .post(`http://hassio/backups/new/upload`, options) - .on("uploadProgress", (e) => { - const percent = e.percent; - if (status.progress !== percent) { - status.progress = percent; - statusTools.setStatus(status); - } - if (percent >= 1) { - logger.info("Upload done..."); - } - }) - .on("response", (res) => { - if (res.statusCode !== 200) { - status.status = "error"; - status.error_code = 4; - status.message = `Fail to upload backup to home assistant (Status code: ${res.statusCode})!`; - statusTools.setStatus(status); - logger.error(status.message); - fs.unlinkSync(path); - reject(status.message); - } else { - logger.info(`...Upload finish ! (status: ${res.statusCode})`); - status.status = "idle"; - status.progress = -1; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - fs.unlinkSync(path); - resolve(undefined); - } - }) - .on("error", (err) => { - fs.unlinkSync(path); - statusTools.setError( - `Fail to upload backup to home assistant (${err}) !`, - 4 - ); - reject(`Fail to upload backup to home assistant (${err}) !`); - }); - }); -} - -function stopAddons() { - return new Promise((resolve, reject) => { - logger.info("Stopping addons..."); - const status = statusTools.getStatus(); - status.status = "stopping"; - status.progress = -1; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - const promises = []; - const option: OptionsOfJSONResponseBody = { - headers: { authorization: `Bearer ${token}` }, - responseType: "json", - }; - const addons_slug = settingsTools.getSettings().auto_stop_addon; - for (const addon of addons_slug) { - if (addon !== "") { - logger.debug(`... Stopping addon ${addon}`); - promises.push(got.post(`http://hassio/addons/${addon}/stop`, option)); - } - } - Promise.allSettled(promises).then((values) => { - let error = null; - for (const val of values) if (val.status === "rejected") error = val.reason; - - if (error) { - statusTools.setError(`Fail to stop addons(${error}) !`, 8); - logger.error(status.message); - reject(status.message); - } else { - logger.info("... Ok"); - resolve(undefined); - } - }); - }); -} - -function startAddons() { - return new Promise((resolve, reject) => { - logger.info("Starting addons..."); - const status = statusTools.getStatus(); - status.status = "starting"; - status.progress = -1; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - const promises = []; - const option: OptionsOfJSONResponseBody = { - headers: { authorization: `Bearer ${token}` }, - responseType: "json", - }; - const addons_slug = settingsTools.getSettings().auto_stop_addon; - for (const addon of addons_slug) { - if (addon !== "") { - logger.debug(`... Starting addon ${addon}`); - promises.push(got.post(`http://hassio/addons/${addon}/start`, option)); - } - } - Promise.allSettled(promises).then((values) => { - let error = null; - for (const val of values) if (val.status === "rejected") error = val.reason; - - if (error) { - statusTools.setError(`Fail to start addons (${error}) !`, 9); - reject(status.message); - } else { - logger.info("... Ok"); - status.status = "idle"; - status.progress = -1; - status.message = undefined; - status.error_code = undefined; - statusTools.setStatus(status); - resolve(undefined); - } - }); - }); -} - -function publish_state(state: Status) { - // let data_error_sensor = { - // state: state.status == "error" ? "on" : "off", - // attributes: { - // friendly_name: "Nexcloud Backup Error", - // device_class: "problem", - // error_code: state.error_code, - // message: state.message, - // icon: state.status == "error" ? "mdi:cloud-alert" : "mdi:cloud-check" - // }, - // } - // let option = { - // headers: { "authorization": `Bearer ${token}` }, - // responseType: "json", - // json: data_error_sensor - // }; - // got.post(`http://hassio/core/api/states/binary_sensor.nextcloud_backup_error`, option) - // .then((result) => { - // logger.debug('Home assistant sensor updated (error status)'); - // }) - // .catch((error) => { - // logger.error(error); - // }); - // let icon = "" - // switch(state.status){ - // case "error": - // icon = "mdi:cloud-alert"; - // break; - // case "download": - // case "download-b": - // icon = "mdi:cloud-download"; - // break; - // case "upload": - // case "upload-b": - // icon = "mdi:cloud-upload"; - // break; - // case "idle": - // icon = "mdi:cloud-check"; - // break; - // default: - // icon = "mdi:cloud-sync"; - // break; - // } - // let data_state_sensor = { - // state: state.status, - // attributes: { - // friendly_name: "Nexcloud Backup Status", - // error_code: state.error_code, - // message: state.message, - // icon: icon, - // last_backup: state.last_backup == null || state.last_backup == "" ? "" : new Date(state.last_backup).toISOString(), - // next_backup: state.next_backup == null || state.next_backup == "" ? "" : new Date(state.next_backup).toISOString() - // }, - // } - // option.json = data_state_sensor - // got.post(`http://hassio/core/api/states/sensor.nextcloud_backup_status`, option) - // .then((result) => { - // logger.debug('Home assistant sensor updated (status)'); - // }) - // .catch((error) => { - // logger.error(error); - // }); -} - -export { - getVersion, - getAddonList, - getFolderList, - getSnapshots, - downloadSnapshot, - createNewBackup, - uploadSnapshot, - stopAddons, - startAddons, - clean, - publish_state, -}; diff --git a/nextcloud_backup/backend/src/tools/messageManager.ts b/nextcloud_backup/backend/src/tools/messageManager.ts new file mode 100644 index 0000000..0c1791d --- /dev/null +++ b/nextcloud_backup/backend/src/tools/messageManager.ts @@ -0,0 +1,40 @@ +import { DateTime } from "luxon"; +import { Message, MessageType } from "../types/message.js"; + +const maxMessageLength = 255; + +class MessageManager { + private messages: Message[] = []; + + public addMessage(type: MessageType, message: string, detail?: string) { + this.messages.push({ + message: message, + type: type, + time: DateTime.now(), + detail: detail + }); + if (this.messages.length > maxMessageLength) { + this.messages.shift(); + } + } + + public error(message: string, detail?: string) { + this.addMessage(MessageType.ERROR, message, detail); + } + + public warn(message: string, detail?: string) { + this.addMessage(MessageType.WARN, message, detail); + } + + public info(message: string, detail?: string) { + this.addMessage(MessageType.INFO, message, detail); + } + + public success(message: string, detail?: string) { + this.addMessage(MessageType.SUCCESS, message, detail); + } +} + + +const messageManager = new MessageManager(); +export default messageManager; diff --git a/nextcloud_backup/backend/src/types/message.ts b/nextcloud_backup/backend/src/types/message.ts new file mode 100644 index 0000000..81a7ea4 --- /dev/null +++ b/nextcloud_backup/backend/src/types/message.ts @@ -0,0 +1,16 @@ +import { DateTime } from "luxon"; + +export enum MessageType { + ERROR, + WARN, + INFO, + SUCCESS +} + + +export interface Message { + time: DateTime; + type: MessageType; + message: string; + detail?: string; +} \ No newline at end of file diff --git a/nextcloud_backup/backend/src/types/services/ha_os_payload.ts b/nextcloud_backup/backend/src/types/services/ha_os_payload.ts index 621866a..72b3dfa 100644 --- a/nextcloud_backup/backend/src/types/services/ha_os_payload.ts +++ b/nextcloud_backup/backend/src/types/services/ha_os_payload.ts @@ -1,4 +1,4 @@ -interface NewPartialBackupPayload { +export interface NewPartialBackupPayload { name?: string; password?: string; homeassistant?: boolean;