mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-04 16:42:58 +01:00
Compare commits
21 Commits
1e29781f07
...
844c030de7
Author | SHA1 | Date | |
---|---|---|---|
|
844c030de7 | ||
|
83e167a0aa | ||
ab2d61efe6 | |||
901d0d52ed | |||
8ab6046c9f | |||
0b5d5babc4 | |||
bc568577f3 | |||
b1b0ea908f | |||
c8f2cd7761 | |||
d6c9cf337d | |||
d38dcc628e | |||
741cdd9b6a | |||
2556ea1719 | |||
6803843b6b | |||
|
fac8a67b72 | ||
|
fa55746e4a | ||
cebd6fe0a6 | |||
b91d68dee5 | |||
696d8e694d | |||
|
c787868ff2 | ||
|
932716a7e8 |
@ -16,27 +16,27 @@
|
||||
"cookie-parser": "1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "3.1.7",
|
||||
"debug": "4.3.4",
|
||||
"debug": "4.3.6",
|
||||
"errorhandler": "^1.5.1",
|
||||
"express": "4.18.2",
|
||||
"fast-xml-parser": "^4.3.4",
|
||||
"express": "4.19.2",
|
||||
"fast-xml-parser": "^4.4.1",
|
||||
"figlet": "^1.7.0",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.2.0",
|
||||
"got": "14.4.2",
|
||||
"http-errors": "2.0.0",
|
||||
"joi": "^17.12.1",
|
||||
"joi": "^17.13.3",
|
||||
"jquery": "3.7.1",
|
||||
"kleur": "^4.1.5",
|
||||
"luxon": "3.4.4",
|
||||
"luxon": "3.5.0",
|
||||
"morgan": "1.10.0",
|
||||
"webdav": "5.3.2",
|
||||
"winston": "3.11.0"
|
||||
"webdav": "5.7.1",
|
||||
"winston": "3.14.1"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.3",
|
||||
"packageManager": "pnpm@9.7.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@tsconfig/recommended": "^1.0.3",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@tsconfig/recommended": "^1.0.7",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/errorhandler": "^1.5.3",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
@ -45,13 +45,13 @@
|
||||
"@types/http-errors": "^2.0.4",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/node": "^22.3.0",
|
||||
"concurrently": "8.2.2",
|
||||
"dotenv": "^16.4.4",
|
||||
"eslint": "^9.6.0",
|
||||
"nodemon": "^3.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^9.9.0",
|
||||
"nodemon": "^3.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "8.0.0-alpha.41"
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "8.1.0"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,7 @@ 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"
|
||||
@ -79,16 +80,16 @@ export function checkWebdavLogin(
|
||||
|
||||
export async function createBackupFolder(conf: WebdavConfig) {
|
||||
const root_splited = conf.backupDir.split("/").splice(1);
|
||||
let path = "/";
|
||||
let thiPath = "/";
|
||||
for (const elem of root_splited) {
|
||||
if (elem != "") {
|
||||
path = path + elem + "/";
|
||||
thiPath = path.join(thiPath, elem);
|
||||
try {
|
||||
await createDirectory(path, conf);
|
||||
logger.debug(`Path ${path} created.`);
|
||||
await createDirectory(thiPath, conf);
|
||||
logger.debug(`Path ${thiPath} created.`);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError && error.response.statusCode == 405)
|
||||
logger.debug(`Path ${path} already exist.`);
|
||||
logger.debug(`Path ${thiPath} already exist.`);
|
||||
else {
|
||||
messageManager.error("Fail to create webdav root folder");
|
||||
logger.error("Fail to create webdav root folder");
|
||||
@ -104,11 +105,11 @@ export async function createBackupFolder(conf: WebdavConfig) {
|
||||
}
|
||||
for (const elem of [pathTools.auto, pathTools.manual]) {
|
||||
try {
|
||||
await createDirectory(conf.backupDir + elem, conf);
|
||||
logger.debug(`Path ${conf.backupDir + elem} created.`);
|
||||
await createDirectory(path.join(conf.backupDir, elem), conf);
|
||||
logger.debug(`Path ${path.join(conf.backupDir, elem)} created.`);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError && error.response.statusCode == 405) {
|
||||
logger.debug(`Path ${conf.backupDir + elem} already exist.`);
|
||||
logger.debug(`Path ${path.join(conf.backupDir, elem)} already exist.`);
|
||||
} else {
|
||||
messageManager.error("Fail to create webdav root folder");
|
||||
logger.error("Fail to create webdav root folder");
|
||||
@ -127,9 +128,9 @@ export async function createBackupFolder(conf: WebdavConfig) {
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
|
||||
function createDirectory(path: string, config: WebdavConfig) {
|
||||
function createDirectory(pathToCreate: string, config: WebdavConfig) {
|
||||
const endpoint = getEndpoint(config);
|
||||
return got(config.url + endpoint + path, {
|
||||
return got(path.join(config.url, endpoint, pathToCreate), {
|
||||
method: "MKCOL" as Method,
|
||||
headers: {
|
||||
authorization:
|
||||
@ -149,7 +150,7 @@ export function getBackups(
|
||||
return Promise.reject(new Error("Not logged in"));
|
||||
}
|
||||
const endpoint = getEndpoint(config);
|
||||
return got(config.url + endpoint + config.backupDir + folder, {
|
||||
return got(path.join(config.url, endpoint, config.backupDir, folder), {
|
||||
method: "PROPFIND" as Method,
|
||||
headers: {
|
||||
authorization:
|
||||
@ -201,11 +202,11 @@ function extractBackupInfo(backups: WebdavBackup[], template: string) {
|
||||
return backups;
|
||||
}
|
||||
|
||||
export function deleteBackup(path: string, config: WebdavConfig) {
|
||||
logger.debug(`Deleting Cloud backup ${path}`);
|
||||
export function deleteBackup(pathToDelete: string, config: WebdavConfig) {
|
||||
logger.debug(`Deleting Cloud backup ${pathToDelete}`);
|
||||
const endpoint = getEndpoint(config);
|
||||
return got
|
||||
.delete(config.url + endpoint + path, {
|
||||
.delete(path.join(config.url, endpoint, pathToDelete), {
|
||||
headers: {
|
||||
authorization:
|
||||
"Basic " +
|
||||
@ -279,7 +280,7 @@ export function webdavUploadFile(
|
||||
},
|
||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
||||
};
|
||||
const url = config.url + getEndpoint(config) + webdavPath;
|
||||
const url = path.join(config.url, getEndpoint(config), webdavPath);
|
||||
|
||||
logger.debug(`...URI: ${encodeURI(url)}`);
|
||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||
@ -343,8 +344,12 @@ export async function chunkedUpload(
|
||||
const fileSize = fs.statSync(localPath).size;
|
||||
|
||||
const chunkEndpoint = getChunkEndpoint(config);
|
||||
const chunkedUrl = config.url + chunkEndpoint + uuid;
|
||||
const finalDestination = config.url + getEndpoint(config) + webdavPath;
|
||||
const chunkedUrl = path.join(config.url, chunkEndpoint, uuid);
|
||||
const finalDestination = path.join(
|
||||
config.url,
|
||||
getEndpoint(config),
|
||||
webdavPath
|
||||
);
|
||||
const status = statusTools.getStatus();
|
||||
status.status = States.BKUP_UPLOAD_CLOUD;
|
||||
status.progress = -1;
|
||||
@ -388,7 +393,7 @@ export async function chunkedUpload(
|
||||
try {
|
||||
const chunckNumber = i.toString().padStart(CHUNK_NUMBER_SIZE, "0");
|
||||
await uploadChunk(
|
||||
chunkedUrl + `/${chunckNumber}`,
|
||||
path.join(chunkedUrl, chunckNumber),
|
||||
finalDestination,
|
||||
chunk,
|
||||
current_size,
|
||||
@ -527,7 +532,7 @@ function assembleChunkedUpload(
|
||||
totalLength: number,
|
||||
config: WebdavConfig
|
||||
) {
|
||||
const chunckFile = `${url}/.file`;
|
||||
const chunckFile = path.join(url, ".file");
|
||||
logger.info(`Assemble chuncked upload.`);
|
||||
logger.debug(`...URI: ${encodeURI(chunckFile)}`);
|
||||
logger.debug(`...Final destination: ${encodeURI(finalDestination)}`);
|
||||
@ -563,7 +568,7 @@ export function downloadFile(
|
||||
},
|
||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
||||
};
|
||||
const url = config.url + getEndpoint(config) + webdavPath;
|
||||
const url = path.join(config.url, getEndpoint(config), webdavPath);
|
||||
logger.debug(`...URI: ${encodeURI(url)}`);
|
||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||
const status = statusTools.getStatus();
|
||||
|
62
nextcloud_backup/frontend/auto-imports.d.ts
vendored
62
nextcloud_backup/frontend/auto-imports.d.ts
vendored
@ -133,65 +133,3 @@ declare module 'vue' {
|
||||
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": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"core-js": "^3.34.0",
|
||||
"ky": "^1.2.0",
|
||||
"luxon": "^3.4.4",
|
||||
"core-js": "^3.38.0",
|
||||
"ky": "^1.6.0",
|
||||
"luxon": "^3.5.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"roboto-fontface": "*",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.3.0",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.4.37",
|
||||
"vuetify": "3.5.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.23.0",
|
||||
"@babel/types": "^7.25.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@types/node": "^20.14.15",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-n": "^16.4.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.19.0",
|
||||
"pinia": "^2.1.0",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.3.0",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"unplugin-fonts": "^1.1.0",
|
||||
"eslint-config-vuetify": "^1.0.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-promise": "^6.4.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"pinia": "^2.2.1",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.17.8",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"unplugin-vue-router": "^0.7.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-vue-layouts": "^0.10.0",
|
||||
"vite-plugin-vuetify": "^2.0.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
"vite-plugin-vuetify": "^2.0.4",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue-tsc": "^2.0.26"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.3"
|
||||
"packageManager": "pnpm@9.7.0"
|
||||
}
|
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 { DateTime } from "luxon";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { onBeforeUnmount, ref } from "vue";
|
||||
import { onBeforeUnmount } from "vue";
|
||||
|
||||
const messagesStore = useMessageStore();
|
||||
const { messages } = storeToRefs(messagesStore);
|
||||
@ -162,7 +162,6 @@ function getTimeDelta(time: string) {
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
const show = ref<boolean[]>([]);
|
||||
refreshMessages();
|
||||
|
||||
function markReaded(id: string) {
|
||||
|
@ -86,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import type { WebdavBackup } from "@/types/webdav";
|
||||
import {
|
||||
getAutoBackupList,
|
||||
|
@ -113,14 +113,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { restoreWebdavBackup } from "@/services/webdavService";
|
||||
import type { WebdavBackup } from "@/types/webdav";
|
||||
import { DateTime } from "luxon";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { ref } from "vue";
|
||||
|
||||
const detail = ref(false);
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
item: WebdavBackup;
|
||||
index: number;
|
||||
}>();
|
||||
|
@ -33,7 +33,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMenuSize } from "@/composable/menuSize";
|
||||
import { deleteHomeAssistantBackup } from "@/services/homeAssistantService";
|
||||
import { deleteWebdabBackup } from "@/services/webdavService";
|
||||
import { useAlertStore } from "@/store/alert";
|
||||
import { BackupModel } from "@/types/homeAssistant";
|
||||
import { ref } from "vue";
|
||||
|
@ -57,7 +57,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { BackupModel } from "@/types/homeAssistant";
|
||||
import { ref, onBeforeUnmount } from "vue";
|
||||
import { ref } from "vue";
|
||||
import {
|
||||
getBackups,
|
||||
uploadHomeAssistantBackup,
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
name: string;
|
||||
version?: string;
|
||||
}>();
|
||||
|
@ -15,8 +15,7 @@
|
||||
<script setup lang="ts">
|
||||
import { getStatus } from "@/services/statusService";
|
||||
import { States, Status } from "@/types/status";
|
||||
import { computed, ref, onBeforeUnmount } from "vue";
|
||||
import { DateTime } from "luxon";
|
||||
import { ref, onBeforeUnmount } from "vue";
|
||||
import ConnectionStatus from "./ConnectionStatus.vue";
|
||||
import BackupStatus from "./BackupStatus.vue";
|
||||
import ActionComponent from "./ActionComponent.vue";
|
||||
|
@ -25,7 +25,7 @@ export function useConfigForm(
|
||||
})
|
||||
.catch(async (reason) => {
|
||||
if (reason instanceof HTTPError) {
|
||||
const response = await reason.response.json();
|
||||
const response: any = await reason.response.json();
|
||||
if (response["type"] == "validation") {
|
||||
for (const elem of response["errors"]) {
|
||||
errorsRef.value[
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { computed } from "vue";
|
||||
import { useDisplay } from "vuetify/lib/framework.mjs";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
export function useMenuSize() {
|
||||
const { xs, mdAndDown } = useDisplay();
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { WebdavBackup } from "@/types/webdav";
|
||||
import kyClient from "./kyClient";
|
||||
import { Status } from "@/types/status";
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { DateTime } from "luxon";
|
||||
|
||||
export enum States {
|
||||
IDLE = "IDLE",
|
||||
|
@ -1,25 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"vite-plugin-vue-layouts/client",
|
||||
"unplugin-vue-router/client"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": ["dist", "node_modules", "cypress"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "nexcloud-backup",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node ./bin/www.js "
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.1.2",
|
||||
"app-root-path": "3.0.0",
|
||||
"bootstrap": "5.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"
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
import express from 'express';
|
||||
import * as statusTools from "../tools/status.js"
|
||||
import webdav from "../tools/webdavTools.js"
|
||||
import * as settingsTools from "../tools/settingsTools.js"
|
||||
import * as pathTools from "../tools/pathTools.js"
|
||||
import * as hassioApiTools from "../tools/hassioApiTools.js"
|
||||
import { humanFileSize } from "../tools/toolbox.js";
|
||||
import cronTools from "../tools/cronTools.js"
|
||||
import logger from "../config/winston.js"
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
var router = express.Router();
|
||||
|
||||
router.get("/status", (req, res, next) => {
|
||||
cronTools.updateNextDate();
|
||||
let status = statusTools.getStatus();
|
||||
res.json(status);
|
||||
});
|
||||
|
||||
router.get("/formated-local-snap", function (req, res, next) {
|
||||
hassioApiTools.getSnapshots()
|
||||
.then((snaps) => {
|
||||
snaps.sort((a, b) => {
|
||||
return 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;
|
||||
|
@ -1,536 +0,0 @@
|
||||
import fs from "fs"
|
||||
|
||||
import stream from "stream"
|
||||
import { promisify } from "util";
|
||||
import got from "got";
|
||||
import FormData from "form-data";
|
||||
import * as statusTools from "../tools/status.js"
|
||||
import * as settingsTools from "../tools/settingsTools.js"
|
||||
|
||||
import logger from "../config/winston.js"
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
|
||||
const token = process.env.SUPERVISOR_TOKEN;
|
||||
|
||||
// Default timeout to 90min
|
||||
const create_snap_timeout = parseInt(process.env.CREATE_BACKUP_TIMEOUT) || (90 * 60 * 1000);
|
||||
|
||||
|
||||
function getVersion() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let status = statusTools.getStatus();
|
||||
let option = {
|
||||
headers: { "authorization": `Bearer ${token}` },
|
||||
responseType: "json",
|
||||
};
|
||||
|
||||
got("http://hassio/core/info", option)
|
||||
.then((result) => {
|
||||
if (status.error_code === 1) {
|
||||
status.status = "idle";
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
let version = result.body.data.version;
|
||||
resolve(version);
|
||||
})
|
||||
.catch((error) => {
|
||||
statusTools.setError(`Fail to fetch HA Version (${error.message})`, 1);
|
||||
reject(`Fail to fetch HA Version (${error.message})`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let status = statusTools.getStatus();
|
||||
let option = {
|
||||
headers: { "authorization": `Bearer ${token}` },
|
||||
responseType: "json",
|
||||
};
|
||||
|
||||
got("http://hassio/addons", option)
|
||||
.then((result) => {
|
||||
if (status.error_code === 1) {
|
||||
status.status = "idle";
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
let addons = result.body.data.addons;
|
||||
addons.sort((a, b) => {
|
||||
let textA = a.name.toUpperCase();
|
||||
let textB = b.name.toUpperCase();
|
||||
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
|
||||
});
|
||||
resolve(addons);
|
||||
})
|
||||
.catch((error) => {
|
||||
statusTools.setError(`Fail to fetch addons list (${error.message})`, 1);
|
||||
reject(`Fail to fetch addons list (${error.message})`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonToBackup() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let excluded_addon = settingsTools.getSettings().exclude_addon;
|
||||
getAddonList()
|
||||
.then((all_addon) => {
|
||||
let slugs = [];
|
||||
for (let addon of all_addon) {
|
||||
if (!excluded_addon.includes(addon.slug))
|
||||
slugs.push(addon.slug)
|
||||
}
|
||||
logger.debug("Addon to backup:")
|
||||
logger.debug(slugs)
|
||||
resolve(slugs)
|
||||
})
|
||||
.catch(() => reject());
|
||||
});
|
||||
}
|
||||
|
||||
function getFolderList() {
|
||||
return [
|
||||
{
|
||||
name: "Home Assistant configuration",
|
||||
slug: "homeassistant"
|
||||
},
|
||||
{
|
||||
name: "SSL",
|
||||
slug: "ssl"
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
slug: "share"
|
||||
},
|
||||
{
|
||||
name: "Media",
|
||||
slug: "media"
|
||||
},
|
||||
{
|
||||
name: "Local add-ons",
|
||||
slug: "addons/local"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function getFolderToBackup() {
|
||||
let excluded_folder = settingsTools.getSettings().exclude_folder;
|
||||
let all_folder = getFolderList()
|
||||
let slugs = [];
|
||||
for (let folder of all_folder) {
|
||||
if (!excluded_folder.includes(folder.slug))
|
||||
slugs.push(folder.slug)
|
||||
}
|
||||
logger.debug("Folders to backup:");
|
||||
logger.debug(slugs)
|
||||
return slugs;
|
||||
}
|
||||
|
||||
function getSnapshots() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let status = statusTools.getStatus();
|
||||
let option = {
|
||||
headers: { "authorization": `Bearer ${token}` },
|
||||
responseType: "json",
|
||||
};
|
||||
|
||||
got("http://hassio/backups", option)
|
||||
.then((result) => {
|
||||
if (status.error_code === 1) {
|
||||
status.status = "idle";
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
let snaps = result.body.data.backups;
|
||||
resolve(snaps);
|
||||
})
|
||||
.catch((error) => {
|
||||
statusTools.setError(`Fail to fetch Hassio backups (${error.message})`, 1);
|
||||
reject(`Fail to fetch Hassio backups (${error.message})`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function downloadSnapshot(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info(`Downloading snapshot ${id}...`);
|
||||
if (!fs.existsSync("./temp/")) fs.mkdirSync("./temp/");
|
||||
let tmp_file = `./temp/${id}.tar`;
|
||||
let stream = fs.createWriteStream(tmp_file);
|
||||
let status = statusTools.getStatus();
|
||||
checkSnap(id)
|
||||
.then(() => {
|
||||
status.status = "download";
|
||||
status.progress = 0;
|
||||
statusTools.setStatus(status);
|
||||
let option = {
|
||||
headers: { "Authorization": `Bearer ${token}` },
|
||||
};
|
||||
|
||||
pipeline(
|
||||
got.stream.get(`http://hassio/backups/${id}/download`, option)
|
||||
.on("downloadProgress", (e) => {
|
||||
let percent = Math.round(e.percent * 100) / 100;
|
||||
if (status.progress !== percent) {
|
||||
status.progress = percent;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
}),
|
||||
stream
|
||||
)
|
||||
.then(() => {
|
||||
logger.info("Download success !");
|
||||
status.progress = 1;
|
||||
statusTools.setStatus(status);
|
||||
logger.debug("Snapshot dl size : " + fs.statSync(tmp_file).size / 1024 / 1024);
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
fs.unlinkSync(tmp_file);
|
||||
statusTools.setError(`Fail to download Hassio backup (${error.message})`, 7);
|
||||
reject(`Fail to download Hassio backup (${error.message})`);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
statusTools.setError("Fail to download Hassio backup. Not found ?", 7);
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function dellSnap(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
checkSnap(id)
|
||||
.then(() => {
|
||||
let option = {
|
||||
headers: { "authorization": `Bearer ${token}` },
|
||||
responseType: "json",
|
||||
};
|
||||
|
||||
got.delete(`http://hassio/backups/${id}`, option)
|
||||
.then(() => resolve())
|
||||
.catch((e) => {
|
||||
logger.error(e)
|
||||
reject();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkSnap(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let option = {
|
||||
headers: { "authorization": `Bearer ${token}` },
|
||||
responseType: "json",
|
||||
};
|
||||
|
||||
got(`http://hassio/backups/${id}/info`, option)
|
||||
.then((result) => {
|
||||
logger.debug(`Snapshot size: ${result.body.data.size}`);
|
||||
resolve();
|
||||
})
|
||||
.catch(() => reject());
|
||||
});
|
||||
}
|
||||
|
||||
function createNewBackup(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let status = statusTools.getStatus();
|
||||
status.status = "creating";
|
||||
status.progress = -1;
|
||||
statusTools.setStatus(status);
|
||||
logger.info("Creating new snapshot...");
|
||||
getAddonToBackup().then((addons) => {
|
||||
let folders = getFolderToBackup();
|
||||
let option = {
|
||||
headers: { "authorization": `Bearer ${token}` },
|
||||
responseType: "json",
|
||||
timeout: {
|
||||
response: create_snap_timeout
|
||||
},
|
||||
json: {
|
||||
name: name,
|
||||
addons: addons,
|
||||
folders: folders
|
||||
},
|
||||
};
|
||||
let password_protected = settingsTools.getSettings().password_protected;
|
||||
logger.debug(`Is password protected ? ${password_protected}`)
|
||||
if ( password_protected === "true") {
|
||||
option.json.password = settingsTools.getSettings().password_protect_value
|
||||
}
|
||||
|
||||
got.post(`http://hassio/backups/new/partial`, option)
|
||||
.then((result) => {
|
||||
logger.info(`Snapshot created with id ${result.body.data.slug}`);
|
||||
resolve(result.body.data.slug);
|
||||
})
|
||||
.catch((error) => {
|
||||
statusTools.setError(`Can't create new snapshot (${error.message})`, 5);
|
||||
reject(`Can't create new snapshot (${error.message})`);
|
||||
});
|
||||
|
||||
}).catch(reject);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function clean() {
|
||||
let limit = settingsTools.getSettings().auto_clean_local_keep;
|
||||
if (limit == null) limit = 5;
|
||||
return new Promise((resolve, reject) => {
|
||||
getSnapshots()
|
||||
.then(async (snaps) => {
|
||||
if (snaps.length < limit) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
snaps.sort((a, b) => {
|
||||
return 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
|
||||
}
|
@ -1,427 +0,0 @@
|
||||
import { createClient } from "webdav";
|
||||
import fs from "fs"
|
||||
import https from "https"
|
||||
import path from "path";
|
||||
import got from "got";
|
||||
import stream from "stream";
|
||||
import { promisify } from "util";
|
||||
|
||||
import * as statusTools from "../tools/status.js"
|
||||
import * as settingsTools from "../tools/settingsTools.js"
|
||||
import * as pathTools from "../tools/pathTools.js"
|
||||
import * as hassioApiTools from "../tools/hassioApiTools.js"
|
||||
import logger from "../config/winston.js"
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
const endpoint = "/remote.php/webdav";
|
||||
const configPath = "/data/webdav_conf.json";
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
|
||||
class WebdavTools {
|
||||
constructor() {
|
||||
this.host = null;
|
||||
this.client = null;
|
||||
this.baseUrl = null;
|
||||
this.username = null;
|
||||
this.password = null;
|
||||
}
|
||||
|
||||
init(ssl, host, username, password, accept_selfsigned_cert) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.host = host;
|
||||
let status = statusTools.getStatus();
|
||||
logger.info("Initializing and checking webdav client...");
|
||||
this.baseUrl = (ssl === "true" ? "https" : "http") + "://" + host + endpoint;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
let agent_option = ssl === "true" ? { rejectUnauthorized: accept_selfsigned_cert === "false" } : {};
|
||||
try {
|
||||
this.client = createClient(this.baseUrl, {
|
||||
username: username,
|
||||
password: password,
|
||||
httpsAgent: new https.Agent(agent_option)
|
||||
});
|
||||
|
||||
this.client
|
||||
.getDirectoryContents("/")
|
||||
.then(() => {
|
||||
if (status.error_code === 3) {
|
||||
status.status = "idle";
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
logger.debug("Nextcloud connection: \x1b[32mSuccess !\x1b[0m");
|
||||
this.initFolder().then(() => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.__cant_connect_status(error);
|
||||
this.client = null;
|
||||
reject("Can't connect to Nextcloud (" + error + ") !");
|
||||
});
|
||||
} catch (err) {
|
||||
this.__cant_connect_status(err);
|
||||
this.client = null;
|
||||
reject("Can't connect to Nextcloud (" + err + ") !");
|
||||
}
|
||||
});
|
||||
}
|
||||
__cant_connect_status(err){
|
||||
statusTools.setError(`Can't connect to Nextcloud (${err})`, 3);
|
||||
}
|
||||
|
||||
async __createRoot() {
|
||||
let root_splited = this.getConf().back_dir.split("/").splice(1);
|
||||
let path = "/";
|
||||
for (let elem of root_splited) {
|
||||
if (elem !== "") {
|
||||
path = path + elem + "/";
|
||||
try {
|
||||
await this.client.createDirectory(path);
|
||||
logger.debug(`Path ${path} created.`);
|
||||
} catch (error) {
|
||||
if (error.status === 405) logger.debug(`Path ${path} already exist.`);
|
||||
else logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initFolder() {
|
||||
return new Promise((resolve) => {
|
||||
this.__createRoot().catch((err) => {
|
||||
logger.error(err);
|
||||
}).then(() => {
|
||||
this.client.createDirectory(this.getConf().back_dir + pathTools.auto)
|
||||
.catch(() => {
|
||||
})
|
||||
.then(() => {
|
||||
this.client
|
||||
.createDirectory(this.getConf().back_dir + pathTools.manual)
|
||||
.catch(() => {
|
||||
})
|
||||
.then(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if theh webdav config is valid, if yes, start init of webdav client
|
||||
*/
|
||||
confIsValid() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let status = statusTools.getStatus();
|
||||
let conf = this.getConf();
|
||||
if (conf !== null) {
|
||||
if (conf.ssl !== null && conf.host !== null && conf.username !== null && conf.password !== null) {
|
||||
if (status.error_code === 2) {
|
||||
status.status = "idle";
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
// Check if self_signed option exist
|
||||
if (conf.self_signed == null || conf.self_signed === "") {
|
||||
conf.self_signed = "false";
|
||||
this.setConf(conf);
|
||||
}
|
||||
this.init(conf.ssl, conf.host, conf.username, conf.password, conf.self_signed)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
status.status = "error";
|
||||
status.error_code = 2;
|
||||
status.message = "Nextcloud config invalid !";
|
||||
statusTools.setStatus(status);
|
||||
logger.error(status.message);
|
||||
reject("Nextcloud config invalid !");
|
||||
}
|
||||
|
||||
if (conf.back_dir == null || conf.back_dir === "") {
|
||||
logger.info("Backup dir is null, initializing it.");
|
||||
conf.back_dir = pathTools.default_root;
|
||||
this.setConf(conf);
|
||||
} else {
|
||||
if (!conf.back_dir.startsWith("/")) {
|
||||
logger.warn("Backup dir not starting with '/', fixing this...");
|
||||
conf.back_dir = `/${conf.back_dir}`;
|
||||
this.setConf(conf);
|
||||
}
|
||||
if (!conf.back_dir.endsWith("/")) {
|
||||
logger.warn("Backup dir not ending with '/', fixing this...");
|
||||
conf.back_dir = `${conf.back_dir}/`;
|
||||
this.setConf(conf);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
status.status = "error";
|
||||
status.error_code = 2;
|
||||
status.message = "Nextcloud config not found !";
|
||||
statusTools.setStatus(status);
|
||||
logger.error(status.message);
|
||||
reject("Nextcloud config not found !");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getConf() {
|
||||
if (fs.existsSync(configPath)) {
|
||||
return JSON.parse(fs.readFileSync(configPath).toString());
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
setConf(conf) {
|
||||
fs.writeFileSync(configPath, JSON.stringify(conf));
|
||||
}
|
||||
|
||||
uploadFile(id, path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.client == null) {
|
||||
this.confIsValid()
|
||||
.then(() => {
|
||||
this._startUpload(id, path)
|
||||
.then(()=> resolve())
|
||||
.catch((err) => reject(err));
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else
|
||||
this._startUpload(id, path)
|
||||
.then(()=> resolve())
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
_startUpload(id, path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let status = statusTools.getStatus();
|
||||
status.status = "upload";
|
||||
status.progress = 0;
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
statusTools.setStatus(status);
|
||||
logger.info("Uploading snap...");
|
||||
let tmpFile = `./temp/${id}.tar`;
|
||||
let stats = fs.statSync(tmpFile);
|
||||
let stream = fs.createReadStream(tmpFile);
|
||||
let conf = this.getConf();
|
||||
let options = {
|
||||
body: stream,
|
||||
// username: this.username,
|
||||
// password: encodeURIComponent(this.password),
|
||||
headers: {
|
||||
'authorization': 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64'),
|
||||
'content-length': String(stats.size)
|
||||
}
|
||||
};
|
||||
if (conf.ssl === "true") {
|
||||
options["https"] = { rejectUnauthorized: conf.self_signed === "false" };
|
||||
}
|
||||
logger.debug(`...URI: ${encodeURI(this.baseUrl.replace(this.host, 'host.hiden') + path)}`);
|
||||
if (conf.ssl === "true")
|
||||
logger.debug(`...rejectUnauthorized: ${options["https"]["rejectUnauthorized"]}`);
|
||||
|
||||
got.stream
|
||||
.put(encodeURI(this.baseUrl + path), options)
|
||||
.on("uploadProgress", (e) => {
|
||||
let percent = e.percent;
|
||||
if (status.progress !== percent) {
|
||||
status.progress = percent;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
if (percent >= 1) {
|
||||
logger.info("Upload done...");
|
||||
}
|
||||
})
|
||||
.on("response", (res) => {
|
||||
if (res.statusCode !== 201 && res.statusCode !== 204) {
|
||||
status.status = "error";
|
||||
status.error_code = 4;
|
||||
status.message = `Fail to upload snapshot to nextcloud (Status code: ${res.statusCode})!`;
|
||||
statusTools.setStatus(status);
|
||||
logger.error(status.message);
|
||||
fs.unlinkSync(tmpFile);
|
||||
reject(status.message);
|
||||
} else {
|
||||
logger.info(`...Upload finish ! (status: ${res.statusCode})`);
|
||||
status.status = "idle";
|
||||
status.progress = -1;
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
status.last_backup = DateTime.now().toFormat("dd MMM yyyy, HH:mm")
|
||||
statusTools.setStatus(status);
|
||||
cleanTempFolder();
|
||||
let autoCleanCloud = settingsTools.getSettings().auto_clean_backup;
|
||||
if (autoCleanCloud != null && autoCleanCloud === "true") {
|
||||
this.clean().catch();
|
||||
}
|
||||
let autoCleanlocal = settingsTools.getSettings().auto_clean_local;
|
||||
if (autoCleanlocal != null && autoCleanlocal === "true") {
|
||||
hassioApiTools.clean().catch();
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.on("error", (err) => {
|
||||
fs.unlinkSync(tmpFile);
|
||||
status.status = "error";
|
||||
status.error_code = 4;
|
||||
status.message = `Fail to upload snapshot to nextcloud (${err}) !`;
|
||||
statusTools.setStatus(status);
|
||||
logger.error(status.message);
|
||||
logger.error(err.stack);
|
||||
reject(status.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
downloadFile(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.client == null) {
|
||||
this.confIsValid()
|
||||
.then(() => {
|
||||
this._startDownload(path)
|
||||
.then((path) => resolve(path))
|
||||
.catch(() => reject());
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else
|
||||
this._startDownload(path)
|
||||
.then((path) => resolve(path))
|
||||
.catch(() => reject());
|
||||
});
|
||||
}
|
||||
|
||||
_startDownload(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let status = statusTools.getStatus();
|
||||
status.status = "download-b";
|
||||
status.progress = 0;
|
||||
status.message = null;
|
||||
status.error_code = null;
|
||||
statusTools.setStatus(status);
|
||||
|
||||
logger.info("Downloading backup...");
|
||||
if (!fs.existsSync("./temp/"))
|
||||
fs.mkdirSync("./temp/");
|
||||
let tmpFile = `./temp/restore_${DateTime.now().toFormat("MMM-dd-yyyy_HH_mm")}.tar`;
|
||||
let stream = fs.createWriteStream(tmpFile);
|
||||
let conf = this.getConf();
|
||||
let options = {
|
||||
headers: {
|
||||
'authorization': 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64')
|
||||
}
|
||||
};
|
||||
if (conf.ssl === "true") {
|
||||
options["https"] = { rejectUnauthorized: conf.self_signed === "false" };
|
||||
}
|
||||
logger.debug(`...URI: ${encodeURI(this.baseUrl.replace(this.host, 'host.hiden') + path)}`);
|
||||
if (conf.ssl === "true")
|
||||
logger.debug(`...rejectUnauthorized: ${options["https"]["rejectUnauthorized"]}`);
|
||||
pipeline(
|
||||
got.stream.get(encodeURI(this.baseUrl + path), options)
|
||||
.on("downloadProgress", (e) => {
|
||||
let percent = Math.round(e.percent * 100) / 100;
|
||||
if (status.progress !== percent) {
|
||||
status.progress = percent;
|
||||
statusTools.setStatus(status);
|
||||
}
|
||||
}),
|
||||
stream
|
||||
).then((res) => {
|
||||
logger.info("Download success !");
|
||||
status.progress = 1;
|
||||
statusTools.setStatus(status);
|
||||
logger.debug("Backup dl size : " + fs.statSync(tmpFile).size / 1024 / 1024);
|
||||
resolve(tmpFile);
|
||||
}).catch((err) => {
|
||||
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
|
||||
status.status = "error";
|
||||
status.message = "Fail to download Hassio snapshot (" + err.message + ")";
|
||||
status.error_code = 7;
|
||||
statusTools.setStatus(status);
|
||||
logger.error(status.message);
|
||||
logger.error(err.stack);
|
||||
reject(err.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getFolderContent(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.client == null) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
this.client
|
||||
.getDirectoryContents(path)
|
||||
.then((contents) => resolve(contents))
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
clean() {
|
||||
let limit = settingsTools.getSettings().auto_clean_backup_keep;
|
||||
if (limit == null) limit = 5;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.getFolderContent(this.getConf().back_dir + pathTools.auto)
|
||||
.then(async (contents) => {
|
||||
|
||||
if (contents.length < limit) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
contents.sort((a, b) => {
|
||||
return 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;
|
@ -1,73 +0,0 @@
|
||||
<div id="modal-settings-nextcloud" class="modal fade">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Nextcloud Settings</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row d-none">
|
||||
<div class="col-12 col-md-10 offset-md-1 text-center alert alert-danger" role="alert" id="nextcloud_settings_message">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" id="ssl" type="checkbox">
|
||||
<label class="form-check-label" for="ssl">SSL</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-8 invisible">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" id="self_signed" type="checkbox">
|
||||
<label class="form-check-label" for="ssl">Accept Self-signed certificate</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label for="hostname" class="form-label">Hostname</label>
|
||||
<input id="hostname" type="text" class="form-control" aria-describedby="hostname-help">
|
||||
<div id="hostname-help" class="form-text">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>
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "hassio-nextcloud-backup",
|
||||
"packageManager": "pnpm@7.13.4",
|
||||
"packageManager": "pnpm@9.7.0",
|
||||
"devDependencies": {
|
||||
"auto-changelog": "2.4.0",
|
||||
"release-it": "15.2.0"
|
||||
"release-it": "17.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"release": "release-it"
|
||||
|
@ -12,7 +12,7 @@
|
||||
"matchStrings": [
|
||||
"(?<currentValue>.*)\\s"
|
||||
],
|
||||
"depNameTemplate": "ghcr.io/hassio-addons/base/amd64",
|
||||
"depNameTemplate": "ghcr.io/home-assistant/amd64-base",
|
||||
"datasourceTemplate": "docker",
|
||||
"versioningTemplate": "semver"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user