This commit is contained in:
SebClem 2024-07-11 15:47:27 +02:00
parent 45e869bbae
commit bdb5148e0b
Signed by: sebclem
GPG Key ID: 5A4308F6A359EA50
19 changed files with 522 additions and 603 deletions

View File

@ -1,6 +0,0 @@
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
};

View File

@ -0,0 +1,20 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
}
}
},
{
ignores: ["dist/", "eslint.config.js"]
}
);

View File

@ -7,12 +7,11 @@
"build:watch": "tsc -w", "build:watch": "tsc -w",
"build": "tsc --build --verbose", "build": "tsc --build --verbose",
"dev": "pnpm build && concurrently -n tsc,node \"pnpm build:watch\" \"pnpm serve:watch\"", "dev": "pnpm build && concurrently -n tsc,node \"pnpm build:watch\" \"pnpm serve:watch\"",
"lint": "tsc --noEmit && eslint \"**/*.{js,ts}\" --quiet --fix", "lint": "tsc --noEmit && eslint --quiet --fix",
"serve:watch": "nodemon dist/server.js", "serve:watch": "nodemon dist/server.js",
"serve": "node dist/server.js" "serve": "node dist/server.js"
}, },
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.1",
"app-root-path": "3.1.0", "app-root-path": "3.1.0",
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
@ -35,23 +34,24 @@
}, },
"packageManager": "pnpm@8.15.3", "packageManager": "pnpm@8.15.3",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.6.0",
"@tsconfig/recommended": "^1.0.3", "@tsconfig/recommended": "^1.0.3",
"@types/cookie-parser": "^1.4.6", "@types/cookie-parser": "^1.4.6",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/cron": "^2.4.0",
"@types/errorhandler": "^1.5.3", "@types/errorhandler": "^1.5.3",
"@types/eslint__js": "^8.42.3",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/figlet": "^1.5.8", "@types/figlet": "^1.5.8",
"@types/http-errors": "^2.0.4", "@types/http-errors": "^2.0.4",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@typescript-eslint/parser": "^7.0.1",
"concurrently": "8.2.2", "concurrently": "8.2.2",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"eslint": "8.56.0", "eslint": "^9.6.0",
"nodemon": "^3.0.3", "nodemon": "^3.0.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"typescript-eslint": "8.0.0-alpha.41"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,26 @@
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import express, { type NextFunction, type Request, type Response } from "express"; import cors from "cors";
import errorHandler from "errorhandler";
import express from "express";
import createError from "http-errors"; import createError from "http-errors";
import morgan from "morgan"; import morgan from "morgan";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import logger from "./config/winston.js"; import logger from "./config/winston.js";
import apiV2Router from "./routes/apiV2.js"; import apiV2Router from "./routes/apiV2.js";
import cors from "cors"
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const app = express(); const app = express();
app.use(cors({ app.use(
origin: true cors({
})) origin: true,
})
);
app.set("port", process.env.PORT || 3000); app.set("port", process.env.PORT || 3000);
// app.use(
// logger("dev", {
// skip: function (req, res) {
// return (res.statusCode = 304);
// },
// })
// );
app.use( app.use(
morgan("dev", { stream: { write: (message) => logger.debug(message) } }) morgan("dev", { stream: { write: (message) => logger.debug(message) } })
); );
@ -46,15 +41,9 @@ app.use((req, res, next) => {
}); });
// error handler // error handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => { if (process.env.NODE_ENV === "development") {
// set locals, only providing error in development // only use in development
res.locals.message = err.message; app.use(errorHandler());
res.locals.error = req.app.get("env") === "development" ? err : {}; }
// render the error page
res.status(err.status || 500);
res.render("error");
});
export default app; export default app;

View File

@ -1,10 +1,12 @@
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
import logger from "./config/winston.js"; import logger from "./config/winston.js";
import * as homeAssistantService from "./services/homeAssistantService.js"; import * as homeAssistantService from "./services/homeAssistantService.js";
import * as settingsTools from "./tools/settingsTools.js";
import * as statusTools from "./tools/status.js"; import * as statusTools from "./tools/status.js";
import kleur from "kleur"; import kleur from "kleur";
import { checkWebdavLogin, createBackupFolder } from "./services/webdavService.js"; import {
checkWebdavLogin,
createBackupFolder,
} from "./services/webdavService.js";
import { import {
getWebdavConfig, getWebdavConfig,
validateWebdavConfig, validateWebdavConfig,
@ -59,16 +61,15 @@ function postInit() {
} }
); );
}, },
(reason) => { (reason: Error) => {
logger.error("Webdav config: " + kleur.red().bold("FAIL !")); logger.error("Webdav config: " + kleur.red().bold("FAIL !"));
logger.error(reason); logger.error(reason);
messageManager.error("Invalid webdav config", reason.message); messageManager.error("Invalid webdav config", reason.message);
} }
); );
settingsTools.check(settingsTools.getSettings(), true); // settingsTools.check(settingsTools.getSettings(), true);
// cronTools.init(); // cronTools.init();
} }
export default postInit; export default postInit;

View File

@ -10,7 +10,7 @@ actionRouter.post("/backup", (req, res) => {
.then(() => { .then(() => {
logger.info("All good !"); logger.info("All good !");
}) })
.catch((reason) => { .catch(() => {
logger.error("Something wrong !"); logger.error("Something wrong !");
}); });
res.statusCode = 200; res.statusCode = 200;

View File

@ -4,45 +4,56 @@ import {
saveBackupConfig, saveBackupConfig,
validateBackupConfig, validateBackupConfig,
} from "../services/backupConfigService.js"; } from "../services/backupConfigService.js";
import { getWebdavConfig, saveWebdavConfig, validateWebdavConfig } from "../services/webdavConfigService.js"; import {
getWebdavConfig,
saveWebdavConfig,
validateWebdavConfig,
} from "../services/webdavConfigService.js";
import { checkWebdavLogin } from "../services/webdavService.js"; import { checkWebdavLogin } from "../services/webdavService.js";
import type { BackupConfig } from "../types/services/backupConfig.js";
import { ValidationError } from "joi";
import type { WebdavConfig } from "../types/services/webdavConfig.js";
const configRouter = express.Router(); const configRouter = express.Router();
configRouter.get("/backup", (req, res, next) => { configRouter.get("/backup", (req, res) => {
res.json(getBackupConfig()); res.json(getBackupConfig());
}); });
configRouter.put("/backup", (req, res, next) => { configRouter.put("/backup", (req, res) => {
validateBackupConfig(req.body) validateBackupConfig(req.body as BackupConfig)
.then(() => { .then(() => {
saveBackupConfig(req.body); saveBackupConfig(req.body as BackupConfig);
res.status(204); res.status(204);
res.send(); res.send();
}) })
.catch((error) => { .catch((error: ValidationError) => {
res.status(400); res.status(400);
res.json(error.details); res.json(error.details);
}); });
}); });
configRouter.get("/webdav", (req, res, next) => { configRouter.get("/webdav", (req, res) => {
res.json(getWebdavConfig()); res.json(getWebdavConfig());
}); });
configRouter.put("/webdav", (req, res, next) => { configRouter.put("/webdav", (req, res) => {
validateWebdavConfig(req.body) validateWebdavConfig(req.body as WebdavConfig)
.then(() => { .then(() => {
return checkWebdavLogin(req.body, true) return checkWebdavLogin(req.body as WebdavConfig, true);
}) })
.then(() => { .then(() => {
saveWebdavConfig(req.body); saveWebdavConfig(req.body as WebdavConfig);
res.status(204); res.status(204);
res.send(); res.send();
}) })
.catch((error) => { .catch((error) => {
res.status(400); res.status(400);
res.json(error.details ? error.details : error); if (error instanceof ValidationError) {
res.json(error.details);
} else {
res.json(error);
}
}); });
}); });

View File

@ -3,11 +3,15 @@ import * as haOsService from "../services/homeAssistantService.js";
const homeAssistantRouter = express.Router(); const homeAssistantRouter = express.Router();
homeAssistantRouter.get("/backups/", (req, res, next) => { homeAssistantRouter.get("/backups/", (req, res) => {
haOsService haOsService
.getBackups() .getBackups()
.then((value) => { .then((value) => {
res.json(value.body.data.backups.sort((a, b)=> Date.parse(b.date) - Date.parse(a.date))); res.json(
value.body.data.backups.sort(
(a, b) => Date.parse(b.date) - Date.parse(a.date)
)
);
}) })
.catch((reason) => { .catch((reason) => {
res.status(500); res.status(500);
@ -15,7 +19,7 @@ homeAssistantRouter.get("/backups/", (req, res, next) => {
}); });
}); });
homeAssistantRouter.get("/backup/:slug", (req, res, next) => { homeAssistantRouter.get("/backup/:slug", (req, res) => {
haOsService haOsService
.getBackupInfo(req.params.slug) .getBackupInfo(req.params.slug)
.then((value) => { .then((value) => {
@ -27,7 +31,7 @@ homeAssistantRouter.get("/backup/:slug", (req, res, next) => {
}); });
}); });
homeAssistantRouter.get("/addons", (req, res, next) => { homeAssistantRouter.get("/addons", (req, res) => {
haOsService haOsService
.getAddonList() .getAddonList()
.then((value) => { .then((value) => {
@ -39,7 +43,7 @@ homeAssistantRouter.get("/addons", (req, res, next) => {
}); });
}); });
homeAssistantRouter.get("/folders", (req, res, next) => { homeAssistantRouter.get("/folders", (req, res) => {
res.json(haOsService.getFolderList()); res.json(haOsService.getFolderList());
}); });

View File

@ -12,7 +12,7 @@ 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) => {
const config = getWebdavConfig(); const config = getWebdavConfig();
const backupConf = getBackupConfig(); const backupConf = getBackupConfig();
validateWebdavConfig(config) validateWebdavConfig(config)
@ -20,8 +20,11 @@ webdavRouter.get("/backup/auto", (req, res, next) => {
return webdavService.checkWebdavLogin(config); return webdavService.checkWebdavLogin(config);
}) })
.then(async () => { .then(async () => {
const value = await webdavService const value = await webdavService.getBackups(
.getBackups(pathTools.auto, config, backupConf.nameTemplate); pathTools.auto,
config,
backupConf.nameTemplate
);
res.json(value); res.json(value);
}) })
.catch((reason) => { .catch((reason) => {
@ -30,7 +33,7 @@ webdavRouter.get("/backup/auto", (req, res, next) => {
}); });
}); });
webdavRouter.get("/backup/manual", (req, res, next) => { webdavRouter.get("/backup/manual", (req, res) => {
const config = getWebdavConfig(); const config = getWebdavConfig();
const backupConf = getBackupConfig(); const backupConf = getBackupConfig();
validateWebdavConfig(config) validateWebdavConfig(config)
@ -38,8 +41,11 @@ webdavRouter.get("/backup/manual", (req, res, next) => {
return webdavService.checkWebdavLogin(config); return webdavService.checkWebdavLogin(config);
}) })
.then(async () => { .then(async () => {
const value = await webdavService const value = await webdavService.getBackups(
.getBackups(pathTools.manual, config, backupConf.nameTemplate); pathTools.manual,
config,
backupConf.nameTemplate
);
res.json(value); res.json(value);
}) })
.catch((reason) => { .catch((reason) => {
@ -48,28 +54,30 @@ webdavRouter.get("/backup/manual", (req, res, next) => {
}); });
}); });
webdavRouter.delete("/", (req, res, next) => { webdavRouter.delete("/", (req, res) => {
const body: WebdavDelete = req.body; const body = req.body as WebdavDelete;
const validator = Joi.object(WebdavDeleteValidation); const validator = Joi.object(WebdavDeleteValidation);
const config = getWebdavConfig(); const config = getWebdavConfig();
validateWebdavConfig(config).then(() => { validateWebdavConfig(config)
validator .then(() => {
.validateAsync(body) return validator.validateAsync(body);
.then(() => { })
return webdavService.checkWebdavLogin(config); .then(() => {
}) return webdavService.checkWebdavLogin(config);
.then(() => { })
webdavService.deleteBackup(body.path, config) .then(() => {
.then(()=>{ webdavService
res.status(201).send(); .deleteBackup(body.path, config)
}).catch((reason)=>{ .then(() => {
res.status(500).json(reason); res.status(201).send();
}); })
}) .catch((reason) => {
.catch((reason) => { res.status(500).json(reason);
res.status(400).json(reason); });
}); })
}); .catch((reason) => {
res.status(400).json(reason);
});
}); });
export default webdavRouter; export default webdavRouter;

View File

@ -4,27 +4,29 @@ import app from "./app.js";
import logger from "./config/winston.js"; import logger from "./config/winston.js";
import postInit from "./postInit.js"; import postInit from "./postInit.js";
import figlet from "figlet"; import figlet from "figlet";
import kleur from 'kleur'; import kleur from "kleur";
/** /**
* Error Handler. Provides full stack * Error Handler. Provides full stack
*/ */
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
app.use(errorHandler()); app.use(errorHandler());
} }
/** /**
* Start Express server. * Start Express server.
*/ */
const server = app.listen(app.get("port"), () => { const server = app.listen(app.get("port"), () => {
console.log(kleur.yellow().bold(figlet.textSync("NC Backup"))) console.log(kleur.yellow().bold(figlet.textSync("NC Backup")));
logger.info( logger.info(
`App is running at ` + kleur.green().bold(`http://localhost:${app.get("port")}`) + " in " + kleur.green().bold(app.get("env")) + " mode" `App is running at ` +
); kleur.green().bold(`http://localhost:${app.get("port")}`) +
logger.info(kleur.red().bold("Press CTRL-C to stop")); " in " +
postInit(); kleur.green().bold(app.get("env") as string) +
" mode"
);
logger.info(kleur.red().bold("Press CTRL-C to stop"));
postInit();
}); });
export default server; export default server;

View File

@ -29,7 +29,9 @@ export function getBackupConfig(): BackupConfig {
saveBackupConfig(defaultConfig); saveBackupConfig(defaultConfig);
return defaultConfig; return defaultConfig;
} else { } else {
return JSON.parse(fs.readFileSync(backupConfigPath).toString()); return JSON.parse(
fs.readFileSync(backupConfigPath).toString()
) as BackupConfig;
} }
} }

View File

@ -5,27 +5,26 @@ import got, {
RequestError, RequestError,
type OptionsOfJSONResponseBody, type OptionsOfJSONResponseBody,
type PlainResponse, type PlainResponse,
type Progress,
type Response, type Response,
} from "got"; } from "got";
import { DateTime } from "luxon";
import stream from "stream"; import stream from "stream";
import { promisify } from "util"; import { promisify } from "util";
import logger from "../config/winston.js"; import logger from "../config/winston.js";
import messageManager from "../tools/messageManager.js"; import messageManager from "../tools/messageManager.js";
import * as settingsTools from "../tools/settingsTools.js";
import * as statusTools from "../tools/status.js"; import * as statusTools from "../tools/status.js";
import { BackupType } from "../types/services/backupConfig.js";
import type { NewBackupPayload } from "../types/services/ha_os_payload.js"; import type { NewBackupPayload } from "../types/services/ha_os_payload.js";
import type { import type {
AddonData, AddonData,
AddonModel,
BackupData, BackupData,
BackupDetailModel, BackupDetailModel,
BackupModel, BackupModel,
CoreInfoBody, CoreInfoBody,
SupervisorResponse, SupervisorResponse,
} from "../types/services/ha_os_response.js"; } from "../types/services/ha_os_response.js";
import { States, type Status } from "../types/status.js"; import { States } from "../types/status.js";
import { DateTime } from "luxon";
import { BackupType } from "../types/services/backupConfig.js";
const pipeline = promisify(stream.pipeline); const pipeline = promisify(stream.pipeline);
@ -44,7 +43,7 @@ function getVersion(): Promise<Response<SupervisorResponse<CoreInfoBody>>> {
(result) => { (result) => {
return result; return result;
}, },
(error) => { (error: Error) => {
messageManager.error( messageManager.error(
"Fail to fetch Home Assistant version", "Fail to fetch Home Assistant version",
error?.message error?.message
@ -73,7 +72,7 @@ function getAddonList(): Promise<Response<SupervisorResponse<AddonData>>> {
}); });
return result; return result;
}, },
(error) => { (error: Error) => {
messageManager.error("Fail to fetch addons list", error?.message); messageManager.error("Fail to fetch addons list", error?.message);
logger.error(`Fail to fetch addons list (${error?.message})`); logger.error(`Fail to fetch addons list (${error?.message})`);
logger.error(error); logger.error(error);
@ -98,7 +97,7 @@ function getBackups(): Promise<Response<SupervisorResponse<BackupData>>> {
statusTools.setStatus(status); statusTools.setStatus(status);
return result; return result;
}, },
(error) => { (error: Error) => {
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.hass.ok = false; status.hass.ok = false;
status.hass.last_check = DateTime.now(); status.hass.last_check = DateTime.now();
@ -127,7 +126,7 @@ function downloadSnapshot(id: string): Promise<string> {
return pipeline( return pipeline(
got.stream got.stream
.get(`http://hassio/backups/${id}/download`, option) .get(`http://hassio/backups/${id}/download`, option)
.on("downloadProgress", (e) => { .on("downloadProgress", (e: Progress) => {
const percent = Math.round(e.percent * 100) / 100; const percent = Math.round(e.percent * 100) / 100;
if (status.progress !== percent) { if (status.progress !== percent) {
status.progress = percent; status.progress = percent;
@ -147,7 +146,7 @@ function downloadSnapshot(id: string): Promise<string> {
); );
return tmp_file; return tmp_file;
}, },
(reason) => { (reason: Error) => {
fs.unlinkSync(tmp_file); fs.unlinkSync(tmp_file);
messageManager.error( messageManager.error(
"Fail to download Home Assistant backup", "Fail to download Home Assistant backup",
@ -170,7 +169,7 @@ function delSnap(id: string) {
(result) => { (result) => {
return result; return result;
}, },
(reason) => { (reason: RequestError) => {
messageManager.error( messageManager.error(
"Fail to delete Homme assistant backup detail.", "Fail to delete Homme assistant backup detail.",
reason.message reason.message
@ -196,7 +195,7 @@ function getBackupInfo(id: string) {
logger.debug(`Backup size: ${result.body.data.size}`); logger.debug(`Backup size: ${result.body.data.size}`);
return result; return result;
}, },
(reason) => { (reason: RequestError) => {
messageManager.error( messageManager.error(
"Fail to retrive Homme assistant backup detail.", "Fail to retrive Homme assistant backup detail.",
reason.message reason.message
@ -250,7 +249,7 @@ function createNewBackup(
statusTools.setStatus(status); statusTools.setStatus(status);
return result; return result;
}, },
(reason) => { (reason: RequestError) => {
messageManager.error("Fail to create new backup.", reason.message); messageManager.error("Fail to create new backup.", reason.message);
logger.error("Fail to create new backup"); logger.error("Fail to create new backup");
logger.error(reason); logger.error(reason);
@ -263,19 +262,15 @@ function createNewBackup(
); );
} }
function clean(backups: BackupModel[]) { function clean(backups: BackupModel[], numberToKeep: number) {
const promises = []; const promises = [];
let limit = settingsTools.getSettings().auto_clean_local_keep; if (backups.length < numberToKeep) {
if (limit == null) {
limit = 5;
}
if (backups.length < limit) {
return; return;
} }
backups.sort((a, b) => { backups.sort((a, b) => {
return Date.parse(b.date) - Date.parse(a.date); return Date.parse(b.date) - Date.parse(a.date);
}); });
const toDel = backups.slice(limit); const toDel = backups.slice(numberToKeep);
for (const i of toDel) { for (const i of toDel) {
promises.push(delSnap(i.slug)); promises.push(delSnap(i.slug));
} }
@ -284,6 +279,7 @@ function clean(backups: BackupModel[]) {
let errors = false; let errors = false;
for (const val of values) { for (const val of values) {
if (val.status == "rejected") { if (val.status == "rejected") {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
messageManager.error("Fail to delete backup", val.reason); messageManager.error("Fail to delete backup", val.reason);
logger.error("Fail to delete backup"); logger.error("Fail to delete backup");
logger.error(val.reason); logger.error(val.reason);
@ -293,7 +289,7 @@ function clean(backups: BackupModel[]) {
if (errors) { if (errors) {
messageManager.error("Fail to clean backups in Home Assistant"); messageManager.error("Fail to clean backups in Home Assistant");
logger.error("Fail to clean backups in Home Assistant"); logger.error("Fail to clean backups in Home Assistant");
return Promise.reject(); return Promise.reject(new Error());
} }
}); });
} }
@ -315,7 +311,7 @@ function uploadSnapshot(path: string) {
}; };
got.stream got.stream
.post(`http://hassio/backups/new/upload`, options) .post(`http://hassio/backups/new/upload`, options)
.on("uploadProgress", (e) => { .on("uploadProgress", (e: Progress) => {
const percent = e.percent; const percent = e.percent;
if (status.progress !== percent) { if (status.progress !== percent) {
status.progress = percent; status.progress = percent;
@ -329,13 +325,13 @@ function uploadSnapshot(path: string) {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
messageManager.error( messageManager.error(
"Fail to upload backup to Home Assistant", "Fail to upload backup to Home Assistant",
`Code: ${res.statusCode} Body: ${res.body}` `Code: ${res.statusCode} Body: ${res.body as string}`
); );
logger.error("Fail to upload backup to Home Assistant"); logger.error("Fail to upload backup to Home Assistant");
logger.error(`Code: ${res.statusCode}`); logger.error(`Code: ${res.statusCode}`);
logger.error(`Body: ${res.body}`); logger.error(`Body: ${res.body as string}`);
fs.unlinkSync(path); fs.unlinkSync(path);
reject(res.statusCode); reject(new Error(res.statusCode.toString()));
} else { } else {
logger.info(`...Upload finish ! (status: ${res.statusCode})`); logger.info(`...Upload finish ! (status: ${res.statusCode})`);
const status = statusTools.getStatus(); const status = statusTools.getStatus();
@ -388,17 +384,17 @@ function stopAddons(addonSlugs: string[]) {
statusTools.setStatus(status); statusTools.setStatus(status);
for (const val of values) { for (const val of values) {
if (val.status == "rejected") { if (val.status == "rejected") {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
messageManager.error("Fail to stop addon", val.reason); messageManager.error("Fail to stop addon", val.reason);
logger.error("Fail to stop addon"); logger.error("Fail to stop addon");
logger.error(val.reason); logger.error(val.reason);
logger.error;
errors = true; errors = true;
} }
} }
if (errors) { if (errors) {
messageManager.error("Fail to stop addon"); messageManager.error("Fail to stop addon");
logger.error("Fail to stop addon"); logger.error("Fail to stop addon");
return Promise.reject(); return Promise.reject(new Error());
} }
}); });
} }
@ -428,6 +424,7 @@ function startAddons(addonSlugs: string[]) {
let errors = false; let errors = false;
for (const val of values) { for (const val of values) {
if (val.status == "rejected") { if (val.status == "rejected") {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
messageManager.error("Fail to start addon", val.reason); messageManager.error("Fail to start addon", val.reason);
logger.error("Fail to start addon"); logger.error("Fail to start addon");
logger.error(val.reason); logger.error(val.reason);
@ -437,7 +434,7 @@ function startAddons(addonSlugs: string[]) {
if (errors) { if (errors) {
messageManager.error("Fail to start addon"); messageManager.error("Fail to start addon");
logger.error("Fail to start addon"); logger.error("Fail to start addon");
return Promise.reject(); return Promise.reject(new Error());
} }
}); });
} }
@ -467,7 +464,7 @@ export function getFolderList() {
]; ];
} }
function publish_state(state: Status) { function publish_state() {
// let data_error_sensor = { // let data_error_sensor = {
// state: state.status == "error" ? "on" : "off", // state: state.status == "error" ? "on" : "off",
// attributes: { // attributes: {
@ -532,15 +529,15 @@ function publish_state(state: Status) {
} }
export { export {
getVersion,
getAddonList,
getBackups,
downloadSnapshot,
createNewBackup,
uploadSnapshot,
stopAddons,
startAddons,
clean, clean,
publish_state, createNewBackup,
downloadSnapshot,
getAddonList,
getBackupInfo, getBackupInfo,
getBackups,
getVersion,
publish_state,
startAddons,
stopAddons,
uploadSnapshot,
}; };

View File

@ -1,15 +1,15 @@
import { unlinkSync } from "fs";
import { DateTime } from "luxon";
import logger from "../config/winston.js";
import messageManager from "../tools/messageManager.js";
import * as statusTools from "../tools/status.js";
import { BackupType } from "../types/services/backupConfig.js";
import type { AddonModel } from "../types/services/ha_os_response.js"; import type { AddonModel } from "../types/services/ha_os_response.js";
import { WorkflowType } from "../types/services/orchecstrator.js"; import { WorkflowType } from "../types/services/orchecstrator.js";
import * as backupConfigService from "./backupConfigService.js"; import * as backupConfigService from "./backupConfigService.js";
import * as homeAssistantService from "./homeAssistantService.js"; import * as homeAssistantService from "./homeAssistantService.js";
import { getBackupFolder, getWebdavConfig } from "./webdavConfigService.js"; import { getBackupFolder, getWebdavConfig } from "./webdavConfigService.js";
import * as webDavService from "./webdavService.js"; import * as webDavService from "./webdavService.js";
import * as statusTools from "../tools/status.js";
import { stat, unlinkSync } from "fs";
import logger from "../config/winston.js";
import { BackupType } from "../types/services/backupConfig.js";
import { DateTime } from "luxon";
import messageManager from "../tools/messageManager.js";
export function doBackupWorkflow(type: WorkflowType) { export function doBackupWorkflow(type: WorkflowType) {
let name = ""; let name = "";
@ -38,7 +38,7 @@ export function doBackupWorkflow(type: WorkflowType) {
.then(() => { .then(() => {
return homeAssistantService.stopAddons(addonsToStartStop); return homeAssistantService.stopAddons(addonsToStartStop);
}) })
.then((response) => { .then(() => {
if (backupConfig.backupType == BackupType.FULL) { if (backupConfig.backupType == BackupType.FULL) {
return homeAssistantService.createNewBackup( return homeAssistantService.createNewBackup(
name, name,
@ -66,21 +66,23 @@ export function doBackupWorkflow(type: WorkflowType) {
} }
}) })
.then((response) => { .then((response) => {
response.body.data.slug;
return homeAssistantService.downloadSnapshot(response.body.data.slug); return homeAssistantService.downloadSnapshot(response.body.data.slug);
}) })
.then((tmpFile) => { .then((tmpFile) => {
tmpBackupFile = tmpFile; tmpBackupFile = tmpFile;
return webDavService.chunkedUpload( if (webdavConfig.chunckedUpload) {
tmpFile, return webDavService.chunkedUpload(
getBackupFolder(type, webdavConfig) + name, tmpFile,
webdavConfig getBackupFolder(type, webdavConfig) + name,
); webdavConfig
// return webDavService.webdavUploadFile( );
// tmpFile, } else {
// getBackupFolder(type, webdavConfig) + name, return webDavService.webdavUploadFile(
// webdavConfig tmpFile,
// ); getBackupFolder(type, webdavConfig) + name,
webdavConfig
);
}
}) })
.then(() => { .then(() => {
logger.info("Backup workflow finished successfully !"); logger.info("Backup workflow finished successfully !");
@ -96,7 +98,7 @@ export function doBackupWorkflow(type: WorkflowType) {
if (tmpBackupFile != "") { if (tmpBackupFile != "") {
unlinkSync(tmpBackupFile); unlinkSync(tmpBackupFile);
} }
return Promise.reject(); return Promise.reject(new Error());
}); });
} }

View File

@ -1,16 +1,14 @@
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 * as pathTools from "../tools/pathTools.js";
import { default_root } from "../tools/pathTools.js"; import { default_root } from "../tools/pathTools.js";
import { WorkflowType } from "../types/services/orchecstrator.js";
import { import {
type WebdavConfig, type WebdavConfig,
WebdavEndpointType, WebdavEndpointType,
} from "../types/services/webdavConfig.js"; } from "../types/services/webdavConfig.js";
import WebdavConfigValidation from "../types/services/webdavConfigValidation.js"; import WebdavConfigValidation from "../types/services/webdavConfigValidation.js";
import { BackupType } from "../types/services/backupConfig.js";
import * as pathTools from "../tools/pathTools.js";
import { WorkflowType } from "../types/services/orchecstrator.js";
import e from "express";
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";
@ -34,7 +32,9 @@ export function getWebdavConfig(): WebdavConfig {
saveWebdavConfig(defaultConfig); saveWebdavConfig(defaultConfig);
return defaultConfig; return defaultConfig;
} else { } else {
return JSON.parse(fs.readFileSync(webdavConfigPath).toString()); return JSON.parse(
fs.readFileSync(webdavConfigPath).toString()
) as WebdavConfig;
} }
} }

View File

@ -1,3 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { XMLParser } from "fast-xml-parser"; import { XMLParser } from "fast-xml-parser";
import fs from "fs"; import fs from "fs";
import got, { import got, {
@ -54,7 +58,10 @@ export function checkWebdavLogin(
}, },
(reason) => { (reason) => {
if (!silent) { if (!silent) {
messageManager.error("Fail to connect to Webdav", reason?.message); messageManager.error(
"Fail to connect to Webdav",
(reason as Error).message
);
} }
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.webdav = { status.webdav = {
@ -65,7 +72,7 @@ export function checkWebdavLogin(
statusTools.setStatus(status); statusTools.setStatus(status);
logger.error(`Fail to connect to Webdav`); logger.error(`Fail to connect to Webdav`);
logger.error(reason); logger.error(reason);
return Promise.reject(reason); return Promise.reject(reason as Error);
} }
); );
} }
@ -90,7 +97,7 @@ export async function createBackupFolder(conf: WebdavConfig) {
status.webdav.folder_created = false; status.webdav.folder_created = false;
status.webdav.last_check = DateTime.now(); status.webdav.last_check = DateTime.now();
statusTools.setStatus(status); statusTools.setStatus(status);
return Promise.reject(error); return Promise.reject(error as Error);
} }
} }
} }
@ -110,7 +117,7 @@ export async function createBackupFolder(conf: WebdavConfig) {
status.webdav.folder_created = false; status.webdav.folder_created = false;
status.webdav.last_check = DateTime.now(); status.webdav.last_check = DateTime.now();
statusTools.setStatus(status); statusTools.setStatus(status);
return Promise.reject(error); return Promise.reject(error as Error);
} }
} }
} }
@ -139,7 +146,7 @@ export function getBackups(
) { ) {
const status = statusTools.getStatus(); const status = statusTools.getStatus();
if (!status.webdav.logged_in && !status.webdav.folder_created) { if (!status.webdav.logged_in && !status.webdav.folder_created) {
return Promise.reject("Not logged in"); return Promise.reject(new Error("Not logged in"));
} }
const endpoint = getEndpoint(config); const endpoint = getEndpoint(config);
return got(config.url + endpoint + config.backupDir + folder, { return got(config.url + endpoint + config.backupDir + folder, {
@ -165,7 +172,7 @@ export function getBackups(
); );
logger.error(`Fail to retrive webdav backups in ${folder} folder`); logger.error(`Fail to retrive webdav backups in ${folder} folder`);
logger.error(reason); logger.error(reason);
return Promise.reject(reason); return Promise.reject(reason as Error);
} }
); );
} }
@ -214,11 +221,11 @@ export function deleteBackup(path: string, config: WebdavConfig) {
(reason) => { (reason) => {
messageManager.error( messageManager.error(
"Fail to delete backup in webdav", "Fail to delete backup in webdav",
reason?.message (reason as Error)?.message
); );
logger.error(`Fail to delete backup in Cloud`); logger.error(`Fail to delete backup in Cloud`);
logger.error(reason); logger.error(reason);
return Promise.reject(reason); return Promise.reject(reason as Error);
} }
); );
} }
@ -302,13 +309,13 @@ export function webdavUploadFile(
if (res.statusCode != 201 && res.statusCode != 204) { if (res.statusCode != 201 && res.statusCode != 204) {
messageManager.error( messageManager.error(
"Fail to upload file to Cloud.", "Fail to upload file to Cloud.",
`Code: ${res.statusCode} Body: ${res.body}` `Code: ${res.statusCode} Body: ${res.body as string}`
); );
logger.error(`Fail to upload file to Cloud`); logger.error(`Fail to upload file to Cloud`);
logger.error(`Code: ${res.statusCode}`); logger.error(`Code: ${res.statusCode}`);
logger.error(`Body: ${res.body}`); logger.error(`Body: ${res.body as string}`);
fs.unlinkSync(localPath); fs.unlinkSync(localPath);
reject(res); reject(new Error(res.statusCode.toString()));
} else { } else {
logger.info(`...Upload finish ! (status: ${res.statusCode})`); logger.info(`...Upload finish ! (status: ${res.statusCode})`);
fs.unlinkSync(localPath); fs.unlinkSync(localPath);
@ -340,6 +347,10 @@ export async function chunkedUpload(
const chunkEndpoint = getChunkEndpoint(config); const chunkEndpoint = getChunkEndpoint(config);
const chunkedUrl = config.url + chunkEndpoint + uuid; const chunkedUrl = config.url + chunkEndpoint + uuid;
const finalDestination = config.url + getEndpoint(config) + webdavPath; const finalDestination = config.url + getEndpoint(config) + webdavPath;
const status = statusTools.getStatus();
status.status = States.BKUP_UPLOAD_CLOUD;
status.progress = 0;
statusTools.setStatus(status);
try { try {
await initChunkedUpload(chunkedUrl, finalDestination, config); await initChunkedUpload(chunkedUrl, finalDestination, config);
} catch (err) { } catch (err) {
@ -366,7 +377,7 @@ export async function chunkedUpload(
let start = 0; let start = 0;
let end = fileSize > CHUNK_SIZE ? CHUNK_SIZE : fileSize; let end = fileSize > CHUNK_SIZE ? CHUNK_SIZE : fileSize;
let current_size = end; let current_size = end;
let uploadedBytes = 0; // const uploadedBytes = 0;
let i = 0; let i = 0;
while (start < fileSize) { while (start < fileSize) {
@ -396,12 +407,12 @@ export async function chunkedUpload(
messageManager.error( messageManager.error(
"Fail to upload file to Cloud.", "Fail to upload file to Cloud.",
`Code: ${(error as PlainResponse).statusCode} Body: ${ `Code: ${(error as PlainResponse).statusCode} Body: ${
(error as PlainResponse).body (error as PlainResponse).body as string
}` }`
); );
logger.error(`Fail to upload file to Cloud`); logger.error(`Fail to upload file to Cloud`);
logger.error(`Code: ${(error as PlainResponse).statusCode}`); logger.error(`Code: ${(error as PlainResponse).statusCode}`);
logger.error(`Body: ${(error as PlainResponse).body}`); logger.error(`Body: ${(error as PlainResponse).body as string}`);
} }
throw error; throw error;
} }
@ -462,7 +473,7 @@ export function uploadChunk(
resolve(res); resolve(res);
} else { } else {
logger.error(`Fail to upload chunk: ${res.statusCode}`); logger.error(`Fail to upload chunk: ${res.statusCode}`);
reject(res); reject(new Error(res.statusCode.toString()));
} }
}) })
.on("error", (err) => { .on("error", (err) => {

View File

@ -1,213 +0,0 @@
import { CronJob } from "cron";
import fs from "fs";
import { DateTime } from "luxon";
import logger from "../config/winston.js";
import type { Settings } from "../types/settings.js";
const settingsPath = "/data/backup_conf.json";
function check_cron(conf: Settings) {
if (conf.cron_base != null) {
if (
conf.cron_base == "1" ||
conf.cron_base == "2" ||
conf.cron_base == "3"
) {
if (conf.cron_hour != null && conf.cron_hour.match(/\d\d:\d\d/)) {
if (conf.cron_base === "1") return true;
} else return false;
}
if (conf.cron_base === "2") {
return (
conf.cron_weekday != null &&
conf.cron_weekday >= 0 &&
conf.cron_weekday <= 6
);
}
if (conf.cron_base === "3") {
return (
conf.cron_month_day != null &&
conf.cron_month_day >= 1 &&
conf.cron_month_day <= 28
);
}
if (conf.cron_base === "4") {
if (conf.cron_custom != null) {
try {
// TODO Need to be destroy
new CronJob(conf.cron_custom, () => {
//Do nothing
});
return true;
} catch (e) {
return false;
}
} else return false;
}
if (conf.cron_base === "0") return true;
} else return false;
return false;
}
function check(conf: Settings, fallback = false) {
let needSave = false;
if (!check_cron(conf)) {
if (fallback) {
logger.warn("Bad value for cron settings, fallback to default ");
conf.cron_base = "0";
conf.cron_hour = "00:00";
conf.cron_weekday = 0;
conf.cron_month_day = 1;
conf.cron_custom = "5 4 * * *";
} else {
logger.error("Bad value for cron settings");
return [false, "Bad cron settings"];
}
}
if (!conf.name_template) {
if (fallback) {
logger.warn("Bad value for 'name_template', fallback to default ");
conf.name_template = "{type}-{ha_version}-{date}_{hour}";
} else {
logger.error("Bad value for 'name_template'");
return [false, "Bad value for 'name_template'"];
}
}
if (
conf.auto_clean_local_keep == undefined ||
!/^\d+$/.test(conf.auto_clean_local_keep)
) {
if (fallback) {
logger.warn("Bad value for 'auto_clean_local_keep', fallback to 5 ");
conf.auto_clean_local_keep = "5";
} else {
logger.error("Bad value for 'auto_clean_local_keep'");
return [false, "Bad value for 'auto_clean_local_keep'"];
}
}
if (
conf.auto_clean_backup_keep == undefined ||
!/^\d+$/.test(conf.auto_clean_backup_keep)
) {
if (fallback) {
logger.warn("Bad value for 'auto_clean_backup_keep', fallback to 5 ");
conf.auto_clean_backup_keep = "5";
} else {
logger.error("Bad value for 'auto_clean_backup_keep'");
return [false, "Bad value for 'auto_clean_backup_keep'"];
}
}
if (conf.auto_clean_local == undefined) {
if (fallback) {
logger.warn("Bad value for 'auto_clean_local', fallback to false ");
conf.auto_clean_local = "false";
} else {
logger.error("Bad value for 'auto_clean_local'");
return [false, "Bad value for 'auto_clean_local'"];
}
}
if (conf.auto_clean_backup == undefined) {
if (fallback) {
logger.warn("Bad value for 'auto_clean_backup', fallback to false ");
conf.auto_clean_backup = "false";
} else {
logger.error("Bad value for 'auto_clean_backup'");
return [false, "Bad value for 'auto_clean_backup'"];
}
}
if (conf.exclude_addon == undefined) {
if (fallback) {
logger.warn("Bad value for 'exclude_addon', fallback to [] ");
conf.exclude_addon = [];
} else {
logger.error("Bad value for 'exclude_addon'");
return [false, "Bad value for 'exclude_addon'"];
}
}
if (conf.exclude_folder == undefined) {
if (fallback) {
logger.warn("Bad value for 'exclude_folder', fallback to [] ");
conf.exclude_folder = [];
} else {
logger.error("Bad value for 'exclude_folder'");
return [false, "Bad value for 'exclude_folder'"];
}
}
if (conf.auto_stop_addon == undefined) {
if (fallback) {
logger.warn("Bad value for 'auto_stop_addon', fallback to [] ");
conf.auto_stop_addon = [];
} else {
logger.error("Bad value for 'auto_stop_addon'");
return [false, "Bad value for 'auto_stop_addon'"];
}
}
if (!Array.isArray(conf.exclude_folder)) {
logger.debug("exclude_folder is not array (Empty value), reset...");
conf.exclude_folder = [];
needSave = true;
}
if (!Array.isArray(conf.exclude_addon)) {
logger.debug("exclude_addon is not array (Empty value), reset...");
conf.exclude_addon = [];
needSave = true;
}
if (conf.password_protected == undefined) {
if (fallback) {
logger.warn("Bad value for 'password_protected', fallback to false ");
conf.password_protected = "false";
} else {
logger.error("Bad value for 'password_protect_value'");
return [false, "Bad value for 'password_protect_value'"];
}
}
if (conf.password_protect_value == null) {
if (fallback) {
logger.warn("Bad value for 'password_protect_value', fallback to '' ");
conf.password_protect_value = "";
} else {
logger.error("Bad value for 'password_protect_value'");
return [false, "Bad value for 'password_protect_value'"];
}
}
if (fallback || needSave) {
setSettings(conf);
}
return [true, null];
}
function getFormatedName(is_manual: boolean, ha_version: string) {
const setting = getSettings();
let template = setting.name_template;
template = template.replace("{type_low}", is_manual ? "manual" : "auto");
template = template.replace("{type}", is_manual ? "Manual" : "Auto");
template = template.replace("{ha_version}", ha_version);
const now = DateTime.now().setLocale("en");
template = template.replace("{hour_12}", now.toFormat("hhmma"));
template = template.replace("{hour}", now.toFormat("HHmm"));
template = template.replace("{date}", now.toFormat("yyyy-MM-dd"));
return template;
}
function getSettings() {
if (!fs.existsSync(settingsPath)) {
return {};
} else {
return JSON.parse(fs.readFileSync(settingsPath).toString());
}
}
function setSettings(settings: Settings) {
fs.writeFileSync(settingsPath, JSON.stringify(settings));
}
export { getSettings, setSettings, check, check_cron, getFormatedName };

View File

@ -1,4 +1,3 @@
import { publish_state } from "../services/homeAssistantService.js";
import { States, type Status } from "../types/status.js"; import { States, type Status } from "../types/status.js";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
@ -33,6 +32,6 @@ export function setStatus(new_state: Status) {
const old_state_str = JSON.stringify(status); const old_state_str = JSON.stringify(status);
if (old_state_str !== JSON.stringify(new_state)) { if (old_state_str !== JSON.stringify(new_state)) {
status = new_state; status = new_state;
publish_state(status); // publish_state(status);
} }
} }

View File

@ -1,7 +1,6 @@
import Joi from "joi"; import Joi from "joi";
import { WebdavEndpointType } from "./webdavConfig.js"; import { WebdavEndpointType } from "./webdavConfig.js";
const WebdavConfigValidation = { const WebdavConfigValidation = {
url: Joi.string().not().empty().uri().required().label("Url"), url: Joi.string().not().empty().uri().required().label("Url"),
username: Joi.string().not().empty().label("Username"), username: Joi.string().not().empty().label("Username"),
@ -10,18 +9,22 @@ const WebdavConfigValidation = {
allowSelfSignedCerts: Joi.boolean().label("Allow self signed certificate"), allowSelfSignedCerts: Joi.boolean().label("Allow self signed certificate"),
chunckedUpload: Joi.boolean().required().label("Chuncked upload"), chunckedUpload: Joi.boolean().required().label("Chuncked upload"),
webdavEndpoint: Joi.object({ webdavEndpoint: Joi.object({
type: Joi.string().valid(WebdavEndpointType.CUSTOM, WebdavEndpointType.NEXTCLOUD).required(), type: Joi.string()
.valid(WebdavEndpointType.CUSTOM, WebdavEndpointType.NEXTCLOUD)
.required(),
customEndpoint: Joi.alternatives().conditional("type", { customEndpoint: Joi.alternatives().conditional("type", {
is: WebdavEndpointType.CUSTOM, is: WebdavEndpointType.CUSTOM,
then: Joi.string().not().empty().required, then: Joi.string().not().empty().required(),
otherwise: Joi.disallow() otherwise: Joi.disallow(),
}), }),
customChunkEndpoint: Joi.alternatives().conditional("type", { customChunkEndpoint: Joi.alternatives().conditional("type", {
is: WebdavEndpointType.CUSTOM, is: WebdavEndpointType.CUSTOM,
then: Joi.string().not().empty().required, then: Joi.string().not().empty().required(),
otherwise: Joi.disallow() otherwise: Joi.disallow(),
}) }),
}).required().label("Webdav endpoint"), })
} .required()
.label("Webdav endpoint"),
};
export default WebdavConfigValidation; export default WebdavConfigValidation;