mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-13 21:12:59 +01:00
Compare commits
1 Commits
fa55746e4a
...
cb3380dd1d
Author | SHA1 | Date | |
---|---|---|---|
|
cb3380dd1d |
@ -88,7 +88,7 @@ check [the contributor's page][contributors].
|
||||
|
||||
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
|
||||
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
|
||||
[armhf-shield]: https://img.shields.io/badge/armhf-no-red.svg
|
||||
[armhf-shield]: https://img.shields.io/badge/armhf-yes-green.svg
|
||||
[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg
|
||||
[buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-2.svg
|
||||
[buymeacoffee]: https://www.buymeacoffee.com/seb6596
|
||||
@ -96,10 +96,10 @@ check [the contributor's page][contributors].
|
||||
[discord-ha]: https://discord.gg/c5DvZ4e
|
||||
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg
|
||||
[forum]: https://community.home-assistant.io/
|
||||
[i386-shield]: https://img.shields.io/badge/i386-no-red.svg
|
||||
[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg
|
||||
[issue]: https://github.com/Sebclem/hassio-nextcloud-backup/issues
|
||||
[license-shield]: https://img.shields.io/github/license/Sebclem/hassio-nextcloud-backup.svg
|
||||
[maintenance-shield]: https://img.shields.io/maintenance/yes/2024.svg
|
||||
[maintenance-shield]: https://img.shields.io/maintenance/yes/2022.svg
|
||||
[project-stage-shield]: https://img.shields.io/badge/project%20stage-Beta-red.svg
|
||||
[reddit]: https://reddit.com/r/homeassistant
|
||||
[releases-shield]: https://img.shields.io/github/release/Sebclem/hassio-nextcloud-backup.svg?include_prereleases
|
||||
|
@ -32,7 +32,7 @@
|
||||
"webdav": "5.3.2",
|
||||
"winston": "3.11.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.0",
|
||||
"packageManager": "pnpm@8.15.3",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@tsconfig/recommended": "^1.0.3",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -24,7 +24,6 @@ import { getChunkEndpoint, getEndpoint } from "./webdavConfigService.js";
|
||||
import { pipeline } from "stream/promises";
|
||||
import { humanFileSize } from "../tools/toolbox.js";
|
||||
import type { BackupConfig } from "../types/services/backupConfig.js";
|
||||
import path from "path";
|
||||
|
||||
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MiB Same as desktop client
|
||||
const CHUNK_NUMBER_SIZE = 5; // To add landing "0"
|
||||
@ -80,16 +79,16 @@ export function checkWebdavLogin(
|
||||
|
||||
export async function createBackupFolder(conf: WebdavConfig) {
|
||||
const root_splited = conf.backupDir.split("/").splice(1);
|
||||
let thiPath = "/";
|
||||
let path = "/";
|
||||
for (const elem of root_splited) {
|
||||
if (elem != "") {
|
||||
thiPath = path.join(thiPath, elem);
|
||||
path = path + elem + "/";
|
||||
try {
|
||||
await createDirectory(thiPath, conf);
|
||||
logger.debug(`Path ${thiPath} created.`);
|
||||
await createDirectory(path, conf);
|
||||
logger.debug(`Path ${path} created.`);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError && error.response.statusCode == 405)
|
||||
logger.debug(`Path ${thiPath} already exist.`);
|
||||
logger.debug(`Path ${path} already exist.`);
|
||||
else {
|
||||
messageManager.error("Fail to create webdav root folder");
|
||||
logger.error("Fail to create webdav root folder");
|
||||
@ -105,11 +104,11 @@ export async function createBackupFolder(conf: WebdavConfig) {
|
||||
}
|
||||
for (const elem of [pathTools.auto, pathTools.manual]) {
|
||||
try {
|
||||
await createDirectory(path.join(conf.backupDir, elem), conf);
|
||||
logger.debug(`Path ${path.join(conf.backupDir, elem)} created.`);
|
||||
await createDirectory(conf.backupDir + elem, conf);
|
||||
logger.debug(`Path ${conf.backupDir + elem} created.`);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError && error.response.statusCode == 405) {
|
||||
logger.debug(`Path ${path.join(conf.backupDir, elem)} already exist.`);
|
||||
logger.debug(`Path ${conf.backupDir + elem} already exist.`);
|
||||
} else {
|
||||
messageManager.error("Fail to create webdav root folder");
|
||||
logger.error("Fail to create webdav root folder");
|
||||
@ -128,9 +127,9 @@ export async function createBackupFolder(conf: WebdavConfig) {
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
|
||||
function createDirectory(pathToCreate: string, config: WebdavConfig) {
|
||||
function createDirectory(path: string, config: WebdavConfig) {
|
||||
const endpoint = getEndpoint(config);
|
||||
return got(path.join(config.url, endpoint, pathToCreate), {
|
||||
return got(config.url + endpoint + path, {
|
||||
method: "MKCOL" as Method,
|
||||
headers: {
|
||||
authorization:
|
||||
@ -150,7 +149,7 @@ export function getBackups(
|
||||
return Promise.reject(new Error("Not logged in"));
|
||||
}
|
||||
const endpoint = getEndpoint(config);
|
||||
return got(path.join(config.url, endpoint, config.backupDir, folder), {
|
||||
return got(config.url + endpoint + config.backupDir + folder, {
|
||||
method: "PROPFIND" as Method,
|
||||
headers: {
|
||||
authorization:
|
||||
@ -202,11 +201,11 @@ function extractBackupInfo(backups: WebdavBackup[], template: string) {
|
||||
return backups;
|
||||
}
|
||||
|
||||
export function deleteBackup(pathToDelete: string, config: WebdavConfig) {
|
||||
logger.debug(`Deleting Cloud backup ${pathToDelete}`);
|
||||
export function deleteBackup(path: string, config: WebdavConfig) {
|
||||
logger.debug(`Deleting Cloud backup ${path}`);
|
||||
const endpoint = getEndpoint(config);
|
||||
return got
|
||||
.delete(path.join(config.url, endpoint, pathToDelete), {
|
||||
.delete(config.url + endpoint + path, {
|
||||
headers: {
|
||||
authorization:
|
||||
"Basic " +
|
||||
@ -280,7 +279,7 @@ export function webdavUploadFile(
|
||||
},
|
||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
||||
};
|
||||
const url = path.join(config.url, getEndpoint(config), webdavPath);
|
||||
const url = config.url + getEndpoint(config) + webdavPath;
|
||||
|
||||
logger.debug(`...URI: ${encodeURI(url)}`);
|
||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||
@ -344,12 +343,8 @@ export async function chunkedUpload(
|
||||
const fileSize = fs.statSync(localPath).size;
|
||||
|
||||
const chunkEndpoint = getChunkEndpoint(config);
|
||||
const chunkedUrl = path.join(config.url, chunkEndpoint, uuid);
|
||||
const finalDestination = path.join(
|
||||
config.url,
|
||||
getEndpoint(config),
|
||||
webdavPath
|
||||
);
|
||||
const chunkedUrl = config.url + chunkEndpoint + uuid;
|
||||
const finalDestination = config.url + getEndpoint(config) + webdavPath;
|
||||
const status = statusTools.getStatus();
|
||||
status.status = States.BKUP_UPLOAD_CLOUD;
|
||||
status.progress = -1;
|
||||
@ -393,7 +388,7 @@ export async function chunkedUpload(
|
||||
try {
|
||||
const chunckNumber = i.toString().padStart(CHUNK_NUMBER_SIZE, "0");
|
||||
await uploadChunk(
|
||||
path.join(chunkedUrl, chunckNumber),
|
||||
chunkedUrl + `/${chunckNumber}`,
|
||||
finalDestination,
|
||||
chunk,
|
||||
current_size,
|
||||
@ -532,7 +527,7 @@ function assembleChunkedUpload(
|
||||
totalLength: number,
|
||||
config: WebdavConfig
|
||||
) {
|
||||
const chunckFile = path.join(url, ".file");
|
||||
const chunckFile = `${url}/.file`;
|
||||
logger.info(`Assemble chuncked upload.`);
|
||||
logger.debug(`...URI: ${encodeURI(chunckFile)}`);
|
||||
logger.debug(`...Final destination: ${encodeURI(finalDestination)}`);
|
||||
@ -568,7 +563,7 @@ export function downloadFile(
|
||||
},
|
||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
||||
};
|
||||
const url = path.join(config.url, getEndpoint(config), webdavPath);
|
||||
const url = config.url + getEndpoint(config) + webdavPath;
|
||||
logger.debug(`...URI: ${encodeURI(url)}`);
|
||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||
const status = statusTools.getStatus();
|
||||
|
@ -48,5 +48,5 @@
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.0"
|
||||
"packageManager": "pnpm@8.15.3"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
28
nextcloud_backup/rootfs/opt/nextcloud_backup/package.json
Normal file
28
nextcloud_backup/rootfs/opt/nextcloud_backup/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"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.1.3",
|
||||
"cookie-parser": "1.4.6",
|
||||
"cron": "2.1.0",
|
||||
"debug": "4.3.4",
|
||||
"ejs": "3.1.8",
|
||||
"express": "4.19.2",
|
||||
"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"
|
||||
}
|
229
nextcloud_backup/rootfs/opt/nextcloud_backup/routes/api.js
Normal file
229
nextcloud_backup/rootfs/opt/nextcloud_backup/routes/api.js
Normal file
@ -0,0 +1,229 @@
|
||||
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 Date.parse(b.date) - Date.parse(a.date);
|
||||
});
|
||||
|
||||
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 Date.parse(b.lastmod) - Date.parse(a.lastmod)
|
||||
});
|
||||
//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 Date.parse(b.lastmod) - Date.parse(a.lastmod)
|
||||
});
|
||||
//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;
|
||||
|
@ -0,0 +1,536 @@
|
||||
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 Date.parse(b.date) - Date.parse(a.date);
|
||||
});
|
||||
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
|
||||
}
|
@ -0,0 +1,427 @@
|
||||
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 Date.parse(b.lastmod) - Date.parse(a.lastmod)
|
||||
});
|
||||
|
||||
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;
|
@ -0,0 +1,73 @@
|
||||
<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">example.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>
|
1561
nextcloud_backup/rootfs/opt/nextcloud_backup/yarn.lock
Normal file
1561
nextcloud_backup/rootfs/opt/nextcloud_backup/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hassio-nextcloud-backup",
|
||||
"packageManager": "pnpm@9.7.0",
|
||||
"packageManager": "pnpm@7.13.4",
|
||||
"devDependencies": {
|
||||
"auto-changelog": "2.4.0",
|
||||
"release-it": "15.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user