mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-25 18:43:00 +01:00
🔨 Move backend to typescript + cleanup
This commit is contained in:
parent
ded4bc7315
commit
f91189999f
6
nextcloud_backup/backend/.eslintrc.cjs
Normal file
6
nextcloud_backup/backend/.eslintrc.cjs
Normal 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
1
nextcloud_backup/backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dist/**
|
55
nextcloud_backup/backend/package.json
Normal file
55
nextcloud_backup/backend/package.json
Normal 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"
|
||||
}
|
||||
}
|
2491
nextcloud_backup/backend/pnpm-lock.yaml
Normal file
2491
nextcloud_backup/backend/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -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();
|
279
nextcloud_backup/backend/src/routes/api.ts
Normal file
279
nextcloud_backup/backend/src/routes/api.ts
Normal 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 '"' at start and end ?
|
||||
for (const backup of contents) {
|
||||
backup.etag = backup.etag.replace(/"/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 '"' at start and end ?
|
||||
for (const backup of contents) {
|
||||
backup.etag = backup.etag.replace(/"/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;
|
25
nextcloud_backup/backend/src/server.ts
Executable file
25
nextcloud_backup/backend/src/server.ts
Executable 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;
|
149
nextcloud_backup/backend/src/tools/cronTools.ts
Normal file
149
nextcloud_backup/backend/src/tools/cronTools.ts
Normal 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;
|
545
nextcloud_backup/backend/src/tools/hassioApiTools.ts
Normal file
545
nextcloud_backup/backend/src/tools/hassioApiTools.ts
Normal 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,
|
||||
};
|
213
nextcloud_backup/backend/src/tools/settingsTools.ts
Normal file
213
nextcloud_backup/backend/src/tools/settingsTools.ts
Normal 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 };
|
@ -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"
|
@ -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) {
|
480
nextcloud_backup/backend/src/tools/webdavTools.ts
Normal file
480
nextcloud_backup/backend/src/tools/webdavTools.ts
Normal 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;
|
@ -0,0 +1,8 @@
|
||||
interface NewPartialBackupPayload {
|
||||
name?: string;
|
||||
password?: string;
|
||||
homeassistant?: boolean;
|
||||
addons?: string[];
|
||||
folders?: string[];
|
||||
compressed?: boolean;
|
||||
}
|
@ -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[];
|
||||
}
|
26
nextcloud_backup/backend/src/types/settings.ts
Normal file
26
nextcloud_backup/backend/src/types/settings.ts
Normal 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;
|
||||
}
|
10
nextcloud_backup/backend/src/types/status.ts
Normal file
10
nextcloud_backup/backend/src/types/status.ts
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
export interface Status {
|
||||
status: string;
|
||||
progress: number;
|
||||
last_backup?: string;
|
||||
next_backup?: string;
|
||||
message?: string;
|
||||
error_code?: number;
|
||||
|
||||
}
|
11
nextcloud_backup/backend/tsconfig.json
Normal file
11
nextcloud_backup/backend/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "es6",
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
@ -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
@ -1,3 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.2.cjs
|
@ -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);
|
||||
}
|
@ -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
@ -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;
|
||||
}
|
@ -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 |
@ -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();
|
||||
});
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 '"' at start and end ?
|
||||
for (let backup of contents) {
|
||||
backup.etag = backup.etag.replace(/"/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 '"' at start and end ?
|
||||
for (let backup of contents) {
|
||||
backup.etag = backup.etag.replace(/"/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;
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
@ -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 };
|
@ -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;
|
@ -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>
|
||||
|
||||
|
||||
<% } %>
|
@ -1,3 +0,0 @@
|
||||
<h1><%= message %></h1>
|
||||
<h2><%= error.status %></h2>
|
||||
<pre><%= error.stack %></pre>
|
@ -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>
|
@ -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>
|
||||
|
||||
|
||||
<% } %>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
Loading…
Reference in New Issue
Block a user