🔨 Move backend to typescript + cleanup

This commit is contained in:
SebClem 2022-09-21 17:24:02 +02:00
parent ded4bc7315
commit f91189999f
Signed by: sebclem
GPG Key ID: 5A4308F6A359EA50
49 changed files with 4392 additions and 15139 deletions

View File

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

1
nextcloud_backup/backend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/**

View File

@ -0,0 +1,55 @@
{
"name": "nexcloud-backup",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p .",
"debug": "pnpm run build && pnpm run watch-debug",
"lint": "tsc --noEmit && eslint \"**/*.{js,ts}\" --quiet --fix",
"serve-debug": "nodemon --inspect dist/server.js",
"serve": "node dist/server.js",
"watch-debug": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"pnpm run watch-ts\" \"pnpm run serve-debug\"",
"watch-node": "nodemon dist/server.js",
"watch-ts": "tsc -w",
"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",
"got": "12.3.0",
"http-errors": "2.0.0",
"jquery": "3.6.0",
"luxon": "3.0.1",
"morgan": "1.10.0",
"webdav": "4.10.0",
"winston": "3.8.1"
},
"packageManager": "pnpm@7.12.1",
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.0",
"@types/errorhandler": "^1.5.0",
"@types/express": "^4.17.14",
"@types/http-errors": "^1.8.2",
"@types/luxon": "^3.0.1",
"@types/morgan": "^1.9.3",
"@types/node": "^18.7.18",
"@typescript-eslint/parser": "^5.38.0",
"concurrently": "6.0.2",
"eslint": "7.19.0",
"nodemon": "^2.0.7",
"ts-node": "^10.9.1",
"typescript": "^4.8.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +1,50 @@
import createError from "http-errors";
import express from "express";
import express, { NextFunction, Request, Response } from "express";
import path from "path";
import cookieParser from "cookie-parser";
import logger from "morgan";
import { fileURLToPath } from 'url';
import { dirname } from 'path';
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 webdav from "./tools/webdavTools.js"
import * as settingsTools from "./tools/settingsTools.js"
import cronTools from "./tools/cronTools.js"
import webdav from "./tools/webdavTools.js";
import indexRouter from "./routes/index.js"
import apiRouter from "./routes/api.js"
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const __dirname = path.dirname(__filename);
const app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
app.use(
logger("dev", {
skip: function (req, res) {
return (res.statusCode = 304);
},
})
);
// app.use(
// logger("dev", {
// skip: function (req, res) {
// return (res.statusCode = 304);
// },
// })
// );
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use("/", indexRouter);
app.use("/api", apiRouter);
/*
-----------------------------------------------------------
Library statics
----------------------------------------------------------
*/
// Boootstrap JS Files
app.use('/js/bootstrap.min.js', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/js/bootstrap.min.js')))
// Fontawesome Files
app.use('/css/fa-all.min.css', express.static(path.join(__dirname, '/node_modules/@fortawesome/fontawesome-free/css/all.min.css')))
app.use('/webfonts/', express.static(path.join(__dirname, '/node_modules/@fortawesome/fontawesome-free/webfonts')))
// Jquery JS Files
app.use('/js/jquery.min.js', express.static(path.join(__dirname, '/node_modules/jquery/dist/jquery.min.js')))
/*
-----------------------------------------------------------
Error handler
----------------------------------------------------------
*/
// catch 404 and forward to error handler
app.use(function (req, res, next) {
app.use((req, res, next) => {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
@ -89,7 +64,8 @@ app.use(function (err, req, res, next) {
newlog.info(`Log level: ${ process.env.LOG_LEVEL }`);
newlog.info(`Backup timeout: ${ parseInt(process.env.CREATE_BACKUP_TIMEOUT) || ( 90 * 60 * 1000 ) }`)
newlog.info(`Backup timeout: ${ (process.env.CREATE_BACKUP_TIMEOUT ? parseInt(process.env.CREATE_BACKUP_TIMEOUT) : false) || ( 90 * 60 * 1000 ) }`)
if (!fs.existsSync("/data")) fs.mkdirSync("/data");
statusTools.init();

View File

@ -0,0 +1,279 @@
import express from "express";
import * as statusTools from "../tools/status.js";
import webdav from "../tools/webdavTools.js";
import * as settingsTools from "../tools/settingsTools.js";
import * as pathTools from "../tools/pathTools.js";
import * as hassioApiTools from "../tools/hassioApiTools.js";
import { humanFileSize } from "../tools/toolbox.js";
import cronTools from "../tools/cronTools.js";
import logger from "../config/winston.js";
import { DateTime } from "luxon";
const router = express.Router();
router.get("/status", (req, res, next) => {
cronTools.updateNextDate();
const status = statusTools.getStatus();
res.json(status);
});
router.get("/formated-local-snap", function (req, res, next) {
hassioApiTools
.getSnapshots()
.then((snaps) => {
snaps.sort((a, b) => {
return a.date < b.date ? 1 : -1;
});
res.render("localSnaps", { snaps: snaps, DateTime: DateTime });
})
.catch((err) => {
logger.error(err);
res.status(500);
res.send("");
});
});
router.get("/formated-backup-manual", function (req, res, next) {
if (webdav.getConf() == null) {
res.send("");
return;
}
webdav
.getFolderContent(webdav.getConf()?.back_dir + pathTools.manual)
.then((contents: any) => {
contents.sort((a: any, b: any) => {
return a.date < b.date ? 1 : -1;
});
//TODO Remove this when bug is fixed, etag contain '&quot;' at start and end ?
for (const backup of contents) {
backup.etag = backup.etag.replace(/&quot;/g, "");
}
res.render("backupSnaps", {
backups: contents,
DateTime: DateTime,
humanFileSize: humanFileSize,
});
})
.catch((err) => {
res.status(500);
res.send(err);
});
});
router.get("/formated-backup-auto", function (req, res, next) {
if (webdav.getConf() == null) {
res.send("");
return;
}
const url = webdav.getConf()?.back_dir + pathTools.auto;
webdav
.getFolderContent(url)
.then((contents: any) => {
contents.sort((a: any, b: any) => {
return a.date < b.date ? 1 : -1;
});
//TODO Remove this when bug is fixed, etag contain '&quot;' at start and end ?
for (const backup of contents) {
backup.etag = backup.etag.replace(/&quot;/g, "");
}
res.render("backupSnaps", {
backups: contents,
DateTime: DateTime,
humanFileSize: humanFileSize,
});
})
.catch((err) => {
res.status(500);
res.send(err);
});
});
router.post("/nextcloud-settings", function (req, res, next) {
const settings = req.body;
if (
settings.ssl != null &&
settings.host != null &&
settings.host !== "" &&
settings.username != null &&
settings.password != null
) {
webdav.setConf(settings);
webdav
.confIsValid()
.then(() => {
res.status(201);
res.send();
})
.catch((err) => {
res.status(406);
res.json({ message: err });
});
} else {
res.status(400);
res.send();
}
});
router.get("/nextcloud-settings", function (req, res, next) {
const conf = webdav.getConf();
if (conf == null) {
res.status(404);
res.send();
} else {
res.json(conf);
}
});
router.post("/manual-backup", function (req, res, next) {
const id = req.query.id;
const name = req.query.name;
const status = statusTools.getStatus();
if (
status.status == "creating" ||
status.status == "upload" ||
status.status == "download"
) {
res.status(503);
res.send();
return;
}
hassioApiTools
.downloadSnapshot(id as string)
.then(() => {
webdav
.uploadFile(
id as string,
webdav.getConf()?.back_dir + pathTools.manual + name + ".tar"
)
.then(() => {
res.status(201);
res.send();
})
.catch(() => {
res.status(500);
res.send();
});
})
.catch(() => {
res.status(500);
res.send();
});
});
router.post("/new-backup", function (req, res, next) {
const status = statusTools.getStatus();
if (
status.status == "creating" ||
status.status == "upload" ||
status.status == "download" ||
status.status == "stopping" ||
status.status == "starting"
) {
res.status(503);
res.send();
return;
}
hassioApiTools
.stopAddons()
.then(() => {
hassioApiTools
.getVersion()
.then((version) => {
const name = settingsTools.getFormatedName(true, version);
hassioApiTools
.createNewBackup(name)
.then((id) => {
hassioApiTools
.downloadSnapshot(id)
.then(() => {
webdav
.uploadFile(
id,
webdav.getConf()?.back_dir +
pathTools.manual +
name +
".tar"
)
.then(() => {
hassioApiTools.startAddons().catch(() => {
// Skip
});
})
.catch(() => {
// Skip
});
})
.catch(() => {
// Skip
});
})
.catch(() => {
// Skip
});
})
.catch(() => {
// Skip
});
})
.catch(() => {
hassioApiTools.startAddons().catch(() => {
// Skip
});
});
res.status(201);
res.send();
});
router.get("/backup-settings", function (req, res, next) {
hassioApiTools.getAddonList().then((addonList) => {
const data = {
folder: hassioApiTools.getFolderList(),
addonList: addonList,
settings: settingsTools.getSettings()
};
res.send(data);
});
});
router.post("/backup-settings", function (req, res, next) {
const [result, message] = settingsTools.check(req.body);
if (result) {
settingsTools.setSettings(req.body);
cronTools.init();
res.send();
} else {
res.status(400);
res.send(message);
}
});
router.post("/clean-now", function (req, res, next) {
webdav
.clean()
.then(() => {
hassioApiTools.clean().catch();
})
.catch(() => {
hassioApiTools.clean().catch();
});
res.status(201);
res.send();
});
router.post("/restore", function (req, res, next) {
if (req.body["path"] != null) {
webdav.downloadFile(req.body["path"]).then((path) => {
hassioApiTools.uploadSnapshot(path).catch();
});
res.status(200);
res.send();
} else {
res.status(400);
res.send();
}
});
export default router;

View File

@ -0,0 +1,25 @@
import errorHandler from "errorhandler";
import app from "./app.js";
/**
* Error Handler. Provides full stack
*/
if (process.env.NODE_ENV === "development") {
app.use(errorHandler());
}
/**
* Start Express server.
*/
const server = app.listen(app.get("port"), () => {
console.log(
" App is running at http://localhost:%d in %s mode",
app.get("port"),
app.get("env")
);
console.log(" Press CTRL-C to stop\n");
});
export default server;

View File

@ -0,0 +1,149 @@
import { CronJob } from "cron";
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 logger from "../config/winston.js";
class CronContainer {
cronJob: CronJob | undefined;
cronClean: CronJob | undefined;
init() {
const settings = settingsTools.getSettings();
let cronStr = "";
if (!this.cronClean) {
logger.info("Starting auto clean cron...");
this.cronClean = new CronJob(
"0 1 * * *",
this._clean,
null,
false,
Intl.DateTimeFormat().resolvedOptions().timeZone
);
this.cronClean.start();
}
if (this.cronJob) {
logger.info("Stopping Cron...");
this.cronJob.stop();
this.cronJob = undefined;
}
if (!settingsTools.check_cron(settingsTools.getSettings())) {
logger.warn("No Cron settings available.");
return;
}
switch (settings.cron_base) {
case "0":
logger.warn("No Cron settings available.");
return;
case "1": {
const splited = settings.cron_hour.split(":");
cronStr = "" + splited[1] + " " + splited[0] + " * * *";
break;
}
case "2": {
const splited = settings.cron_hour.split(":");
cronStr =
"" + splited[1] + " " + splited[0] + " * * " + settings.cron_weekday;
break;
}
case "3": {
const splited = settings.cron_hour.split(":");
cronStr =
"" +
splited[1] +
" " +
splited[0] +
" " +
settings.cron_month_day +
" * *";
break;
}
case "4": {
cronStr = settings.cron_custom;
break;
}
}
logger.info("Starting Cron...");
this.cronJob = new CronJob(
cronStr,
this._createBackup,
null,
false,
Intl.DateTimeFormat().resolvedOptions().timeZone
);
this.cronJob.start();
this.updateNextDate();
}
updateNextDate() {
let date;
if (this.cronJob) {
date = this.cronJob
.nextDate()
.setLocale("en")
.toFormat("dd MMM yyyy, HH:mm");
}
const status = statusTools.getStatus();
status.next_backup = date;
statusTools.setStatus(status);
}
_createBackup() {
logger.debug("Cron triggered !");
const status = statusTools.getStatus();
if (
status.status === "creating" ||
status.status === "upload" ||
status.status === "download" ||
status.status === "stopping" ||
status.status === "starting"
)
return;
hassioApiTools
.stopAddons()
.then(() => {
hassioApiTools.getVersion().then((version) => {
const name = settingsTools.getFormatedName(false, version);
hassioApiTools.createNewBackup(name).then((id) => {
hassioApiTools.downloadSnapshot(id).then(() => {
webdav
.uploadFile(
id,
webdav.getConf()?.back_dir + pathTools.auto + name + ".tar"
)
.then(() => {
hassioApiTools.startAddons().catch(() => {
// Skip
});
});
});
});
});
})
.catch(() => {
hassioApiTools.startAddons().catch(() => {
// Skip
});
});
}
_clean() {
const autoCleanlocal = settingsTools.getSettings().auto_clean_local;
if (autoCleanlocal && autoCleanlocal == "true") {
hassioApiTools.clean().catch();
}
const autoCleanCloud = settingsTools.getSettings().auto_clean_backup;
if (autoCleanCloud && autoCleanCloud == "true") {
webdav.clean().catch();
}
}
}
const INSTANCE = new CronContainer();
export default INSTANCE;

View File

@ -0,0 +1,545 @@
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<string>((resolve, reject) => {
const status = statusTools.getStatus();
got<CoreInfoBody>("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<AddonModel[]>((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<string[]>((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<BackupModel[]>((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<BackupDetailModel>(`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<string>((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,
};

View File

@ -0,0 +1,213 @@
import { CronJob } from "cron";
import fs from "fs";
import { DateTime } from "luxon";
import logger from "../config/winston.js";
import { 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,17 +1,19 @@
import * as hassioApiTools from "./hassioApiTools.js";
import logger from "../config/winston.js"
import { type Status } from "../types/status.js";
let status = {
let status: Status = {
status: "idle",
last_backup: null,
next_backup: null,
last_backup: undefined,
next_backup: undefined,
progress: -1,
};
export function init() {
if (status.status !== "idle") {
status.status = "idle";
status.message = null;
status.message = undefined;
status.progress = -1;
}
}
@ -19,15 +21,15 @@ export function getStatus() {
return status;
}
export function setStatus(new_state) {
let old_state_str = JSON.stringify(status);
export function setStatus(new_state: Status) {
const old_state_str = JSON.stringify(status);
if(old_state_str !== JSON.stringify(new_state)){
status = new_state;
hassioApiTools.publish_state(status);
}
}
export function setError(message, error_code){
export function setError(message: string, error_code: number){
// Check if we don't have another error stored
if (status.status != "error") {
status.status = "error"

View File

@ -1,5 +1,5 @@
// Found on Stackoverflow, perfect code :D https://stackoverflow.com/a/14919494/8654475
function humanFileSize(bytes, si = false, dp = 1) {
function humanFileSize(bytes: number, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {

View File

@ -0,0 +1,480 @@
import fs from "fs";
import got from "got";
import https from "https";
import path from "path";
import stream from "stream";
import { promisify } from "util";
import { createClient, WebDAVClient } from "webdav";
import { DateTime } from "luxon";
import logger from "../config/winston.js";
import { WebdavSettings } from "../types/settings.js";
import * as hassioApiTools from "./hassioApiTools.js";
import * as pathTools from "./pathTools.js";
import * as settingsTools from "./settingsTools.js";
import * as statusTools from "./status.js";
const endpoint = "/remote.php/webdav";
const configPath = "/data/webdav_conf.json";
const pipeline = promisify(stream.pipeline);
class WebdavTools {
host: string | undefined;
client: WebDAVClient | undefined;
baseUrl: string | undefined;
username: string | undefined;
password: string | undefined;
init(
ssl: boolean,
host: string,
username: string,
password: string,
accept_selfsigned_cert: boolean
) {
return new Promise((resolve, reject) => {
this.host = host;
const status = statusTools.getStatus();
logger.info("Initializing and checking webdav client...");
this.baseUrl = (ssl ? "https" : "http") + "://" + host + endpoint;
this.username = username;
this.password = password;
const agent_option = ssl
? { rejectUnauthorized: !accept_selfsigned_cert }
: {};
try {
this.client = createClient(this.baseUrl, {
username: username,
password: password,
httpsAgent: new https.Agent(agent_option),
});
this.client
.getDirectoryContents("/")
.then(() => {
if (status.error_code === 3) {
status.status = "idle";
status.message = undefined;
status.error_code = undefined;
statusTools.setStatus(status);
}
logger.debug("Nextcloud connection: \x1b[32mSuccess !\x1b[0m");
this.initFolder().then(() => {
resolve(undefined);
});
})
.catch((error) => {
this.__cant_connect_status(error);
this.client = undefined;
reject("Can't connect to Nextcloud (" + error + ") !");
});
} catch (err) {
this.__cant_connect_status(err);
this.client = undefined;
reject("Can't connect to Nextcloud (" + err + ") !");
}
});
}
__cant_connect_status(err: any) {
statusTools.setError(`Can't connect to Nextcloud (${err})`, 3);
}
async __createRoot() {
const conf = this.getConf();
if (this.client && conf) {
const root_splited = conf.back_dir.split("/").splice(1);
let path = "/";
for (const elem of root_splited) {
if (elem !== "") {
path = path + elem + "/";
try {
await this.client.createDirectory(path);
logger.debug(`Path ${path} created.`);
} catch (error) {
if ((error as any).status === 405)
logger.debug(`Path ${path} already exist.`);
else logger.error(error);
}
}
}
}
}
initFolder() {
return new Promise((resolve, reject) => {
this.__createRoot()
.catch((err) => {
logger.error(err);
})
.then(() => {
if (!this.client) {
return;
}
this.client
.createDirectory(this.getConf()?.back_dir + pathTools.auto)
.catch(() => {
// Ignore
})
.then(() => {
if (!this.client) {
return;
}
this.client
.createDirectory(this.getConf()?.back_dir + pathTools.manual)
.catch(() => {
// Ignore
})
.then(() => {
resolve(undefined);
});
});
});
});
}
/**
* Check if theh webdav config is valid, if yes, start init of webdav client
*/
confIsValid() {
return new Promise((resolve, reject) => {
const status = statusTools.getStatus();
const conf = this.getConf();
if (conf != undefined) {
if (
conf.ssl != undefined &&
conf.host != undefined &&
conf.username != undefined &&
conf.password != undefined
) {
if (status.error_code === 2) {
status.status = "idle";
status.message = undefined;
status.error_code = undefined;
statusTools.setStatus(status);
}
// Check if self_signed option exist
if (conf.self_signed == undefined) {
conf.self_signed = false;
this.setConf(conf);
}
this.init(
conf.ssl,
conf.host,
conf.username,
conf.password,
conf.self_signed
)
.then(() => {
resolve(undefined);
})
.catch((err) => {
reject(err);
});
} else {
status.status = "error";
status.error_code = 2;
status.message = "Nextcloud config invalid !";
statusTools.setStatus(status);
logger.error(status.message);
reject("Nextcloud config invalid !");
}
if (conf.back_dir == null || conf.back_dir === "") {
logger.info("Backup dir is null, initializing it.");
conf.back_dir = pathTools.default_root;
this.setConf(conf);
} else {
if (!conf.back_dir.startsWith("/")) {
logger.warn("Backup dir not starting with '/', fixing this...");
conf.back_dir = `/${conf.back_dir}`;
this.setConf(conf);
}
if (!conf.back_dir.endsWith("/")) {
logger.warn("Backup dir not ending with '/', fixing this...");
conf.back_dir = `${conf.back_dir}/`;
this.setConf(conf);
}
}
} else {
status.status = "error";
status.error_code = 2;
status.message = "Nextcloud config not found !";
statusTools.setStatus(status);
logger.error(status.message);
reject("Nextcloud config not found !");
}
});
}
getConf(): WebdavSettings | undefined {
if (fs.existsSync(configPath)) {
return JSON.parse(fs.readFileSync(configPath).toString());
} else return undefined;
}
setConf(conf: WebdavSettings) {
fs.writeFileSync(configPath, JSON.stringify(conf));
}
uploadFile(id: string, path: string) {
return new Promise((resolve, reject) => {
if (this.client == null) {
this.confIsValid()
.then(() => {
this._startUpload(id, path)
.then(() => resolve(undefined))
.catch((err) => reject(err));
})
.catch((err) => {
reject(err);
});
} else
this._startUpload(id, path)
.then(() => resolve(undefined))
.catch((err) => reject(err));
});
}
_startUpload(id: string, path: string) {
return new Promise((resolve, reject) => {
const status = statusTools.getStatus();
status.status = "upload";
status.progress = 0;
status.message = undefined;
status.error_code = undefined;
statusTools.setStatus(status);
logger.info("Uploading snap...");
const tmpFile = `./temp/${id}.tar`;
const stats = fs.statSync(tmpFile);
const stream = fs.createReadStream(tmpFile);
const conf = this.getConf();
const options = {
body: stream,
headers: {
authorization:
"Basic " +
Buffer.from(this.username + ":" + this.password).toString("base64"),
"content-length": String(stats.size),
},
https: undefined as any | undefined,
};
if (conf?.ssl) {
options["https"] = { rejectUnauthorized: !conf.self_signed };
}
logger.debug(
`...URI: ${encodeURI(
this.baseUrl?.replace(this.host as string, "host.hiden") + path
)}`
);
if (conf?.ssl)
logger.debug(
`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`
);
got.stream
.put(encodeURI(this.baseUrl + path), 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 !== 201 && res.statusCode !== 204) {
status.status = "error";
status.error_code = 4;
status.message = `Fail to upload snapshot to nextcloud (Status code: ${res.statusCode})!`;
statusTools.setStatus(status);
logger.error(status.message);
fs.unlinkSync(tmpFile);
reject(status.message);
} else {
logger.info(`...Upload finish ! (status: ${res.statusCode})`);
status.status = "idle";
status.progress = -1;
status.message = undefined;
status.error_code = undefined;
status.last_backup = DateTime.now().toFormat("dd MMM yyyy, HH:mm");
statusTools.setStatus(status);
cleanTempFolder();
const autoCleanCloud =
settingsTools.getSettings().auto_clean_backup;
if (autoCleanCloud != null && autoCleanCloud === "true") {
this.clean().catch();
}
const autoCleanlocal = settingsTools.getSettings().auto_clean_local;
if (autoCleanlocal != null && autoCleanlocal === "true") {
hassioApiTools.clean().catch();
}
resolve(undefined);
}
})
.on("error", (err) => {
fs.unlinkSync(tmpFile);
status.status = "error";
status.error_code = 4;
status.message = `Fail to upload snapshot to nextcloud (${err}) !`;
statusTools.setStatus(status);
logger.error(status.message);
logger.error(err.stack);
reject(status.message);
});
});
}
downloadFile(path: string) {
return new Promise<string>((resolve, reject) => {
if (this.client == null) {
this.confIsValid()
.then(() => {
this._startDownload(path)
.then((path) => resolve(path))
.catch(() => reject());
})
.catch((err) => {
reject(err);
});
} else
this._startDownload(path)
.then((path) => resolve(path))
.catch(() => reject());
});
}
_startDownload(path: string) {
return new Promise<string>((resolve, reject) => {
const status = statusTools.getStatus();
status.status = "download-b";
status.progress = 0;
status.message = undefined;
status.error_code = undefined;
statusTools.setStatus(status);
logger.info("Downloading backup...");
if (!fs.existsSync("./temp/")) fs.mkdirSync("./temp/");
const tmpFile = `./temp/restore_${DateTime.now().toFormat(
"MMM-dd-yyyy_HH_mm"
)}.tar`;
const stream = fs.createWriteStream(tmpFile);
const conf = this.getConf();
const options = {
headers: {
authorization:
"Basic " +
Buffer.from(this.username + ":" + this.password).toString("base64"),
},
https: undefined as any | undefined,
};
if (conf?.ssl) {
options["https"] = { rejectUnauthorized: !conf?.self_signed };
}
logger.debug(
`...URI: ${encodeURI(
this.baseUrl?.replace(this.host as string, "host.hiden") + path
)}`
);
if (conf?.ssl)
logger.debug(
`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`
);
pipeline(
got.stream
.get(encodeURI(this.baseUrl + path), options)
.on("downloadProgress", (e) => {
const percent = Math.round(e.percent * 100) / 100;
if (status.progress !== percent) {
status.progress = percent;
statusTools.setStatus(status);
}
}),
stream
)
.then((res) => {
logger.info("Download success !");
status.progress = 1;
statusTools.setStatus(status);
logger.debug(
"Backup dl size : " + fs.statSync(tmpFile).size / 1024 / 1024
);
resolve(tmpFile);
})
.catch((err) => {
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
status.status = "error";
status.message =
"Fail to download Hassio snapshot (" + err.message + ")";
status.error_code = 7;
statusTools.setStatus(status);
logger.error(status.message);
logger.error(err.stack);
reject(err.message);
});
});
}
getFolderContent(path: string) {
return new Promise((resolve, reject) => {
if (this.client == null) {
reject();
return;
}
this.client
.getDirectoryContents(path)
.then((contents) => resolve(contents))
.catch((error) => reject(error));
});
}
clean() {
let limit = settingsTools.getSettings().auto_clean_backup_keep;
if (limit == null) limit = 5;
return new Promise((resolve, reject) => {
this.getFolderContent(this.getConf()?.back_dir + pathTools.auto)
.then(async (contents: any) => {
if (contents.length < limit) {
resolve(undefined);
return;
}
contents.sort((a: any, b: any) => {
return a.date < b.date ? 1 : -1;
});
const toDel = contents.slice(limit);
for (const i in toDel) {
await this.client?.deleteFile(toDel[i].filename);
}
logger.info("Cloud clean done.");
resolve(undefined);
})
.catch((error) => {
const status = statusTools.getStatus();
status.status = "error";
status.error_code = 6;
status.message = "Fail to clean Nexcloud (" + error + ") !";
statusTools.setStatus(status);
logger.error(status.message);
reject(status.message);
});
});
}
}
function cleanTempFolder() {
fs.readdir("./temp/", (err, files) => {
if (err) throw err;
for (const file of files) {
fs.unlink(path.join("./temp/", file), (err) => {
if (err) throw err;
});
}
});
}
const INSTANCE = new WebdavTools();
export default INSTANCE;

View File

@ -0,0 +1,8 @@
interface NewPartialBackupPayload {
name?: string;
password?: string;
homeassistant?: boolean;
addons?: string[];
folders?: string[];
compressed?: boolean;
}

View File

@ -0,0 +1,66 @@
export interface CoreInfoBody {
version: string;
version_latest: string;
update_available: boolean;
arch: string;
machine: string;
ip_address: string;
image: string;
boot: boolean;
port: number;
ssl: boolean;
watchdog: boolean;
wait_boot: number;
audio_input: string;
audio_output: string;
}
export interface AddonModel {
name: string;
slug: string;
advanced: boolean;
description: string;
repository: string;
version: string;
version_latest: string;
update_available: boolean;
installed: string;
available: boolean;
icon: boolean;
logo: boolean;
state: string;
}
export interface BackupModel {
slug: string;
date: string;
name: string;
type: string;
protected: boolean;
content: BackupContent;
compressed: boolean;
}
export interface BackupContent {
homeassistant: boolean;
addons: string[];
folders: string[];
}
export interface BackupDetailModel {
slug: string;
type: string;
name: string;
date: string;
size: string;
protected: boolean;
homeassistant: string;
addons: {
slug: string;
name: string;
version: string;
size: number;
};
repositories: string[];
folders: string[];
}

View File

@ -0,0 +1,26 @@
export interface Settings {
name_template?: string;
cron_base?: string;
cron_hour?: string;
cron_weekday?: number;
cron_month_day?: number;
cron_custom?: string;
auto_clean_local?: string;
auto_clean_local_keep?: string;
auto_clean_backup?: string;
auto_clean_backup_keep?: string;
auto_stop_addon?: string[];
password_protected?: string;
password_protect_value?: string;
exclude_addon: string[];
exclude_folder: string[];
}
export interface WebdavSettings {
ssl: boolean;
host: string;
username: string;
password: string;
back_dir: string;
self_signed: boolean;
}

View File

@ -0,0 +1,10 @@
export interface Status {
status: string;
progress: number;
last_backup?: string;
next_backup?: string;
message?: string;
error_code?: number;
}

View File

@ -0,0 +1,11 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"outDir": "./dist",
"module": "ES2022",
"moduleResolution": "NodeNext",
"target": "es6",
},
"include": ["src/**/*"]
}

View File

@ -1,15 +0,0 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es2021": true,
"node": true
},
"extends": ["eslint:recommended","prettier"],
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
"no-unused-vars": "warn"
}
};

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.2.cjs

View File

@ -1,92 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
import app from "../app.js"
import http from "http"
import _debug from 'debug';
const debug = _debug('nexcloud-backup:server');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View File

@ -1,28 +0,0 @@
{
"name": "nexcloud-backup",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
"start": "node ./bin/www.js "
},
"dependencies": {
"@fortawesome/fontawesome-free": "6.1.2",
"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",
"express": "4.18.1",
"form-data": "4.0.0",
"got": "12.3.0",
"http-errors": "2.0.0",
"jquery": "3.6.0",
"luxon": "3.0.1",
"morgan": "1.10.0",
"webdav": "4.10.0",
"winston": "3.8.1"
},
"packageManager": "yarn@3.2.2"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,15 +0,0 @@
.navbar {
background-color: #0091ea;
}
#header-box {
min-height: 160px;
}
.toast-container {
position: fixed;
z-index: 1055;
margin: 5px;
top: 58px;
right: 0;
}

View File

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 134.44119 72.098951"
enable-background="new 0 0 196.6 72"
xml:space="preserve"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="Nextcloud_Logo.svg"
width="134.44118"
height="72.098953"
inkscape:export-filename="nextcloud-logo-white-transparent.png"
inkscape:export-xdpi="300.09631"
inkscape:export-ydpi="300.09631"><metadata
id="metadata20"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs18" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview16"
showgrid="false"
inkscape:zoom="4"
inkscape:cx="75.820577"
inkscape:cy="10.858529"
inkscape:current-layer="Layer_1"
fit-margin-top="10"
fit-margin-left="10"
fit-margin-right="10"
fit-margin-bottom="10"
inkscape:window-x="1440"
inkscape:window-y="0"
inkscape:window-maximized="1"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:snap-page="true" /><path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.59273672;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 67.307795,10 C 55.445624,10 45.391448,18.041769 42.275325,28.937463 39.567089,23.158221 33.698247,19.109372 26.93836,19.109372 17.64192,19.109372 10,26.751292 10,36.047732 c 0,9.296429 7.64192,16.941836 16.93836,16.941836 6.759887,0 12.628729,-4.05132 15.336965,-9.831567 3.116123,10.896507 13.170299,18.940949 25.03247,18.940949 11.77445,0 21.77736,-7.92312 24.973175,-18.696727 2.758286,5.649027 8.55449,9.587345 15.21836,9.587345 9.29644,0 16.94184,-7.645407 16.94184,-16.941836 0,-9.29644 -7.6454,-16.93836 -16.94184,-16.93836 -6.66387,0 -12.460074,3.935846 -15.21836,9.583869 C 89.085155,17.920467 79.082245,10 67.307795,10 Z m 0,9.943213 c 8.954599,0 16.108025,7.14995 16.108025,16.104519 0,8.954569 -7.153426,16.108005 -16.108025,16.108005 -8.954549,0 -16.104498,-7.153436 -16.104498,-16.108005 0,-8.954569 7.149949,-16.104519 16.104498,-16.104519 z M 26.93836,29.052595 c 3.923195,0 6.998623,3.071921 6.998623,6.995137 0,3.923205 -3.075428,6.998623 -6.998623,6.998623 -3.923216,0 -6.995147,-3.075418 -6.995147,-6.998623 0,-3.923216 3.071931,-6.995137 6.995147,-6.995137 z m 80.56097,0 c 3.92322,0 6.99862,3.071921 6.99862,6.995137 0,3.923205 -3.07542,6.998623 -6.99862,6.998623 -3.9232,0 -6.99513,-3.075418 -6.99513,-6.998623 0,-3.923216 3.07195,-6.995137 6.99513,-6.995137 z"
id="XMLID_107_"
inkscape:connector-curvature="0" /></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,521 +0,0 @@
var last_status = "";
var last_local_snap = "";
var last_manu_back = "";
var last_auto_back = "";
const default_toast_timeout = 10000;
let loadingModal;
let nextcloud_setting_modal;
let backup_setting_modal;
document.addEventListener("DOMContentLoaded", function () {
$.ajaxSetup({ traditional: true });
updateLocalSnaps();
update_status();
loadingModal = new bootstrap.Modal(document.getElementById("loading-modal"), {
keyboard: false,
backdrop: "static",
});
nextcloud_setting_modal = new bootstrap.Modal(document.getElementById("modal-settings-nextcloud"));
backup_setting_modal = new bootstrap.Modal(document.getElementById("modal-settings-backup"));
setInterval(update_status, 500);
setInterval(updateLocalSnaps, 5000);
listeners();
});
function updateDynamicListeners() {
$(".local-snap-listener").on("click", function () {
let id = this.getAttribute("data-id");
console.log(id);
});
let manual_back_list = $(".manual-back-list");
manual_back_list.unbind();
manual_back_list.on("click", function () {
let id = this.getAttribute("data-id");
let name = this.getAttribute("data-name");
manualBackup(id, name);
});
let restore_btn = $(".restore");
restore_btn.unbind();
restore_btn.click(function () {
let to_restore = this.getAttribute("data-id");
console.log(to_restore);
restore(to_restore);
});
}
function updateLocalSnaps() {
let needUpdate = false;
$.get("./api/formated-local-snap", (data) => {
if (JSON.stringify(data) === last_local_snap) return;
last_local_snap = JSON.stringify(data);
needUpdate = true;
let local_snaps = $("#local_snaps");
local_snaps.empty();
local_snaps.html(data);
}).always(() => {
updateManuBackup(needUpdate);
});
}
function updateManuBackup(prevUpdate) {
let needUpdate = false;
$.get("./api/formated-backup-manual", (data) => {
if (JSON.stringify(data) === last_manu_back) return;
last_manu_back = JSON.stringify(data);
needUpdate = true;
let manual_backups = $("#manual_backups");
manual_backups.empty();
manual_backups.html(data);
}).always(() => {
updateAutoBackup(prevUpdate || needUpdate);
});
}
function updateAutoBackup(prevUpdate) {
let needUpdate = false;
$.get("./api/formated-backup-auto", (data) => {
if (JSON.stringify(data) === last_auto_back) return;
needUpdate = true;
last_auto_back = JSON.stringify(data);
let auto_backups = $("#auto_backups");
auto_backups.empty();
auto_backups.html(data);
}).always(() => {
if (prevUpdate || needUpdate) updateDynamicListeners();
});
}
function update_status() {
$.get("./api/status", (data) => {
if (JSON.stringify(data) !== last_status) {
last_status = JSON.stringify(data);
let buttons = $("#btn-backup-now, #btn-clean-now");
switch (data.status) {
case "error":
printStatus("Error", data.message);
buttons.removeClass("disabled");
break;
case "idle":
printStatus("Idle", "Waiting for next backup.");
buttons.removeClass("disabled");
break;
case "download":
printStatusWithBar("Downloading Snapshot", data.progress);
buttons.addClass("disabled");
break;
case "download-b":
printStatusWithBar("Downloading Backup", data.progress);
buttons.addClass("disabled");
break;
case "upload":
printStatusWithBar("Uploading Snapshot", data.progress);
buttons.addClass("disabled");
break;
case "upload-b":
printStatusWithBar("Uploading Snapshot", data.progress);
buttons.addClass("disabled");
break;
case "creating":
printStatusWithBar("Creating Snapshot", data.progress);
buttons.addClass("disabled");
break;
case "stopping":
printStatusWithBar("Stopping addons", data.progress);
buttons.addClass("disabled");
break;
case "starting":
printStatusWithBar("Starting addons", data.progress);
buttons.addClass("disabled");
break;
}
if (data.last_backup != null) {
let last_back_status = $("#last_back_status");
if (last_back_status.html() !== data.last_backup) last_back_status.html(data.last_backup);
}
if (data.next_backup != null) {
let next_back_status = $("#next_back_status");
if (next_back_status.html() !== data.next_backup) next_back_status.html(data.next_backup);
}
}
});
}
function printStatus(status, secondLine) {
let status_jq = $("#status");
status_jq.empty();
status_jq.html(status);
let status_s_l_jq = $("#status-second-line");
status_s_l_jq.empty();
status_s_l_jq.removeClass("text-center");
status_s_l_jq.html(secondLine);
$("#progress").addClass("invisible");
}
function printStatusWithBar(status, progress) {
let status_jq = $("#status");
status_jq.empty();
status_jq.html(status);
let secondLine = $("#status-second-line");
secondLine.empty();
secondLine.html(progress === -1 ? "Waiting..." : Math.round(progress * 100) + "%");
secondLine.addClass("text-center");
let progressDiv = $("#progress");
progressDiv.removeClass("invisible");
if (progress === -1) {
progressDiv.children().css("width", "100%");
progressDiv.children().addClass("progress-bar-striped progress-bar-animated");
} else {
progressDiv.children().removeClass("progress-bar-striped progress-bar-animated");
progressDiv.children().css("width", progress * 100 + "%");
}
}
function listeners() {
$("#btn-backup-now").on("click", backupNow);
$("#btn-clean-now").on("click", cleanNow);
$("#trigger-backup-settings").on("click", getBackupSettings);
$("#password_protected").on("change", function () {
if (!$(this).is(":checked")) {
$("#password_protect_value").parent().parent().addClass("d-none");
} else {
$("#password_protect_value").parent().parent().removeClass("d-none");
}
});
$("#cron-drop-settings").on("change", updateDropVisibility);
$("#save-backup-settings").click(sendBackupSettings);
$("#auto_clean_local").on("change", function () {
if (!$(this).is(":checked")) {
$("#local-snap-keep").parent().parent().addClass("d-none");
} else {
$("#local-snap-keep").parent().parent().removeClass("d-none");
}
});
$("#auto_clean_backup").on("change", function () {
if (!$(this).is(":checked")) {
$("#backup-snap-keep").parent().parent().addClass("d-none");
} else {
$("#backup-snap-keep").parent().parent().removeClass("d-none");
}
});
$("#trigger-nextcloud-settings").click(getNextcloudSettings);
$("#save-nextcloud-settings").click(sendNextcloudSettings);
$("#ssl").on("change", function () {
let div = $("#self_signed").parent().parent();
if ($("#ssl").is(":checked")) div.removeClass("invisible");
else div.addClass("invisible");
});
}
function restore(id) {
loadingModal.show();
$.post("./api/restore", { path: id })
.done(() => {
console.log("Restore cmd send !");
create_toast("success", "Command sent !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
})
.always(() => loadingModal.hide());
}
function sendNextcloudSettings() {
loadingModal.show();
nextcloud_setting_modal.hide();
let ssl = $("#ssl").is(":checked");
let self_signed = $("#self_signed").is(":checked");
let hostname = $("#hostname").val();
let username = $("#username").val();
let password = $("#password").val();
let back_dir = $("#back-dir").val();
$.post("./api/nextcloud-settings", {
ssl: ssl,
host: hostname,
username: username,
password: password,
back_dir: back_dir,
self_signed: self_signed,
})
.done(() => {
console.log("Saved");
$("#nextcloud_settings_message").parent().addClass("d-none");
create_toast("success", "Nextcloud settings saved !", default_toast_timeout);
})
.fail((data) => {
let nextcloud_settings_message = $("#nextcloud_settings_message");
if (data.status === 406) {
console.log(data.responseJSON.message);
nextcloud_settings_message.html(data.responseJSON.message);
} else {
nextcloud_settings_message.html("Invalid Settings.");
}
nextcloud_settings_message.parent().removeClass("d-none");
nextcloud_setting_modal.show();
create_toast("error", "Invalid Nextcloud settings !", default_toast_timeout);
console.log("Fail");
})
.always(() => {
loadingModal.hide();
});
}
function manualBackup(id, name) {
$.post(`./api/manual-backup?id=${id}&name=${name}`)
.done(() => {
console.log("manual bk cmd send !");
create_toast("success", "Command sent !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
});
}
function getNextcloudSettings() {
loadingModal.show();
$.get("./api/nextcloud-settings", (data) => {
$("#ssl").prop("checked", data.ssl === "true");
let sef_signed_jq = $("#self_signed");
if (data.ssl === "true") {
let div = sef_signed_jq.parent().parent();
div.removeClass("invisible");
}
sef_signed_jq.prop("checked", data.self_signed === "true");
$("#hostname").val(data.host);
$("#username").val(data.username);
$("#password").val(data.password);
$("#back-dir").val(data.back_dir);
nextcloud_setting_modal.show();
})
.fail((error) => {
if (error.status === 404) nextcloud_setting_modal.show();
else {
console.log(error);
create_toast("error", "Failed to fetch Nextcloud config !", default_toast_timeout);
}
})
.always(() => {
loadingModal.hide();
});
}
function backupNow() {
loadingModal.show();
$.post("./api/new-backup")
.done(() => {
create_toast("success", "Command sent !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
})
.always(() => {
loadingModal.hide();
});
}
function cleanNow() {
loadingModal.show();
$.post("./api/clean-now")
.done(() => {
create_toast("success", "Command sent !", default_toast_timeout);
})
.fail((error) => {
console.log(error);
create_toast("error", "Can't send command !", default_toast_timeout);
})
.always(() => {
loadingModal.hide();
});
}
function getBackupSettings() {
loadingModal.show();
$.get("./api/backup-settings", (data) => {
if (JSON.stringify(data) === "{}") {
data = {
settings: {
cron_base: "0",
cron_hour: "00:00",
cron_weekday: "0",
cron_month_day: "1",
cron_custom: "5 4 * * *",
auto_clean_local: false,
auto_clean_local_keep: 5,
auto_clean_backup: false,
auto_clean_backup_keep: 5,
}
};
}
if (data.settings.cron_custom == null || data.settings.cron_custom == "") {
data.settings.cron_custom = "5 4 * * *";
}
$("#cron-drop-settings").val(data.settings.cron_base);
$("#name-template").val(data.settings.name_template);
$("#timepicker").val(data.settings.cron_hour);
$("#cron-drop-day-month-read").val(data.settings.cron_month_day);
$("#cron-drop-day-month").val(data.settings.cron_month_day);
$("#cron-drop-custom").val(data.settings.cron_custom);
$("#auto_clean_local").prop("checked", data.settings.auto_clean_local === "true");
let local_snap_keep = $("#local-snap-keep");
if (data.settings.auto_clean_local === "true") local_snap_keep.parent().parent().removeClass("d-none");
else local_snap_keep.parent().parent().addClass("d-none");
local_snap_keep.val(data.settings.auto_clean_local_keep);
$("#auto_clean_backup").prop("checked", data.settings.auto_clean_backup === "true");
let backup_snap_keep = $("#backup-snap-keep");
if (data.settings.auto_clean_backup === "true") backup_snap_keep.parent().parent().removeClass("d-none");
else backup_snap_keep.parent().parent().addClass("d-none");
backup_snap_keep.val(data.settings.auto_clean_backup_keep);
$("#cron-drop-day").val(data.settings.cron_weekday);
let folder_html = "";
for (let thisFolder of data.folders) {
let exclude = data.settings.exclude_folder.includes(thisFolder.slug);
folder_html += `<li class="list-group-item"><div class="form-check"><input class="form-check-input folders-box" type="checkbox" id="${
thisFolder.slug
}" ${exclude ? "" : "checked"}><label class="form-label mb-0" for="${thisFolder.slug}">${thisFolder.name}</label></div></li>`;
}
$("#folders-div").html(folder_html);
let addons_html = "";
for (let thisAddon of data.addonList) {
let exclude = data.settings.exclude_addon.includes(thisAddon.slug);
addons_html += `<li class="list-group-item"><div class="form-check"><input class="form-check-input addons-box" type="checkbox" id="${
thisAddon.slug
}" ${exclude ? "" : "checked"}><label class="form-label mb-0" for="${thisAddon.slug}">${thisAddon.name}</label></div></li>`;
}
$("#addons-div").html(addons_html);
let addons_stop_html = "";
for (let thisAddon of data.addonList) {
if (thisAddon.slug !== "229cc4d7_nextcloud_backup") {
let on = data.settings.auto_stop_addon.includes(thisAddon.slug);
addons_stop_html += `<li class="list-group-item"><div class="form-check"><input class="form-check-input stop-addons-box" type="checkbox" id="${
thisAddon.slug
}" ${on ? "checked" : ""}><label class="form-label mb-0" for="${thisAddon.slug}">${thisAddon.name}</label></div></li>`;
}
}
$("#auto-stop-addons-div").html(addons_stop_html);
updateDropVisibility();
backup_setting_modal.show();
})
.fail((error) => {
console.log(error);
create_toast("error", "Failed to fetch Nextcloud config !", default_toast_timeout);
})
.always(() => {
loadingModal.hide();
});
}
function updateDropVisibility() {
let cronBase = $("#cron-drop-settings").val();
let timepicker = $("#timepicker");
let cron_drop_day = $("#cron-drop-day");
let cron_drop_day_mount = $("#cron-drop-day-month");
let cron_drop_custom = $("#cron-drop-custom");
switch (cronBase) {
case "4":
timepicker.parent().parent().addClass("d-none");
cron_drop_day.parent().parent().addClass("d-none");
cron_drop_day_mount.parent().parent().addClass("d-none");
cron_drop_custom.parent().parent().removeClass("d-none");
break;
case "3":
timepicker.parent().parent().removeClass("d-none");
cron_drop_day.parent().parent().addClass("d-none");
cron_drop_day_mount.parent().parent().removeClass("d-none");
cron_drop_custom.parent().parent().addClass("d-none");
break;
case "2":
timepicker.parent().parent().removeClass("d-none");
cron_drop_day.parent().parent().removeClass("d-none");
cron_drop_day_mount.parent().parent().addClass("d-none");
cron_drop_custom.parent().parent().addClass("d-none");
break;
case "1":
timepicker.parent().parent().removeClass("d-none");
cron_drop_day.parent().parent().addClass("d-none");
cron_drop_day_mount.parent().parent().addClass("d-none");
cron_drop_custom.parent().parent().addClass("d-none");
break;
case "0":
timepicker.parent().parent().addClass("d-none");
cron_drop_day.parent().parent().addClass("d-none");
cron_drop_day_mount.parent().parent().addClass("d-none");
cron_drop_custom.parent().parent().addClass("d-none");
break;
}
}
function sendBackupSettings() {
let cron_month_day = $("#cron-drop-day-month").val();
let cron_weekday = $("#cron-drop-day").val();
let cron_hour = $("#timepicker").val();
let cron_custom = $("#cron-drop-custom").val();
let cron_base = $("#cron-drop-settings").val();
let auto_clean_local = $("#auto_clean_local").is(":checked");
let auto_clean_backup = $("#auto_clean_backup").is(":checked");
let auto_clean_local_keep = $("#local-snap-keep").val();
let auto_clean_backup_keep = $("#backup-snap-keep").val();
let name_template = $("#name-template").val();
let excluded_folders_nodes = document.querySelectorAll(".folders-box:not(:checked)");
let exclude_folder = [""];
let password_protected = $("#password_protected").is(":checked");
let password_protect_value = $("#password_protect_value").val();
for (let i of excluded_folders_nodes) {
exclude_folder.push(i.id);
}
let excluded_addons_nodes = document.querySelectorAll(".addons-box:not(:checked)");
let exclude_addon = [""];
for (let i of excluded_addons_nodes) {
exclude_addon.push(i.id);
}
let stop_addons_nodes = document.querySelectorAll(".stop-addons-box:checked");
let stop_addon = [""];
for (let i of stop_addons_nodes) {
stop_addon.push(i.id);
}
loadingModal.show();
backup_setting_modal.hide();
$.post("./api/backup-settings", {
name_template: name_template,
cron_base: cron_base,
cron_hour: cron_hour,
cron_weekday: cron_weekday,
cron_month_day: cron_month_day,
cron_custom: cron_custom,
auto_clean_local: auto_clean_local,
auto_clean_local_keep: auto_clean_local_keep,
auto_clean_backup: auto_clean_backup,
auto_clean_backup_keep: auto_clean_backup_keep,
exclude_addon: exclude_addon,
exclude_folder: exclude_folder,
auto_stop_addon: stop_addon,
password_protected: password_protected,
password_protect_value: password_protect_value,
})
.done(() => {
create_toast("success", "Backup settings saved !", default_toast_timeout);
})
.fail((data) => {
debugger
create_toast("error", `Can't save backup settings ! <br> Error: ${data.responseText}`, default_toast_timeout);
backup_setting_modal.show();
})
.always(() => {
loadingModal.hide();
});
}

View File

@ -1,30 +0,0 @@
function create_toast(type, message, delay) {
let toast_class;
let icon_class;
switch (type) {
case 'error':
toast_class = 'bg-danger';
icon_class = 'fa-exclamation-triangle'
break;
default:
toast_class = `bg-${type}`
icon_class = 'fa-check-square'
}
let toast_id = Date.now().toString();
let toast_html = `<div id="${toast_id}" class="toast d-flex align-items-center text-white ${toast_class}" role="alert" aria-live="assertive" aria-atomic="true">`
toast_html += `<div class="toast-body h6 mb-0 align-middle"><i class="fas ${icon_class} me-2 h5 mb-0"></i><span>${message}</span></div>`
toast_html += `<button type="button" class="btn-close btn-close-white ms-auto me-2" data-bs-dismiss="toast" aria-label="Close"></button></div>`
$('#toast-container').prepend(toast_html);
let toast_dom = document.getElementById(toast_id)
let toast = new bootstrap.Toast(toast_dom, {
animation: true,
autohide: delay !== -1,
delay: delay
});
toast_dom.addEventListener('hidden.bs.toast', function () {
this.remove();
});
toast.show();
return toast;
}

View File

@ -1,229 +0,0 @@
import express from 'express';
import * as statusTools from "../tools/status.js"
import webdav from "../tools/webdavTools.js"
import * as settingsTools from "../tools/settingsTools.js"
import * as pathTools from "../tools/pathTools.js"
import * as hassioApiTools from "../tools/hassioApiTools.js"
import { humanFileSize } from "../tools/toolbox.js";
import cronTools from "../tools/cronTools.js"
import logger from "../config/winston.js"
import {DateTime} from "luxon";
var router = express.Router();
router.get("/status", (req, res, next) => {
cronTools.updateNextDate();
let status = statusTools.getStatus();
res.json(status);
});
router.get("/formated-local-snap", function (req, res, next) {
hassioApiTools.getSnapshots()
.then((snaps) => {
snaps.sort((a, b) => {
return a.date < b.date ? 1 : -1
});
res.render("localSnaps", { snaps: snaps, DateTime: DateTime });
})
.catch((err) => {
logger.error(err);
res.status(500);
res.send("");
}
);
});
router.get("/formated-backup-manual", function (req, res, next) {
if (webdav.getConf() == null) {
res.send("");
return;
}
webdav
.getFolderContent(webdav.getConf().back_dir + pathTools.manual)
.then((contents) => {
contents.sort((a, b) => {
return a.date < b.date ? 1 : -1
});
//TODO Remove this when bug is fixed, etag contain '&quot;' at start and end ?
for (let backup of contents) {
backup.etag = backup.etag.replace(/&quot;/g, '');
}
res.render("backupSnaps", { backups: contents, DateTime: DateTime, humanFileSize: humanFileSize });
})
.catch((err) => {
res.status(500)
res.send(err);
});
});
router.get("/formated-backup-auto", function (req, res, next) {
if (webdav.getConf() == null) {
res.send("");
return;
}
let url = webdav.getConf().back_dir + pathTools.auto;
webdav
.getFolderContent(url)
.then((contents) => {
contents.sort((a, b) => {
return a.date < b.date ? 1 : -1
});
//TODO Remove this when bug is fixed, etag contain '&quot;' at start and end ?
for (let backup of contents) {
backup.etag = backup.etag.replace(/&quot;/g, '');
}
res.render("backupSnaps", { backups: contents, DateTime: DateTime, humanFileSize: humanFileSize });
})
.catch((err) => {
res.status(500)
res.send(err);
});
});
router.post("/nextcloud-settings", function (req, res, next) {
let settings = req.body;
if (settings.ssl != null && settings.host != null && settings.host !== "" && settings.username != null && settings.password != null) {
webdav.setConf(settings);
webdav
.confIsValid()
.then(() => {
res.status(201);
res.send();
})
.catch((err) => {
res.status(406);
res.json({ message: err });
});
} else {
res.status(400);
res.send();
}
});
router.get("/nextcloud-settings", function (req, res, next) {
let conf = webdav.getConf();
if (conf == null) {
res.status(404);
res.send();
} else {
res.json(conf);
}
});
router.post("/manual-backup", function (req, res, next) {
let id = req.query.id;
let name = req.query.name;
let status = statusTools.getStatus();
if (status.status === "creating" && status.status === "upload" && status.status === "download") {
res.status(503);
res.send();
return;
}
hassioApiTools
.downloadSnapshot(id)
.then(() => {
webdav.uploadFile(id, webdav.getConf().back_dir + pathTools.manual + name + ".tar").then(()=>{
res.status(201);
res.send();
}).catch(()=>{
res.status(500);
res.send();
}
);
})
.catch(() => {
res.status(500);
res.send();
});
});
router.post("/new-backup", function (req, res, next) {
let status = statusTools.getStatus();
if (status.status === "creating" || status.status === "upload" || status.status === "download" || status.status === "stopping" || status.status === "starting") {
res.status(503);
res.send();
return;
}
hassioApiTools.stopAddons()
.then(() => {
hassioApiTools.getVersion()
.then((version) => {
let name = settingsTools.getFormatedName(true, version);
hassioApiTools.createNewBackup(name)
.then((id) => {
hassioApiTools
.downloadSnapshot(id)
.then(() => {
webdav.uploadFile(id, webdav.getConf().back_dir + pathTools.manual + name + ".tar")
.then(() => {
hassioApiTools.startAddons().catch(() => {
})
}).catch(()=>{});
}).catch(()=>{});
}).catch(()=>{});
}).catch(()=>{});
})
.catch(() => {
hassioApiTools.startAddons().catch(() => {
});
});
res.status(201);
res.send();
});
router.get("/backup-settings", function (req, res, next) {
hassioApiTools.getAddonList().then((addonList) => {
let data = {};
data['folders'] = hassioApiTools.getFolderList();
data['addonList'] = addonList;
data['settings'] = settingsTools.getSettings();
res.send(data);
})
});
router.post("/backup-settings", function (req, res, next) {
let [result, message] = settingsTools.check(req.body)
if (result) {
settingsTools.setSettings(req.body);
cronTools.init();
res.send();
} else {
res.status(400);
res.send(message);
}
});
router.post("/clean-now", function (req, res, next) {
webdav
.clean()
.then(() => {
hassioApiTools.clean().catch();
})
.catch(() => {
hassioApiTools.clean().catch();
});
res.status(201);
res.send();
});
router.post("/restore", function (req, res, next) {
if (req.body["path"] != null) {
webdav.downloadFile(req.body["path"]).then((path) => {
hassioApiTools.uploadSnapshot(path).catch();
});
res.status(200);
res.send();
} else {
res.status(400);
res.send();
}
});
export default router;

View File

@ -1,9 +0,0 @@
import express from 'express';
var router = express.Router();
/* GET home page. */
router.get("/", function (req, res, next) {
res.render("index");
});
export default router;

View File

@ -1,79 +0,0 @@
$body-bg: #222222;
$dark: #292929;
$secondary: #343a40;
$accent: #b58e51;
$custom-colors:(
"accent": $accent
);
$enable-shadows: true;
$btn-box-shadow: none;
$component-active-bg: $accent;
$input-color: $accent;
$input-bg: $secondary;
$input-border-color: $secondary;
$form-select-indicator-color: $accent;
$form-switch-color: $accent;
$form-switch-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-color}'/></svg>");
$form-check-input-border: 1px solid $accent !default;
$list-group-action-color: $accent;
$list-group-bg: $secondary;
$list-group-hover-bg: $secondary;
$list-group-action-hover-color: #adb5bd;
$list-group-action-active-bg: $body-bg;
$list-group-action-active-color: $accent;
$alert-bg-scale: 0%;
$alert-border-scale: -10%;
$alert-color-scale: -100%;
// Configuration
@import "../node_modules/bootstrap/scss/functions";
@import "../node_modules/bootstrap/scss/variables";
$theme-colors: map-merge($theme-colors, $custom-colors);
@import "../node_modules/bootstrap/scss/mixins";
@import "../node_modules/bootstrap/scss/utilities";
//All other bootstrap imports
@import "bootstrap_imports.scss";
input[type="time"] {
&::-webkit-calendar-picker-indicator {
filter: invert(63%) sepia(23%) saturate(819%) hue-rotate(358deg) brightness(88%) contrast(91%);
height: 30px;
margin: -12px;
width: 32px;
}
&::-webkit-calendar-picker-indicator:hover {
cursor: pointer;
}
}
.modal-dialog-scrollable .modal-body {
&::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
background-color: $secondary;
}
&::-webkit-scrollbar {
width: 12px;
background-color: $dark;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: $accent;
}
}

View File

@ -1,37 +0,0 @@
// Layout & components
@import "../node_modules/bootstrap/scss/root";
@import "../node_modules/bootstrap/scss/reboot";
@import "../node_modules/bootstrap/scss/type";
@import "../node_modules/bootstrap/scss/images";
@import "../node_modules/bootstrap/scss/containers";
@import "../node_modules/bootstrap/scss/grid";
//@import "tables";
@import "../node_modules/bootstrap/scss/forms";
@import "../node_modules/bootstrap/scss/buttons";
@import "../node_modules/bootstrap/scss/transitions";
@import "../node_modules/bootstrap/scss/dropdown";
//@import "button-group";
//@import "nav";
@import "../node_modules/bootstrap/scss/navbar";
@import "../node_modules/bootstrap/scss/card";
//@import "accordion";
//@import "breadcrumb";
//@import "pagination";
@import "../node_modules/bootstrap/scss/badge";
@import "../node_modules/bootstrap/scss/alert";
@import "../node_modules/bootstrap/scss/progress";
@import "../node_modules/bootstrap/scss/list-group";
@import "../node_modules/bootstrap/scss/close";
@import "../node_modules/bootstrap/scss/toasts";
@import "../node_modules/bootstrap/scss/modal";
//@import "tooltip";
//@import "popover";
//@import "carousel";
@import "../node_modules/bootstrap/scss/spinners";
// Helpers
@import "../node_modules/bootstrap/scss/helpers";
// Utilities
@import "../node_modules/bootstrap/scss/utilities/api";
// scss-docs-end import-stack

View File

@ -1,121 +0,0 @@
import { CronJob } from "cron"
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 logger from "../config/winston.js"
class CronContainer {
constructor() {
this.cronJob = null;
this.cronClean = null;
}
init() {
let settings = settingsTools.getSettings();
let cronStr = "";
if (this.cronClean == null) {
logger.info("Starting auto clean cron...");
this.cronClean = new CronJob("0 1 * * *", this._clean, null, false, Intl.DateTimeFormat().resolvedOptions().timeZone);
this.cronClean.start();
}
if (this.cronJob != null) {
logger.info("Stopping Cron...");
this.cronJob.stop();
this.cronJob = null;
}
if (!settingsTools.check_cron(settingsTools.getSettings())) {
logger.warn("No Cron settings available.");
return;
}
switch (settings.cron_base) {
case "0":
logger.warn("No Cron settings available.");
return;
case "1": {
let splited = settings.cron_hour.split(":");
cronStr = "" + splited[1] + " " + splited[0] + " * * *";
break;
}
case "2": {
let splited = settings.cron_hour.split(":");
cronStr = "" + splited[1] + " " + splited[0] + " * * " + settings.cron_weekday;
break;
}
case "3": {
let splited = settings.cron_hour.split(":");
cronStr = "" + splited[1] + " " + splited[0] + " " + settings.cron_month_day + " * *";
break;
}
case "4": {
cronStr = settings.cron_custom;
break;
}
}
logger.info("Starting Cron...");
this.cronJob = new CronJob(cronStr, this._createBackup, null, false, Intl.DateTimeFormat().resolvedOptions().timeZone);
this.cronJob.start();
this.updateNextDate();
}
updateNextDate() {
let date;
if (this.cronJob == null) date = null;
else date = this.cronJob.nextDate().setLocale("en").toFormat("dd MMM yyyy, HH:mm");
let status = statusTools.getStatus();
status.next_backup = date;
statusTools.setStatus(status);
}
_createBackup() {
logger.debug("Cron triggered !");
let status = statusTools.getStatus();
if (status.status === "creating" || status.status === "upload" || status.status === "download" || status.status === "stopping" || status.status === "starting")
return;
hassioApiTools.stopAddons()
.then(() => {
hassioApiTools.getVersion()
.then((version) => {
let name = settingsTools.getFormatedName(false, version);
hassioApiTools.createNewBackup(name)
.then((id) => {
hassioApiTools
.downloadSnapshot(id)
.then(() => {
webdav.uploadFile(id, webdav.getConf().back_dir + pathTools.auto + name + ".tar")
.then(() => {
hassioApiTools.startAddons().catch(() => {
});
});
});
});
});
})
.catch(() => {
hassioApiTools.startAddons().catch(() => {
});
});
}
_clean() {
let autoCleanlocal = settingsTools.getSettings().auto_clean_local;
if (autoCleanlocal != null && autoCleanlocal === "true") {
hassioApiTools.clean().catch();
}
let autoCleanCloud = settingsTools.getSettings().auto_clean_backup;
if (autoCleanCloud != null && autoCleanCloud === "true") {
webdav.clean().catch();
}
}
}
const INSTANCE = new CronContainer();
export default INSTANCE;

View File

@ -1,536 +0,0 @@
import fs from "fs"
import stream from "stream"
import { promisify } from "util";
import got from "got";
import FormData from "form-data";
import * as statusTools from "../tools/status.js"
import * as settingsTools from "../tools/settingsTools.js"
import logger from "../config/winston.js"
import {DateTime} from "luxon";
const pipeline = promisify(stream.pipeline);
const token = process.env.SUPERVISOR_TOKEN;
// Default timeout to 90min
const create_snap_timeout = parseInt(process.env.CREATE_BACKUP_TIMEOUT) || (90 * 60 * 1000);
function getVersion() {
return new Promise((resolve, reject) => {
let status = statusTools.getStatus();
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
};
got("http://hassio/core/info", option)
.then((result) => {
if (status.error_code === 1) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
let version = result.body.data.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) => {
let status = statusTools.getStatus();
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
};
got("http://hassio/addons", option)
.then((result) => {
if (status.error_code === 1) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
let addons = result.body.data.addons;
addons.sort((a, b) => {
let textA = a.name.toUpperCase();
let 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) => {
let excluded_addon = settingsTools.getSettings().exclude_addon;
getAddonList()
.then((all_addon) => {
let slugs = [];
for (let 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() {
let excluded_folder = settingsTools.getSettings().exclude_folder;
let all_folder = getFolderList()
let slugs = [];
for (let 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) => {
let status = statusTools.getStatus();
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
};
got("http://hassio/backups", option)
.then((result) => {
if (status.error_code === 1) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
let snaps = result.body.data.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) {
return new Promise((resolve, reject) => {
logger.info(`Downloading snapshot ${id}...`);
if (!fs.existsSync("./temp/")) fs.mkdirSync("./temp/");
let tmp_file = `./temp/${id}.tar`;
let stream = fs.createWriteStream(tmp_file);
let status = statusTools.getStatus();
checkSnap(id)
.then(() => {
status.status = "download";
status.progress = 0;
statusTools.setStatus(status);
let option = {
headers: { "Authorization": `Bearer ${token}` },
};
pipeline(
got.stream.get(`http://hassio/backups/${id}/download`, option)
.on("downloadProgress", (e) => {
let 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();
})
.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) {
return new Promise((resolve, reject) => {
checkSnap(id)
.then(() => {
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
};
got.delete(`http://hassio/backups/${id}`, option)
.then(() => resolve())
.catch((e) => {
logger.error(e)
reject();
});
})
.catch(() => {
reject();
});
});
}
function checkSnap(id) {
return new Promise((resolve, reject) => {
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
};
got(`http://hassio/backups/${id}/info`, option)
.then((result) => {
logger.debug(`Snapshot size: ${result.body.data.size}`);
resolve();
})
.catch(() => reject());
});
}
function createNewBackup(name) {
return new Promise((resolve, reject) => {
let status = statusTools.getStatus();
status.status = "creating";
status.progress = -1;
statusTools.setStatus(status);
logger.info("Creating new snapshot...");
getAddonToBackup().then((addons) => {
let folders = getFolderToBackup();
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
timeout: {
response: create_snap_timeout
},
json: {
name: name,
addons: addons,
folders: folders
},
};
let password_protected = settingsTools.getSettings().password_protected;
logger.debug(`Is password protected ? ${password_protected}`)
if ( password_protected === "true") {
option.json.password = settingsTools.getSettings().password_protect_value
}
got.post(`http://hassio/backups/new/partial`, option)
.then((result) => {
logger.info(`Snapshot created with id ${result.body.data.slug}`);
resolve(result.body.data.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();
return;
}
snaps.sort((a, b) => {
return a.date < b.date ? 1 : -1
});
let toDel = snaps.slice(limit);
for (let i of toDel) {
await dellSnap(i.slug);
}
logger.info("Local clean done.");
resolve();
})
.catch((e) => {
statusTools.setError(`Fail to clean backups (${e}) !`, 6);
reject(`Fail to clean backups (${e}) !`);
});
});
}
function uploadSnapshot(path) {
return new Promise((resolve, reject) => {
let status = statusTools.getStatus();
status.status = "upload-b";
status.progress = 0;
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
logger.info("Uploading backup...");
let stream = fs.createReadStream(path);
let form = new FormData();
form.append("file", stream);
let options = {
body: form,
headers: { "authorization": `Bearer ${token}` },
};
got.stream
.post(`http://hassio/backups/new/upload`, options)
.on("uploadProgress", (e) => {
let 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 = null;
status.error_code = null;
statusTools.setStatus(status);
fs.unlinkSync(path);
resolve();
}
})
.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...');
let status = statusTools.getStatus();
status.status = "stopping";
status.progress = -1;
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
let promises = [];
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
};
let addons_slug = settingsTools.getSettings().auto_stop_addon
for (let 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 (let 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();
}
});
}));
}
function startAddons() {
return new Promise(((resolve, reject) => {
logger.info('Starting addons...');
let status = statusTools.getStatus();
status.status = "starting";
status.progress = -1;
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
let promises = [];
let option = {
headers: { "authorization": `Bearer ${token}` },
responseType: "json",
};
let addons_slug = settingsTools.getSettings().auto_stop_addon
for (let 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 (let 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 = null;
status.error_code = null;
statusTools.setStatus(status);
resolve();
}
});
}));
}
function publish_state(state) {
// 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
}

View File

@ -1,192 +0,0 @@
import fs from "fs"
import { CronJob } from "cron";
import logger from "../config/winston.js"
import {DateTime} from "luxon";
const settingsPath = "/data/backup_conf.json";
function check_cron(conf) {
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, () => {});
return true;
} catch(e) {
return false;
}
}else return false;
}
if (conf.cron_base === "0") return true;
} else return false;
return false;
}
function check(conf, 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 == null) {
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 == null || !/^\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 == null || !/^\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 == null) {
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 == null) {
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 == null) {
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 == null) {
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 == null) {
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 == null) {
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, ha_version) {
let 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) {
fs.writeFileSync(settingsPath, JSON.stringify(settings));
}
export { getSettings, setSettings, check, check_cron, getFormatedName };

View File

@ -1,426 +0,0 @@
import { createClient } from "webdav";
import fs from "fs"
import https from "https"
import path from "path";
import got from "got";
import stream from "stream";
import { promisify } from "util";
import * as statusTools from "../tools/status.js"
import * as settingsTools from "../tools/settingsTools.js"
import * as pathTools from "../tools/pathTools.js"
import * as hassioApiTools from "../tools/hassioApiTools.js"
import logger from "../config/winston.js"
import {DateTime} from "luxon";
const endpoint = "/remote.php/webdav";
const configPath = "/data/webdav_conf.json";
const pipeline = promisify(stream.pipeline);
class WebdavTools {
constructor() {
this.host = null;
this.client = null;
this.baseUrl = null;
this.username = null;
this.password = null;
}
init(ssl, host, username, password, accept_selfsigned_cert) {
return new Promise((resolve, reject) => {
this.host = host;
let status = statusTools.getStatus();
logger.info("Initializing and checking webdav client...");
this.baseUrl = (ssl === "true" ? "https" : "http") + "://" + host + endpoint;
this.username = username;
this.password = password;
let agent_option = ssl === "true" ? { rejectUnauthorized: accept_selfsigned_cert === "false" } : {};
try {
this.client = createClient(this.baseUrl, {
username: username,
password: password,
httpsAgent: new https.Agent(agent_option)
});
this.client
.getDirectoryContents("/")
.then(() => {
if (status.error_code === 3) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
logger.debug("Nextcloud connection: \x1b[32mSuccess !\x1b[0m");
this.initFolder().then(() => {
resolve();
});
})
.catch((error) => {
this.__cant_connect_status(error);
this.client = null;
reject("Can't connect to Nextcloud (" + error + ") !");
});
} catch (err) {
this.__cant_connect_status(err);
this.client = null;
reject("Can't connect to Nextcloud (" + err + ") !");
}
});
}
__cant_connect_status(err){
statusTools.setError(`Can't connect to Nextcloud (${err})`, 3);
}
async __createRoot() {
let root_splited = this.getConf().back_dir.split("/").splice(1);
let path = "/";
for (let elem of root_splited) {
if (elem !== "") {
path = path + elem + "/";
try {
await this.client.createDirectory(path);
logger.debug(`Path ${path} created.`);
} catch (error) {
if (error.status === 405) logger.debug(`Path ${path} already exist.`);
else logger.error(error);
}
}
}
}
initFolder() {
return new Promise((resolve) => {
this.__createRoot().catch((err) => {
logger.error(err);
}).then(() => {
this.client.createDirectory(this.getConf().back_dir + pathTools.auto)
.catch(() => {
})
.then(() => {
this.client
.createDirectory(this.getConf().back_dir + pathTools.manual)
.catch(() => {
})
.then(() => {
resolve();
});
});
});
});
}
/**
* Check if theh webdav config is valid, if yes, start init of webdav client
*/
confIsValid() {
return new Promise((resolve, reject) => {
let status = statusTools.getStatus();
let conf = this.getConf();
if (conf !== null) {
if (conf.ssl !== null && conf.host !== null && conf.username !== null && conf.password !== null) {
if (status.error_code === 2) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
// Check if self_signed option exist
if (conf.self_signed == null || conf.self_signed === "") {
conf.self_signed = "false";
this.setConf(conf);
}
this.init(conf.ssl, conf.host, conf.username, conf.password, conf.self_signed)
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
} else {
status.status = "error";
status.error_code = 2;
status.message = "Nextcloud config invalid !";
statusTools.setStatus(status);
logger.error(status.message);
reject("Nextcloud config invalid !");
}
if (conf.back_dir == null || conf.back_dir === "") {
logger.info("Backup dir is null, initializing it.");
conf.back_dir = pathTools.default_root;
this.setConf(conf);
} else {
if (!conf.back_dir.startsWith("/")) {
logger.warn("Backup dir not starting with '/', fixing this...");
conf.back_dir = `/${conf.back_dir}`;
this.setConf(conf);
}
if (!conf.back_dir.endsWith("/")) {
logger.warn("Backup dir not ending with '/', fixing this...");
conf.back_dir = `${conf.back_dir}/`;
this.setConf(conf);
}
}
} else {
status.status = "error";
status.error_code = 2;
status.message = "Nextcloud config not found !";
statusTools.setStatus(status);
logger.error(status.message);
reject("Nextcloud config not found !");
}
});
}
getConf() {
if (fs.existsSync(configPath)) {
return JSON.parse(fs.readFileSync(configPath).toString());
} else
return null;
}
setConf(conf) {
fs.writeFileSync(configPath, JSON.stringify(conf));
}
uploadFile(id, path) {
return new Promise((resolve, reject) => {
if (this.client == null) {
this.confIsValid()
.then(() => {
this._startUpload(id, path)
.then(()=> resolve())
.catch((err) => reject(err));
})
.catch((err) => {
reject(err);
});
} else
this._startUpload(id, path)
.then(()=> resolve())
.catch((err) => reject(err));
});
}
_startUpload(id, path) {
return new Promise((resolve, reject) => {
let status = statusTools.getStatus();
status.status = "upload";
status.progress = 0;
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
logger.info("Uploading snap...");
let tmpFile = `./temp/${id}.tar`;
let stats = fs.statSync(tmpFile);
let stream = fs.createReadStream(tmpFile);
let conf = this.getConf();
let options = {
body: stream,
// username: this.username,
// password: encodeURIComponent(this.password),
headers: {
'authorization': 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64'),
'content-length': String(stats.size)
}
};
if (conf.ssl === "true") {
options["https"] = { rejectUnauthorized: conf.self_signed === "false" };
}
logger.debug(`...URI: ${encodeURI(this.baseUrl.replace(this.host, 'host.hiden') + path)}`);
if (conf.ssl === "true")
logger.debug(`...rejectUnauthorized: ${options["https"]["rejectUnauthorized"]}`);
got.stream
.put(encodeURI(this.baseUrl + path), options)
.on("uploadProgress", (e) => {
let 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 !== 201 && res.statusCode !== 204) {
status.status = "error";
status.error_code = 4;
status.message = `Fail to upload snapshot to nextcloud (Status code: ${res.statusCode})!`;
statusTools.setStatus(status);
logger.error(status.message);
fs.unlinkSync(tmpFile);
reject(status.message);
} else {
logger.info(`...Upload finish ! (status: ${res.statusCode})`);
status.status = "idle";
status.progress = -1;
status.message = null;
status.error_code = null;
status.last_backup = DateTime.now().toFormat("dd MMM yyyy, HH:mm")
statusTools.setStatus(status);
cleanTempFolder();
let autoCleanCloud = settingsTools.getSettings().auto_clean_backup;
if (autoCleanCloud != null && autoCleanCloud === "true") {
this.clean().catch();
}
let autoCleanlocal = settingsTools.getSettings().auto_clean_local;
if (autoCleanlocal != null && autoCleanlocal === "true") {
hassioApiTools.clean().catch();
}
resolve();
}
})
.on("error", (err) => {
fs.unlinkSync(tmpFile);
status.status = "error";
status.error_code = 4;
status.message = `Fail to upload snapshot to nextcloud (${err}) !`;
statusTools.setStatus(status);
logger.error(status.message);
logger.error(err.stack);
reject(status.message);
});
});
}
downloadFile(path) {
return new Promise((resolve, reject) => {
if (this.client == null) {
this.confIsValid()
.then(() => {
this._startDownload(path)
.then((path) => resolve(path))
.catch(() => reject());
})
.catch((err) => {
reject(err);
});
} else
this._startDownload(path)
.then((path) => resolve(path))
.catch(() => reject());
});
}
_startDownload(path) {
return new Promise((resolve, reject) => {
let status = statusTools.getStatus();
status.status = "download-b";
status.progress = 0;
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
logger.info("Downloading backup...");
if (!fs.existsSync("./temp/"))
fs.mkdirSync("./temp/");
let tmpFile = `./temp/restore_${DateTime.now().toFormat("MMM-dd-yyyy_HH_mm")}.tar`;
let stream = fs.createWriteStream(tmpFile);
let conf = this.getConf();
let options = {
headers: {
'authorization': 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64')
}
};
if (conf.ssl === "true") {
options["https"] = { rejectUnauthorized: conf.self_signed === "false" };
}
logger.debug(`...URI: ${encodeURI(this.baseUrl.replace(this.host, 'host.hiden') + path)}`);
if (conf.ssl === "true")
logger.debug(`...rejectUnauthorized: ${options["https"]["rejectUnauthorized"]}`);
pipeline(
got.stream.get(encodeURI(this.baseUrl + path), options)
.on("downloadProgress", (e) => {
let percent = Math.round(e.percent * 100) / 100;
if (status.progress !== percent) {
status.progress = percent;
statusTools.setStatus(status);
}
}),
stream
).then((res) => {
logger.info("Download success !");
status.progress = 1;
statusTools.setStatus(status);
logger.debug("Backup dl size : " + fs.statSync(tmpFile).size / 1024 / 1024);
resolve(tmpFile);
}).catch((err) => {
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
status.status = "error";
status.message = "Fail to download Hassio snapshot (" + err.message + ")";
status.error_code = 7;
statusTools.setStatus(status);
logger.error(status.message);
logger.error(err.stack);
reject(err.message);
});
});
}
getFolderContent(path) {
return new Promise((resolve, reject) => {
if (this.client == null) {
reject();
return;
}
this.client
.getDirectoryContents(path)
.then((contents) => resolve(contents))
.catch((error) => reject(error));
});
}
clean() {
let limit = settingsTools.getSettings().auto_clean_backup_keep;
if (limit == null) limit = 5;
return new Promise((resolve, reject) => {
this.getFolderContent(this.getConf().back_dir + pathTools.auto)
.then(async (contents) => {
if (contents.length < limit) {
resolve();
return;
}
contents.sort((a, b) => {
return a.date < b.date ? 1 : -1
});
let toDel = contents.slice(limit);
for (let i in toDel) {
await this.client.deleteFile(toDel[i].filename);
}
logger.info("Cloud clean done.");
resolve();
})
.catch((error) => {
let status = statusTools.getStatus();
status.status = "error";
status.error_code = 6;
status.message = "Fail to clean Nexcloud (" + error + ") !";
statusTools.setStatus(status);
logger.error(status.message);
reject(status.message);
});
});
}
}
function cleanTempFolder() {
fs.readdir("./temp/", (err, files) => {
if (err)
throw err;
for (const file of files) {
fs.unlink(path.join("./temp/", file), (err) => {
if (err)
throw err;
});
}
});
}
const INSTANCE = new WebdavTools();
export default INSTANCE;

View File

@ -1,66 +0,0 @@
<% if (locals.backups) { %>
<div class="list-group ">
<% for(const index in backups) { %>
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
href="#"
data-id="<%= backups[index].etag %>"
data-bs-toggle="modal"
data-bs-target="#modal-<%= backups[index].etag %>">
<%= backups[index].basename %>
<span class="badge bg-primary">
<%= DateTime.fromRFC2822(backups[index].lastmod).toFormat("MMM dd, yyyy HH:mm") %>
</span>
</a>
<div id="modal-<%= backups[index].etag %>" class="modal fade">
<div class="modal-dialog modal-lg">
<div class="modal-content bg-dark">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="exampleModalLabel"> Backup Detail</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="name-<%= backups[index].etag %>" class="form-label">Name</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent"
id="name-<%= backups[index].etag %>"
value="<%= backups[index].basename %>"/>
</div>
<div class="mb-3">
<label for="date-<%= backups[index].etag %>" class="form-label">Date</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent"
id="date-<%= backups[index].etag %>"
value="<%= DateTime.fromRFC2822(backups[index].lastmod).toFormat("MMM dd, yyyy HH:mm") %>"/>
</div>
<div class="input-field col s12">
<label for="size-<%= backups[index].etag %>" class="form-label">Size</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent"
id="size-<%= backups[index].etag %>"
value="<%= humanFileSize(backups[index].size, false) %>"/>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger">Close</button>
<button class="btn btn-success restore"
data-id="<%= backups[index].filename %>"
data-name='<%= backups[index].basename ? backups[index].basename : backups[index].etag %>'
data-bs-dismiss="modal">
Upload to HA
</button>
</div>
</div>
</div>
</div>
<% } %>
</div>
<% } %>

View File

@ -1,3 +0,0 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

View File

@ -1,135 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Nextcloud Backup</title>
<link rel='stylesheet' href='./css/style.css'/>
<link rel="stylesheet" href="./css/bootstrap.css">
<link rel="stylesheet" href="./css/fa-all.min.css">
</head>
<body class="">
<nav class="navbar navbar-dark navbar-expand">
<div class="container">
<a href="#" class="navbar-brand p-0">
<img src="./images/Nextcloud_Logo.svg" height="40">
<span class="align-middle d-none d-sm-inline">Nextcloud Backup</span>
</a>
<ul class="navbar-nav">
<li class="nav-item dropdown" id="setting-trigger">
<a class="btn btn-outline-light bg-transparent nav-link px-2 text-white" href="#" id="dropdown-settings"
role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-cogs"></i>
</a>
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end" aria-labelledby="dropdown-settings">
<li><a href="#" id="trigger-nextcloud-settings"
class="modal-trigger dropdown-item">Nextcloud</a>
</li>
<li><a href="#" id="trigger-backup-settings" class="dropdown-item">Backup</a></li>
</ul>
</li>
</ul>
</div>
</nav>
<div class="container">
<div class="row mt-3 mb-3" id="header-box">
<div class="col-12 col-md-3 mb-3 mb-md-0">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h5 border-secondary border-bottom">Status</div>
<div class="card-body">
<h6 id="status" class="white-text"></h6>
<div id="status-second-line" class="truncate mb-2 text-accent"></div>
<div class="progress bg-secondary invisible" id="progress">
<div class="progress-bar bg-success" role="progressbar" style="width: 15%" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-3 mb-3 mb-md-0">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h5 border-secondary border-bottom">Last Backup</div>
<div class="card-body">
<h6 class="white-text" id="last_back_status"></h6>
</div>
</div>
</div>
<div class="col-12 col-md-3 mb-3 mb-md-0">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h5 border-secondary border-bottom">Next Backup</div>
<div class="card-body">
<h6 class="white-text" id="next_back_status"></h6>
</div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h5 border-secondary border-bottom">Manual</div>
<div class="card-body">
<div class="w-100">
<a class="btn btn-success d-block mb-2" id="btn-backup-now">
Backup Now
</a>
<a class="btn btn-danger d-block" id="btn-clean-now">
Clean Now
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-12 col-lg-6 mb-3 mb-lg-0">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h4 text-center border-secondary border-bottom">Local Snapshots</div>
<div class="card-body">
<div id="local_snaps"></div>
</div>
</div>
</div>
<div class="col-12 col-md-12 col-lg-6 ">
<div class="card text-white bg-dark h-100 shadow-sm border-secondary">
<div class="card-header fw-bold h4 text-center border-secondary border-bottom">Snapshots in Nextcloud
</div>
<div class="card-body">
<h5 class="card-title text-center fw-bold border-bottom border-secondary pb-2">Auto</h5>
<div id="auto_backups"></div>
<h5 class="card-title text-center fw-bold border-bottom border-secondary mt-3 pb-2">Manual</h5>
<div id="manual_backups"></div>
</div>
</div>
</div>
</div>
</div>
<%- include('modals/nextcloud-settings-modal') %>
<%- include('modals/backup-settings-modal') %>
<%- include('modals/loading-modal') %>
<div id="toast-container" class="toast-container" aria-live="polite" aria-atomic="true">
</div>
</body>
<script src="./js/bootstrap.min.js"></script>
<script src="./js/jquery.min.js"></script>
<script src="./js/index.js"></script>
<script src="./js/toast.js"></script>
<script>
</script>
</html>

View File

@ -1,71 +0,0 @@
<% if (locals.snaps) { %>
<div class="list-group">
<% for(const index in snaps) { %>
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
href="#"
data-id="<%= snaps[index].slug %>"
data-bs-toggle="modal"
data-bs-target="#modal-<%= snaps[index].slug %>">
<%= snaps[index].name ? snaps[index].name : snaps[index].slug %>
<span class="badge bg-primary">
<%= DateTime.fromISO(snaps[index].date).toFormat("MMM dd, yyyy HH:mm") %>
</span>
</a>
<div id="modal-<%= snaps[index].slug %>" class="modal fade">
<div class="modal-dialog modal-lg">
<div class="modal-content bg-dark">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="exampleModalLabel"> Snapshot Detail</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="name-<%= snaps[index].slug %>" class="form-label">Name</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="name-<%= snaps[index].slug %>"
value="<%= snaps[index].name ? snaps[index].name : snaps[index].slug %>"/>
</div>
<div class="mb-3">
<label for="date-<%= snaps[index].slug %>" class="form-label">Date</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="date-<%= snaps[index].slug %>"
value="<%= DateTime.fromISO(snaps[index].date).setLocale("en").toFormat("MMM dd, yyyy HH:mm") %>"/>
</div>
<div class="mb-3">
<label for="protected-<%= snaps[index].slug %>"
class="form-label">Protected</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="protected-<%= snaps[index].slug %>"
value="<%= snaps[index].protected %>"/>
</div>
<div >
<label for="type-<%= snaps[index].slug %>" class="form-label">Type</label>
<input disabled type="text" class="form-control bg-secondary border-dark text-accent" id="type-<%= snaps[index].slug %>"
value="<%= snaps[index].type %>"/>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger">Close</button>
<button class="btn btn-success manual-back-list"
data-id="<%= snaps[index].slug %>"
data-name='<%= snaps[index].name ? snaps[index].name : snaps[index].slug %>'
data-bs-dismiss="modal">
Backup now
</button>
</div>
</div>
</div>
</div>
<% } %>
</div>
<% } %>

View File

@ -1,217 +0,0 @@
<div id="modal-settings-backup" class="modal fade">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="exampleModalLabel">Backup Settings</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="name-template" class="form-label">Backup name</label>
<input id="name-template" type="text" class="form-control" aria-describedby="help-template">
<span id="help-template" class="form-text">
You can find all available variables
<a target="_blank"
href="https://github.com/Sebclem/hassio-nextcloud-backup/blob/master/nextcloud_backup/naming_template.md">
here
</a>
</span>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-6">
<div class="row">
<div class="col-12 text-center">
<h5>Folders</h5>
</div>
</div>
<div class="row">
<div class="col-12">
<ul id="folders-div" class="list-group">
</ul>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="row">
<div class="col-12 text-center">
<h5>Addons</h5>
</div>
</div>
<div class="row">
<div class="col-12">
<ul id="addons-div" class="list-group">
</ul>
</div>
</div>
</div>
</div>
<div class="row my-3">
<div class="col-12 text-center border-secondary border"></div>
</div>
<div class="row">
<div class="col-12 text-center">
<h4>Security</h4>
</div>
</div>
<div class="row mt-2">
<div class="col-lg-10 offset-lg-1 col-12">
<div class="form-check form-switch">
<input class="form-check-input" id="password_protected" type="checkbox">
<label class="form-check-label" for="password_protected">Password Protected</label>
</div>
</div>
</div>
<div class="row mt-2 d-none">
<div class="col-lg-10 offset-lg-1 col-12">
<label for="password_protect_value" class="form-label">Password</label>
<input type="password" class="form-control" id="password_protect_value" min="0">
</div>
</div>
<div class="row my-3">
<div class="col-12 text-center border-secondary border"></div>
</div>
<div class="row">
<div class="col-12 text-center">
<h4>Automation</h4>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="cron-drop-settings" class="form-label">Auto Backup</label>
<select id="cron-drop-settings" class="form-select">
<option value="0">Disable</option>
<option value="1">Daily</option>
<option value="2">Weekly</option>
<option value="3">Monthly</option>
<option value="4">Custom</option>
</select>
</div>
</div>
<div class="row d-none mt-2">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="timepicker" class="form-label">Hour </label>
<input type="time" class="form-control" id="timepicker">
</div>
</div>
<div class="row d-none mt-2">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="cron-drop-day" class="form-label">Day</label>
<select id="cron-drop-day" class="form-select">
<option value="1">Monday</option>
<option value="2">Tuesday</option>
<option value="3">Wednesday</option>
<option value="4">Thursday</option>
<option value="5">Friday</option>
<option value="6">Saturday</option>
<option value="0">Sunday</option>
</select>
</div>
</div>
<div class="row d-none mt-2">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="cron-drop-day-month" class="form-label">Day of month</label>
<input type="number" class="form-control" id="cron-drop-day-month" min="1" max="28">
</div>
</div>
<div class="row d-none mt-2">
<div class="col-12 col-lg-10 offset-lg-1">
<label for="cron-drop-custom" class="form-label">Custom Cron Pattern</label>
<input type="text" class="form-control" id="cron-drop-custom">
<span id="help-cron-custom"" class="form-text">
Only '*', ranges (1-3,5), steps (*/2) are allowed.
<a target="_blank"
href="https://crontab.guru/">
Generator
</a>
</span>
</div>
</div>
<div class="row my-3">
<div class="col-12 text-center border-secondary border"></div>
</div>
<div class="row mt-3">
<div class="col-12 text-center">
<h4>Auto Stop Addons</h4>
</div>
<div class="col-12 text-center">
<p><i>Auto stopped Addons before backup</i></p>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-10 offset-lg-1">
<ul id="auto-stop-addons-div" class="list-group">
</ul>
</div>
</div>
<div class="row my-3">
<div class="col-12 text-center border-secondary border"></div>
</div>
<div class="row">
<div class="col-12 text-center">
<h4>Auto Clean Settings</h4>
</div>
</div>
<div class="row my-3">
<div class="col-12 col-md-5 offset-md-1">
<div class="row">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" id="auto_clean_local" type="checkbox">
<label class="form-check-label" for="auto_clean_local">Auto Clean Local
Snapshots</label>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<label for="local-snap-keep" class="form-label">Local snapshot to keep</label>
<input type="number" class="form-control" id="local-snap-keep" min="0">
</div>
</div>
</div>
<div class="col-12 col-md-5">
<div class="row">
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" id="auto_clean_backup" type="checkbox">
<label class="form-check-label" for="auto_clean_backup">Auto Clean Nextcloud
Backups</label>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<label for="backup-snap-keep" class="form-label">Nextcloud Backup to keep</label>
<input type="number" class="form-control" id="backup-snap-keep" min="0">
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger"><b>Cancel</b></button>
<button class="btn btn-success" id="save-backup-settings"><b>Save</b></button>
</div>
</div>
</div>
</div>

View File

@ -1,17 +0,0 @@
<div class="modal" id="loading-modal" tabindex="-1"
aria-hidden="true">
<div class="modal-dialog border-secondary">
<div class="modal-content bg-dark text-white">
<div class="modal-header border-secondary d-flex justify-content-center">
<h3 class="modal-title fw-bold">Loading</h4>
</div>
<div class="modal-body text-center">
<div class="spinner-border text-accent border-5" style="width: 5rem; height: 5rem" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,73 +0,0 @@
<div id="modal-settings-nextcloud" class="modal fade">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="exampleModalLabel">Nextcloud Settings</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row d-none">
<div class="col-12 col-md-10 offset-md-1 text-center alert alert-danger" role="alert" id="nextcloud_settings_message">
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" id="ssl" type="checkbox">
<label class="form-check-label" for="ssl">SSL</label>
</div>
</div>
<div class="col-sm-12 col-md-8 invisible">
<div class="form-check form-switch">
<input class="form-check-input" id="self_signed" type="checkbox">
<label class="form-check-label" for="ssl">Accept Self-signed certificate</label>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<label for="hostname" class="form-label">Hostname</label>
<input id="hostname" type="text" class="form-control" aria-describedby="hostname-help">
<div id="hostname-help" class="form-text">exemple.com:8080</span>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-md-6">
<label for="username" class="form-label">Username</label>
<input id="username" type="text" class="form-control">
</div>
<div class="col-12 col-md-6">
<label for="password" class="form-label">Password</label>
<input id="password" type="password" class="form-control" aria-describedby="password-help">
<div id="password-help" class="form-text">
!!! Use App Password !!! See
<a target="_blank"
href="https://github.com/Sebclem/hassio-nextcloud-backup#nextcloud-config">
doc
</a>
for more information.
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<label for="back-dir" class="form-label">Backup Directory</label>
<input id="back-dir" type="text" class="form-control" aria-describedby="dir-help" placeholder="/Hassio Backup/">
<div id="dir-help" class="form-text">Default: /Hassio Backup/</div>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button data-bs-dismiss="modal" class="btn btn-danger"><b>Cancel</b></button>
<button class="btn btn-success" id="save-nextcloud-settings"><b>Save</b></button>
</div>
</div>
</div>
</div>

View File

@ -1,22 +0,0 @@
<div id="modal-restore" class="modal blue-grey darken-4 white-text">
<div class="modal-content">
<div class="row">
<div class="col s12 center">
<h4>Restore snapshot</h2>
</div>
</div>
<div class="row">
<div class="col s12 center divider">
</div>
</div>
<div class="row" style="margin-top: 40px;">
<h5 class="center">Are you sure you want to restore this snapshot ?</h5>
<h6 class="center">This will remplace all your data !</h6>
</div>
</div>
<div class="modal-footer blue-grey darken-4">
<a href="#!" id="confirm-restore" class="waves-effect waves-green btn green modal-close">Confirm</a>
<a href="#!" class="modal-close waves-effect waves-green btn red">Cancel</a>
</div>
</div>

File diff suppressed because it is too large Load Diff