mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-05 09:02:58 +01:00
Compare commits
1 Commits
5187de661d
...
0521080064
Author | SHA1 | Date | |
---|---|---|---|
|
0521080064 |
@ -88,7 +88,7 @@ check [the contributor's page][contributors].
|
|||||||
|
|
||||||
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
|
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
|
||||||
[amd64-shield]: https://img.shields.io/badge/amd64-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
|
[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-shield]: https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-2.svg
|
||||||
[buymeacoffee]: https://www.buymeacoffee.com/seb6596
|
[buymeacoffee]: https://www.buymeacoffee.com/seb6596
|
||||||
@ -96,10 +96,10 @@ check [the contributor's page][contributors].
|
|||||||
[discord-ha]: https://discord.gg/c5DvZ4e
|
[discord-ha]: https://discord.gg/c5DvZ4e
|
||||||
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg
|
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg
|
||||||
[forum]: https://community.home-assistant.io/
|
[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
|
[issue]: https://github.com/Sebclem/hassio-nextcloud-backup/issues
|
||||||
[license-shield]: https://img.shields.io/github/license/Sebclem/hassio-nextcloud-backup.svg
|
[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
|
[project-stage-shield]: https://img.shields.io/badge/project%20stage-Beta-red.svg
|
||||||
[reddit]: https://reddit.com/r/homeassistant
|
[reddit]: https://reddit.com/r/homeassistant
|
||||||
[releases-shield]: https://img.shields.io/github/release/Sebclem/hassio-nextcloud-backup.svg?include_prereleases
|
[releases-shield]: https://img.shields.io/github/release/Sebclem/hassio-nextcloud-backup.svg?include_prereleases
|
||||||
|
@ -16,27 +16,27 @@
|
|||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron": "3.1.7",
|
"cron": "3.1.7",
|
||||||
"debug": "4.3.6",
|
"debug": "4.3.4",
|
||||||
"errorhandler": "^1.5.1",
|
"errorhandler": "^1.5.1",
|
||||||
"express": "4.19.2",
|
"express": "4.18.2",
|
||||||
"fast-xml-parser": "^4.4.1",
|
"fast-xml-parser": "^4.3.4",
|
||||||
"figlet": "^1.7.0",
|
"figlet": "^1.7.0",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "14.4.2",
|
"got": "14.2.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"joi": "^17.13.3",
|
"joi": "^17.12.1",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"kleur": "^4.1.5",
|
"kleur": "^4.1.5",
|
||||||
"luxon": "3.5.0",
|
"luxon": "3.4.4",
|
||||||
"morgan": "1.10.0",
|
"morgan": "1.10.0",
|
||||||
"webdav": "5.7.1",
|
"webdav": "5.3.2",
|
||||||
"winston": "3.14.1"
|
"winston": "3.11.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.7.0",
|
"packageManager": "pnpm@8.15.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.6.0",
|
||||||
"@tsconfig/recommended": "^1.0.7",
|
"@tsconfig/recommended": "^1.0.3",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.6",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/errorhandler": "^1.5.3",
|
"@types/errorhandler": "^1.5.3",
|
||||||
"@types/eslint__js": "^8.42.3",
|
"@types/eslint__js": "^8.42.3",
|
||||||
@ -45,13 +45,13 @@
|
|||||||
"@types/http-errors": "^2.0.4",
|
"@types/http-errors": "^2.0.4",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/node": "^22.3.0",
|
"@types/node": "^20.11.19",
|
||||||
"concurrently": "8.2.2",
|
"concurrently": "8.2.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.4",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.6.0",
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.0.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "8.1.0"
|
"typescript-eslint": "8.0.0-alpha.41"
|
||||||
}
|
}
|
||||||
}
|
}
|
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 { pipeline } from "stream/promises";
|
||||||
import { humanFileSize } from "../tools/toolbox.js";
|
import { humanFileSize } from "../tools/toolbox.js";
|
||||||
import type { BackupConfig } from "../types/services/backupConfig.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_SIZE = 5 * 1024 * 1024; // 5MiB Same as desktop client
|
||||||
const CHUNK_NUMBER_SIZE = 5; // To add landing "0"
|
const CHUNK_NUMBER_SIZE = 5; // To add landing "0"
|
||||||
@ -80,16 +79,16 @@ export function checkWebdavLogin(
|
|||||||
|
|
||||||
export async function createBackupFolder(conf: WebdavConfig) {
|
export async function createBackupFolder(conf: WebdavConfig) {
|
||||||
const root_splited = conf.backupDir.split("/").splice(1);
|
const root_splited = conf.backupDir.split("/").splice(1);
|
||||||
let thiPath = "/";
|
let path = "/";
|
||||||
for (const elem of root_splited) {
|
for (const elem of root_splited) {
|
||||||
if (elem != "") {
|
if (elem != "") {
|
||||||
thiPath = path.join(thiPath, elem);
|
path = path + elem + "/";
|
||||||
try {
|
try {
|
||||||
await createDirectory(thiPath, conf);
|
await createDirectory(path, conf);
|
||||||
logger.debug(`Path ${thiPath} created.`);
|
logger.debug(`Path ${path} created.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HTTPError && error.response.statusCode == 405)
|
if (error instanceof HTTPError && error.response.statusCode == 405)
|
||||||
logger.debug(`Path ${thiPath} already exist.`);
|
logger.debug(`Path ${path} already exist.`);
|
||||||
else {
|
else {
|
||||||
messageManager.error("Fail to create webdav root folder");
|
messageManager.error("Fail to create webdav root folder");
|
||||||
logger.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]) {
|
for (const elem of [pathTools.auto, pathTools.manual]) {
|
||||||
try {
|
try {
|
||||||
await createDirectory(path.join(conf.backupDir, elem), conf);
|
await createDirectory(conf.backupDir + elem, conf);
|
||||||
logger.debug(`Path ${path.join(conf.backupDir, elem)} created.`);
|
logger.debug(`Path ${conf.backupDir + elem} created.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HTTPError && error.response.statusCode == 405) {
|
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 {
|
} else {
|
||||||
messageManager.error("Fail to create webdav root folder");
|
messageManager.error("Fail to create webdav root folder");
|
||||||
logger.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);
|
statusTools.setStatus(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDirectory(pathToCreate: string, config: WebdavConfig) {
|
function createDirectory(path: string, config: WebdavConfig) {
|
||||||
const endpoint = getEndpoint(config);
|
const endpoint = getEndpoint(config);
|
||||||
return got(path.join(config.url, endpoint, pathToCreate), {
|
return got(config.url + endpoint + path, {
|
||||||
method: "MKCOL" as Method,
|
method: "MKCOL" as Method,
|
||||||
headers: {
|
headers: {
|
||||||
authorization:
|
authorization:
|
||||||
@ -150,7 +149,7 @@ export function getBackups(
|
|||||||
return Promise.reject(new Error("Not logged in"));
|
return Promise.reject(new Error("Not logged in"));
|
||||||
}
|
}
|
||||||
const endpoint = getEndpoint(config);
|
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,
|
method: "PROPFIND" as Method,
|
||||||
headers: {
|
headers: {
|
||||||
authorization:
|
authorization:
|
||||||
@ -202,11 +201,11 @@ function extractBackupInfo(backups: WebdavBackup[], template: string) {
|
|||||||
return backups;
|
return backups;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteBackup(pathToDelete: string, config: WebdavConfig) {
|
export function deleteBackup(path: string, config: WebdavConfig) {
|
||||||
logger.debug(`Deleting Cloud backup ${pathToDelete}`);
|
logger.debug(`Deleting Cloud backup ${path}`);
|
||||||
const endpoint = getEndpoint(config);
|
const endpoint = getEndpoint(config);
|
||||||
return got
|
return got
|
||||||
.delete(path.join(config.url, endpoint, pathToDelete), {
|
.delete(config.url + endpoint + path, {
|
||||||
headers: {
|
headers: {
|
||||||
authorization:
|
authorization:
|
||||||
"Basic " +
|
"Basic " +
|
||||||
@ -280,7 +279,7 @@ export function webdavUploadFile(
|
|||||||
},
|
},
|
||||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
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(`...URI: ${encodeURI(url)}`);
|
||||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||||
@ -344,12 +343,8 @@ export async function chunkedUpload(
|
|||||||
const fileSize = fs.statSync(localPath).size;
|
const fileSize = fs.statSync(localPath).size;
|
||||||
|
|
||||||
const chunkEndpoint = getChunkEndpoint(config);
|
const chunkEndpoint = getChunkEndpoint(config);
|
||||||
const chunkedUrl = path.join(config.url, chunkEndpoint, uuid);
|
const chunkedUrl = config.url + chunkEndpoint + uuid;
|
||||||
const finalDestination = path.join(
|
const finalDestination = config.url + getEndpoint(config) + webdavPath;
|
||||||
config.url,
|
|
||||||
getEndpoint(config),
|
|
||||||
webdavPath
|
|
||||||
);
|
|
||||||
const status = statusTools.getStatus();
|
const status = statusTools.getStatus();
|
||||||
status.status = States.BKUP_UPLOAD_CLOUD;
|
status.status = States.BKUP_UPLOAD_CLOUD;
|
||||||
status.progress = -1;
|
status.progress = -1;
|
||||||
@ -393,7 +388,7 @@ export async function chunkedUpload(
|
|||||||
try {
|
try {
|
||||||
const chunckNumber = i.toString().padStart(CHUNK_NUMBER_SIZE, "0");
|
const chunckNumber = i.toString().padStart(CHUNK_NUMBER_SIZE, "0");
|
||||||
await uploadChunk(
|
await uploadChunk(
|
||||||
path.join(chunkedUrl, chunckNumber),
|
chunkedUrl + `/${chunckNumber}`,
|
||||||
finalDestination,
|
finalDestination,
|
||||||
chunk,
|
chunk,
|
||||||
current_size,
|
current_size,
|
||||||
@ -532,7 +527,7 @@ function assembleChunkedUpload(
|
|||||||
totalLength: number,
|
totalLength: number,
|
||||||
config: WebdavConfig
|
config: WebdavConfig
|
||||||
) {
|
) {
|
||||||
const chunckFile = path.join(url, ".file");
|
const chunckFile = `${url}/.file`;
|
||||||
logger.info(`Assemble chuncked upload.`);
|
logger.info(`Assemble chuncked upload.`);
|
||||||
logger.debug(`...URI: ${encodeURI(chunckFile)}`);
|
logger.debug(`...URI: ${encodeURI(chunckFile)}`);
|
||||||
logger.debug(`...Final destination: ${encodeURI(finalDestination)}`);
|
logger.debug(`...Final destination: ${encodeURI(finalDestination)}`);
|
||||||
@ -568,7 +563,7 @@ export function downloadFile(
|
|||||||
},
|
},
|
||||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
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(`...URI: ${encodeURI(url)}`);
|
||||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||||
const status = statusTools.getStatus();
|
const status = statusTools.getStatus();
|
||||||
|
62
nextcloud_backup/frontend/auto-imports.d.ts
vendored
62
nextcloud_backup/frontend/auto-imports.d.ts
vendored
@ -133,3 +133,65 @@ declare module 'vue' {
|
|||||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
interface GlobalComponents {}
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
|
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||||
|
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||||
|
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||||
|
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||||
|
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||||
|
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||||
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||||
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
|
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||||
|
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||||
|
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||||
|
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||||
|
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||||
|
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||||
|
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||||
|
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||||
|
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||||
|
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||||
|
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||||
|
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||||
|
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||||
|
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||||
|
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||||
|
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||||
|
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||||
|
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||||
|
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||||
|
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||||
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
|
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||||
|
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||||
|
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||||
|
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||||
|
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||||
|
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||||
|
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||||
|
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||||
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||||
|
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||||
|
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||||
|
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||||
|
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||||
|
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||||
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
|
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||||
|
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||||
|
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||||
|
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,42 +11,42 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "7.4.47",
|
"@mdi/font": "7.4.47",
|
||||||
"core-js": "^3.38.0",
|
"core-js": "^3.34.0",
|
||||||
"ky": "^1.6.0",
|
"ky": "^1.2.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.4.4",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "*",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.3.0",
|
||||||
"vuetify": "3.5.16"
|
"vuetify": "3.5.16"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/types": "^7.25.2",
|
"@babel/types": "^7.23.0",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20.14.15",
|
"@types/node": "^20.10.0",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-standard": "^17.1.0",
|
"eslint-config-standard": "^17.1.0",
|
||||||
"eslint-config-vuetify": "^1.0.0",
|
"eslint-plugin-import": "^2.29.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-n": "^16.4.0",
|
||||||
"eslint-plugin-n": "^16.6.2",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^6.4.0",
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.19.0",
|
||||||
"pinia": "^2.2.1",
|
"pinia": "^2.1.0",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.69.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.3.0",
|
||||||
"unplugin-auto-import": "^0.17.8",
|
"unplugin-auto-import": "^0.17.3",
|
||||||
"unplugin-fonts": "^1.1.1",
|
"unplugin-fonts": "^1.1.0",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"unplugin-vue-router": "^0.7.0",
|
"unplugin-vue-router": "^0.7.0",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.0.0",
|
||||||
"vite-plugin-vue-layouts": "^0.10.0",
|
"vite-plugin-vue-layouts": "^0.10.0",
|
||||||
"vite-plugin-vuetify": "^2.0.4",
|
"vite-plugin-vuetify": "^2.0.0",
|
||||||
"vue-router": "^4.4.3",
|
"vue-router": "^4.2.0",
|
||||||
"vue-tsc": "^2.0.26"
|
"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
@ -94,7 +94,7 @@ import { useMessageStore } from "@/store/message";
|
|||||||
import { MessageType } from "@/types/messages";
|
import { MessageType } from "@/types/messages";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { onBeforeUnmount } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
|
|
||||||
const messagesStore = useMessageStore();
|
const messagesStore = useMessageStore();
|
||||||
const { messages } = storeToRefs(messagesStore);
|
const { messages } = storeToRefs(messagesStore);
|
||||||
@ -162,6 +162,7 @@ function getTimeDelta(time: string) {
|
|||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const show = ref<boolean[]>([]);
|
||||||
refreshMessages();
|
refreshMessages();
|
||||||
|
|
||||||
function markReaded(id: string) {
|
function markReaded(id: string) {
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
import type { WebdavBackup } from "@/types/webdav";
|
import type { WebdavBackup } from "@/types/webdav";
|
||||||
import {
|
import {
|
||||||
getAutoBackupList,
|
getAutoBackupList,
|
||||||
|
@ -113,13 +113,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { restoreWebdavBackup } from "@/services/webdavService";
|
||||||
import type { WebdavBackup } from "@/types/webdav";
|
import type { WebdavBackup } from "@/types/webdav";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const detail = ref(false);
|
const detail = ref(false);
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
item: WebdavBackup;
|
item: WebdavBackup;
|
||||||
index: number;
|
index: number;
|
||||||
}>();
|
}>();
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMenuSize } from "@/composable/menuSize";
|
import { useMenuSize } from "@/composable/menuSize";
|
||||||
import { deleteHomeAssistantBackup } from "@/services/homeAssistantService";
|
import { deleteHomeAssistantBackup } from "@/services/homeAssistantService";
|
||||||
|
import { deleteWebdabBackup } from "@/services/webdavService";
|
||||||
import { useAlertStore } from "@/store/alert";
|
import { useAlertStore } from "@/store/alert";
|
||||||
import { BackupModel } from "@/types/homeAssistant";
|
import { BackupModel } from "@/types/homeAssistant";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { BackupModel } from "@/types/homeAssistant";
|
import type { BackupModel } from "@/types/homeAssistant";
|
||||||
import { ref } from "vue";
|
import { ref, onBeforeUnmount } from "vue";
|
||||||
import {
|
import {
|
||||||
getBackups,
|
getBackups,
|
||||||
uploadHomeAssistantBackup,
|
uploadHomeAssistantBackup,
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
}>();
|
}>();
|
||||||
|
@ -15,7 +15,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getStatus } from "@/services/statusService";
|
import { getStatus } from "@/services/statusService";
|
||||||
import { States, Status } from "@/types/status";
|
import { States, Status } from "@/types/status";
|
||||||
import { ref, onBeforeUnmount } from "vue";
|
import { computed, ref, onBeforeUnmount } from "vue";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
import ConnectionStatus from "./ConnectionStatus.vue";
|
import ConnectionStatus from "./ConnectionStatus.vue";
|
||||||
import BackupStatus from "./BackupStatus.vue";
|
import BackupStatus from "./BackupStatus.vue";
|
||||||
import ActionComponent from "./ActionComponent.vue";
|
import ActionComponent from "./ActionComponent.vue";
|
||||||
|
@ -25,7 +25,7 @@ export function useConfigForm(
|
|||||||
})
|
})
|
||||||
.catch(async (reason) => {
|
.catch(async (reason) => {
|
||||||
if (reason instanceof HTTPError) {
|
if (reason instanceof HTTPError) {
|
||||||
const response: any = await reason.response.json();
|
const response = await reason.response.json();
|
||||||
if (response["type"] == "validation") {
|
if (response["type"] == "validation") {
|
||||||
for (const elem of response["errors"]) {
|
for (const elem of response["errors"]) {
|
||||||
errorsRef.value[
|
errorsRef.value[
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify/lib/framework.mjs";
|
||||||
|
|
||||||
export function useMenuSize() {
|
export function useMenuSize() {
|
||||||
const { xs, mdAndDown } = useDisplay();
|
const { xs, mdAndDown } = useDisplay();
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { WebdavBackup } from "@/types/webdav";
|
||||||
import kyClient from "./kyClient";
|
import kyClient from "./kyClient";
|
||||||
import { Status } from "@/types/status";
|
import { Status } from "@/types/status";
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { DateTime } from "luxon";
|
||||||
|
|
||||||
export enum States {
|
export enum States {
|
||||||
IDLE = "IDLE",
|
IDLE = "IDLE",
|
||||||
|
@ -1,33 +1,25 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
|
||||||
"jsx": "preserve",
|
|
||||||
"lib": ["DOM", "ESNext"],
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "Node",
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
},
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"types": [
|
|
||||||
"vite/client",
|
|
||||||
"vite-plugin-vue-layouts/client",
|
|
||||||
"unplugin-vue-router/client"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": true,
|
"jsx": "preserve",
|
||||||
"noUnusedLocals": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"skipLibCheck": true
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
"src/**/*",
|
|
||||||
"src/**/*.vue"
|
|
||||||
],
|
|
||||||
"exclude": ["dist", "node_modules", "cypress"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }],
|
"references": [{ "path": "./tsconfig.node.json" }],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
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.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"
|
||||||
|
}
|
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>
|
1554
nextcloud_backup/rootfs/opt/nextcloud_backup/yarn.lock
Normal file
1554
nextcloud_backup/rootfs/opt/nextcloud_backup/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "hassio-nextcloud-backup",
|
"name": "hassio-nextcloud-backup",
|
||||||
"packageManager": "pnpm@9.7.0",
|
"packageManager": "pnpm@7.13.4",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
"release-it": "17.6.0"
|
"release-it": "15.2.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"release": "release-it"
|
"release": "release-it"
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"matchStrings": [
|
"matchStrings": [
|
||||||
"(?<currentValue>.*)\\s"
|
"(?<currentValue>.*)\\s"
|
||||||
],
|
],
|
||||||
"depNameTemplate": "ghcr.io/home-assistant/amd64-base",
|
"depNameTemplate": "ghcr.io/hassio-addons/base/amd64",
|
||||||
"datasourceTemplate": "docker",
|
"datasourceTemplate": "docker",
|
||||||
"versioningTemplate": "semver"
|
"versioningTemplate": "semver"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user