mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-12-23 22:46:44 +01:00
Add orchestrator, manual backup is functional 🎉
This commit is contained in:
parent
564cebc560
commit
84f0afb6d8
18
nextcloud_backup/backend/src/routes/action.ts
Normal file
18
nextcloud_backup/backend/src/routes/action.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import express from "express";
|
||||
import { doBackupWorkflow } from "../services/orchestrator.js";
|
||||
import { WorkflowType } from "../types/services/orchecstrator.js";
|
||||
import logger from "../config/winston.js";
|
||||
|
||||
const actionRouter = express.Router();
|
||||
|
||||
actionRouter.post("/backup", (req, res) => {
|
||||
doBackupWorkflow(WorkflowType.MANUAL)
|
||||
.then(() => {
|
||||
logger.info("All good !");
|
||||
})
|
||||
.catch((reason) => {
|
||||
logger.error("Something wrong !");
|
||||
});
|
||||
});
|
||||
|
||||
export default actionRouter;
|
@ -1,17 +1,18 @@
|
||||
import express from "express"
|
||||
import express from "express";
|
||||
import configRouter from "./config.js";
|
||||
import homeAssistant from "./homeAssistant.js"
|
||||
import homeAssistant from "./homeAssistant.js";
|
||||
import messageRouter from "./messages.js";
|
||||
import webdavRouter from "./webdav.js";
|
||||
import statusRouter from "./status.js";
|
||||
|
||||
import actionRouter from "./action.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use("/homeAssistant", homeAssistant)
|
||||
router.use("/homeAssistant", homeAssistant);
|
||||
router.use("/config", configRouter);
|
||||
router.use("/webdav", webdavRouter);
|
||||
router.use("/messages", messageRouter);
|
||||
router.use('/status', statusRouter);
|
||||
router.use("/status", statusRouter);
|
||||
router.use("/action", actionRouter);
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
@ -1,8 +1,13 @@
|
||||
import fs from "fs";
|
||||
import Joi from "joi";
|
||||
import logger from "../config/winston.js";
|
||||
import { type BackupConfig, BackupType } from "../types/services/backupConfig.js";
|
||||
import {
|
||||
type BackupConfig,
|
||||
BackupType,
|
||||
} from "../types/services/backupConfig.js";
|
||||
import backupConfigValidation from "../types/services/backupConfigValidation.js";
|
||||
import { DateTime } from "luxon";
|
||||
import { WorkflowType } from "../types/services/orchecstrator.js";
|
||||
|
||||
const backupConfigPath = "/data/backupConfigV2.json";
|
||||
|
||||
@ -52,10 +57,32 @@ export function templateToRegexp(template: string) {
|
||||
let regexp = template.replace("{date}", "(?<date>\\d{4}-\\d{2}-\\d{2})");
|
||||
regexp = regexp.replace("{hour}", "(?<hour>\\d{4})");
|
||||
regexp = regexp.replace("{hour_12}", "(?<hour12>\\d{4}(AM|PM))");
|
||||
regexp = regexp.replace("{type}", "(?<type>Auto|Manual|)")
|
||||
regexp = regexp.replace("{type_low}", "(?<type>auto|manual|)")
|
||||
regexp = regexp.replace("{type}", "(?<type>Auto|Manual|)");
|
||||
regexp = regexp.replace("{type_low}", "(?<type>auto|manual|)");
|
||||
return regexp.replace(
|
||||
"{ha_version}",
|
||||
"(?<version>\\d+\\.\\d+\\.\\d+(b\\d+)?)"
|
||||
);
|
||||
}
|
||||
|
||||
export function getFormatedName(
|
||||
workflowType: WorkflowType,
|
||||
ha_version: string
|
||||
) {
|
||||
const setting = getBackupConfig();
|
||||
let template = setting.nameTemplate;
|
||||
template = template.replace(
|
||||
"{type_low}",
|
||||
workflowType == WorkflowType.MANUAL ? "manual" : "auto"
|
||||
);
|
||||
template = template.replace(
|
||||
"{type}",
|
||||
workflowType == WorkflowType.MANUAL ? "Manual" : "Auto"
|
||||
);
|
||||
template = template.replace("{ha_version}", ha_version);
|
||||
const now = DateTime.now().setLocale("en");
|
||||
template = template.replace("{hour_12}", now.toFormat("hhmma"));
|
||||
template = template.replace("{hour}", now.toFormat("HHmm"));
|
||||
template = template.replace("{date}", now.toFormat("yyyy-MM-dd"));
|
||||
return template;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import logger from "../config/winston.js";
|
||||
import messageManager from "../tools/messageManager.js";
|
||||
import * as settingsTools from "../tools/settingsTools.js";
|
||||
import * as statusTools from "../tools/status.js";
|
||||
import type { NewPartialBackupPayload } from "../types/services/ha_os_payload.js";
|
||||
import type { NewBackupPayload } from "../types/services/ha_os_payload.js";
|
||||
import type {
|
||||
AddonData,
|
||||
AddonModel,
|
||||
@ -25,6 +25,7 @@ import type {
|
||||
} from "../types/services/ha_os_response.js";
|
||||
import { States, type Status } from "../types/status.js";
|
||||
import { DateTime } from "luxon";
|
||||
import { BackupType } from "../types/services/backupConfig.js";
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
|
||||
@ -81,29 +82,6 @@ function getAddonList(): Promise<Response<SupervisorResponse<AddonData>>> {
|
||||
);
|
||||
}
|
||||
|
||||
function getAddonToBackup(addons: AddonModel[]) {
|
||||
const excluded_addon = settingsTools.getSettings().exclude_addon;
|
||||
const slugs: string[] = [];
|
||||
for (const addon of addons) {
|
||||
if (!excluded_addon.includes(addon.slug)) slugs.push(addon.slug);
|
||||
}
|
||||
logger.debug("Addon to backup:");
|
||||
logger.debug(slugs);
|
||||
return slugs;
|
||||
}
|
||||
|
||||
function getFolderToBackup() {
|
||||
const excluded_folder = settingsTools.getSettings().exclude_folder;
|
||||
const all_folder = getFolderList();
|
||||
const slugs = [];
|
||||
for (const folder of all_folder) {
|
||||
if (!excluded_folder.includes(folder.slug)) slugs.push(folder.slug);
|
||||
}
|
||||
logger.debug("Folders to backup:");
|
||||
logger.debug(slugs);
|
||||
return slugs;
|
||||
}
|
||||
|
||||
function getBackups(): Promise<Response<SupervisorResponse<BackupData>>> {
|
||||
const option: OptionsOfJSONResponseBody = {
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
@ -232,25 +210,24 @@ function getBackupInfo(id: string) {
|
||||
|
||||
function createNewBackup(
|
||||
name: string,
|
||||
addonSlugs: string[],
|
||||
folders: string[]
|
||||
type: BackupType,
|
||||
passwordEnable: boolean,
|
||||
password?: string,
|
||||
addonSlugs?: string[],
|
||||
folders?: string[]
|
||||
) {
|
||||
const status = statusTools.getStatus();
|
||||
status.status = States.BKUP_CREATION;
|
||||
status.progress = -1;
|
||||
statusTools.setStatus(status);
|
||||
logger.info("Creating new snapshot...");
|
||||
const body: NewPartialBackupPayload = {
|
||||
const body: NewBackupPayload = {
|
||||
name: name,
|
||||
addons: addonSlugs,
|
||||
folders: folders,
|
||||
password: passwordEnable ? password : undefined,
|
||||
addons: type == BackupType.PARTIAL ? addonSlugs : undefined,
|
||||
folders: type == BackupType.PARTIAL ? folders : undefined,
|
||||
};
|
||||
|
||||
const password_protected = settingsTools.getSettings().password_protected;
|
||||
logger.debug(`Is password protected ? ${password_protected}`);
|
||||
if (password_protected === "true") {
|
||||
body.password = settingsTools.getSettings().password_protect_value;
|
||||
}
|
||||
const option: OptionsOfJSONResponseBody = {
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
responseType: "json",
|
||||
@ -259,31 +236,31 @@ function createNewBackup(
|
||||
},
|
||||
json: body,
|
||||
};
|
||||
return got
|
||||
.post<SupervisorResponse<{ slug: string }>>(
|
||||
`http://hassio/backups/new/partial`,
|
||||
option
|
||||
)
|
||||
.then(
|
||||
(result) => {
|
||||
logger.info(`Snapshot created with id ${result.body.data.slug}`);
|
||||
const status = statusTools.getStatus();
|
||||
status.status = States.IDLE;
|
||||
status.progress = undefined;
|
||||
statusTools.setStatus(status);
|
||||
return result;
|
||||
},
|
||||
(reason) => {
|
||||
messageManager.error("Fail to create new backup.", reason.message);
|
||||
logger.error("Fail to create new backup");
|
||||
logger.error(reason);
|
||||
const status = statusTools.getStatus();
|
||||
status.status = States.IDLE;
|
||||
status.progress = undefined;
|
||||
statusTools.setStatus(status);
|
||||
return Promise.reject(reason);
|
||||
}
|
||||
);
|
||||
|
||||
const url =
|
||||
type == BackupType.PARTIAL
|
||||
? "http://hassio/backups/new/partial"
|
||||
: "http://hassio/backups/new/full";
|
||||
return got.post<SupervisorResponse<{ slug: string }>>(url, option).then(
|
||||
(result) => {
|
||||
logger.info(`Snapshot created with id ${result.body.data.slug}`);
|
||||
const status = statusTools.getStatus();
|
||||
status.status = States.IDLE;
|
||||
status.progress = undefined;
|
||||
statusTools.setStatus(status);
|
||||
return result;
|
||||
},
|
||||
(reason) => {
|
||||
messageManager.error("Fail to create new backup.", reason.message);
|
||||
logger.error("Fail to create new backup");
|
||||
logger.error(reason);
|
||||
const status = statusTools.getStatus();
|
||||
status.status = States.IDLE;
|
||||
status.progress = undefined;
|
||||
statusTools.setStatus(status);
|
||||
return Promise.reject(reason);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function clean(backups: BackupModel[]) {
|
||||
|
133
nextcloud_backup/backend/src/services/orchestrator.ts
Normal file
133
nextcloud_backup/backend/src/services/orchestrator.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import type { AddonModel } from "../types/services/ha_os_response.js";
|
||||
import { WorkflowType } from "../types/services/orchecstrator.js";
|
||||
import * as backupConfigService from "./backupConfigService.js";
|
||||
import * as homeAssistantService from "./homeAssistantService.js";
|
||||
import { getBackupFolder, getWebdavConfig } from "./webdavConfigService.js";
|
||||
import * as webDavService from "./webdavService.js";
|
||||
import * as statusTools from "../tools/status.js";
|
||||
import { stat, unlinkSync } from "fs";
|
||||
import logger from "../config/winston.js";
|
||||
import { BackupType } from "../types/services/backupConfig.js";
|
||||
import { DateTime } from "luxon";
|
||||
import messageManager from "../tools/messageManager.js";
|
||||
|
||||
export function doBackupWorkflow(type: WorkflowType) {
|
||||
let name = "";
|
||||
let addonsToStartStop = [] as string[];
|
||||
let addonInHa = [] as AddonModel[];
|
||||
let tmpBackupFile = "";
|
||||
|
||||
const backupConfig = backupConfigService.getBackupConfig();
|
||||
const webdavConfig = getWebdavConfig();
|
||||
|
||||
return homeAssistantService
|
||||
.getVersion()
|
||||
.then((value) => {
|
||||
const version = value.body.data.version;
|
||||
name = backupConfigService.getFormatedName(type, version);
|
||||
return homeAssistantService.getAddonList();
|
||||
})
|
||||
.then((response) => {
|
||||
addonInHa = response.body.data.addons;
|
||||
addonsToStartStop = sanitizeAddonList(
|
||||
backupConfig.autoStopAddon,
|
||||
response.body.data.addons
|
||||
);
|
||||
return webDavService.checkWebdavLogin(webdavConfig);
|
||||
})
|
||||
.then(() => {
|
||||
return homeAssistantService.stopAddons(addonsToStartStop);
|
||||
})
|
||||
.then((response) => {
|
||||
if (backupConfig.backupType == BackupType.FULL) {
|
||||
return homeAssistantService.createNewBackup(
|
||||
name,
|
||||
backupConfig.backupType,
|
||||
backupConfig.password.enabled,
|
||||
backupConfig.password.value
|
||||
);
|
||||
} else {
|
||||
const addons = getAddonToBackup(
|
||||
backupConfig.exclude?.addon as string[],
|
||||
addonInHa
|
||||
);
|
||||
const folders = getFolderToBackup(
|
||||
backupConfig.exclude?.folder as string[],
|
||||
homeAssistantService.getFolderList()
|
||||
);
|
||||
return homeAssistantService.createNewBackup(
|
||||
name,
|
||||
backupConfig.backupType,
|
||||
backupConfig.password.enabled,
|
||||
backupConfig.password.value,
|
||||
addons,
|
||||
folders
|
||||
);
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
response.body.data.slug;
|
||||
return homeAssistantService.downloadSnapshot(response.body.data.slug);
|
||||
})
|
||||
.then((tmpFile) => {
|
||||
tmpBackupFile = tmpFile;
|
||||
return webDavService.webdavUploadFile(
|
||||
tmpFile,
|
||||
getBackupFolder(type, webdavConfig) + name,
|
||||
webdavConfig
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Backup workflow finished successfully !");
|
||||
messageManager.info("Backup workflow finished successfully !");
|
||||
const status = statusTools.getStatus();
|
||||
status.last_backup.success = true;
|
||||
status.last_backup.last_try = DateTime.now();
|
||||
status.last_backup.last_success = DateTime.now();
|
||||
statusTools.setStatus(status);
|
||||
})
|
||||
.catch(() => {
|
||||
backupFail();
|
||||
if (tmpBackupFile != "") {
|
||||
unlinkSync(tmpBackupFile);
|
||||
}
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
// This methods remove addon that are no installed in HA from the conf array
|
||||
function sanitizeAddonList(addonInConf: string[], addonInHA: AddonModel[]) {
|
||||
addonInConf.filter((value) => addonInHA.some((v) => v.slug == value));
|
||||
return addonInConf;
|
||||
}
|
||||
|
||||
function getAddonToBackup(excludedAddon: string[], addonInHA: AddonModel[]) {
|
||||
const slugs: string[] = [];
|
||||
for (const addon of addonInHA) {
|
||||
if (!excludedAddon.includes(addon.slug)) slugs.push(addon.slug);
|
||||
}
|
||||
logger.debug("Addon to backup:");
|
||||
logger.debug(slugs);
|
||||
return slugs;
|
||||
}
|
||||
|
||||
function getFolderToBackup(
|
||||
excludedFolder: string[],
|
||||
folderInHA: { name: string; slug: string }[]
|
||||
) {
|
||||
const slugs = [];
|
||||
for (const folder of folderInHA) {
|
||||
if (!excludedFolder.includes(folder.slug)) slugs.push(folder.slug);
|
||||
}
|
||||
logger.debug("Folders to backup:");
|
||||
logger.debug(slugs);
|
||||
return slugs;
|
||||
}
|
||||
|
||||
function backupFail() {
|
||||
const status = statusTools.getStatus();
|
||||
status.last_backup.success = false;
|
||||
status.last_backup.last_try = DateTime.now();
|
||||
statusTools.setStatus(status);
|
||||
messageManager.error("Last backup as failed !");
|
||||
}
|
@ -4,9 +4,13 @@ import logger from "../config/winston.js";
|
||||
import { default_root } from "../tools/pathTools.js";
|
||||
import {
|
||||
type WebdavConfig,
|
||||
WebdavEndpointType
|
||||
WebdavEndpointType,
|
||||
} from "../types/services/webdavConfig.js";
|
||||
import WebdavConfigValidation from "../types/services/webdavConfigValidation.js";
|
||||
import { BackupType } from "../types/services/backupConfig.js";
|
||||
import * as pathTools from "../tools/pathTools.js";
|
||||
import { WorkflowType } from "../types/services/orchecstrator.js";
|
||||
import e from "express";
|
||||
|
||||
const webdavConfigPath = "/data/webdavConfigV2.json";
|
||||
const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username";
|
||||
@ -46,12 +50,24 @@ export function getEndpoint(config: WebdavConfig) {
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
if (endpoint.endsWith("/")) {
|
||||
return endpoint.slice(0, -1);
|
||||
if (!endpoint.startsWith("/")) {
|
||||
endpoint = "/" + endpoint;
|
||||
}
|
||||
|
||||
if (!endpoint.endsWith("/")) {
|
||||
return endpoint + "/";
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
export function getBackupFolder(type: WorkflowType, config: WebdavConfig) {
|
||||
const end = type == WorkflowType.AUTO ? pathTools.auto : pathTools.manual;
|
||||
return config.backupDir.endsWith("/")
|
||||
? config.backupDir + end
|
||||
: config.backupDir + "/" + end;
|
||||
}
|
||||
|
||||
export function getWebdavDefaultConfig(): WebdavConfig {
|
||||
return {
|
||||
url: "",
|
||||
|
@ -268,8 +268,7 @@ export function webdavUploadFile(
|
||||
},
|
||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
||||
};
|
||||
const url =
|
||||
config.url + getEndpoint(config) + config.backupDir + webdavPath;
|
||||
const url = config.url + getEndpoint(config) + webdavPath;
|
||||
|
||||
logger.debug(`...URI: ${encodeURI(url)}`);
|
||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||
|
@ -39,7 +39,7 @@ function check_cron(conf: Settings) {
|
||||
if (conf.cron_custom != null) {
|
||||
try {
|
||||
// TODO Need to be destroy
|
||||
new CronJob(conf.cron_custom, () => {
|
||||
new CronJob(conf.cron_custom, () => {
|
||||
//Do nothing
|
||||
});
|
||||
return true;
|
||||
|
@ -1,8 +1,8 @@
|
||||
export interface NewPartialBackupPayload {
|
||||
name?: string;
|
||||
export interface NewBackupPayload {
|
||||
name?: string;
|
||||
password?: string;
|
||||
homeassistant?: boolean;
|
||||
addons?: string[];
|
||||
folders?: string[];
|
||||
compressed?: boolean;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
export enum WorkflowType {
|
||||
AUTO,
|
||||
MANUAL,
|
||||
}
|
@ -17,7 +17,7 @@ export interface Status {
|
||||
last_backup: {
|
||||
success?: boolean;
|
||||
last_success?: DateTime;
|
||||
message?: string;
|
||||
last_try?: DateTime;
|
||||
};
|
||||
next_backup?: string;
|
||||
webdav: {
|
||||
|
6
nextcloud_backup/frontend/components.d.ts
vendored
6
nextcloud_backup/frontend/components.d.ts
vendored
@ -7,6 +7,7 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ActionComponent: typeof import('./src/components/statusBar/ActionComponent.vue')['default']
|
||||
AlertManager: typeof import('./src/components/AlertManager.vue')['default']
|
||||
BackupConfigAddon: typeof import('./src/components/settings/BackupConfig/BackupConfigAddon.vue')['default']
|
||||
BackupConfigAutoBackup: typeof import('./src/components/settings/BackupConfig/BackupConfigAutoBackup.vue')['default']
|
||||
@ -16,9 +17,11 @@ declare module 'vue' {
|
||||
BackupConfigForm: typeof import('./src/components/settings/BackupConfigForm.vue')['default']
|
||||
BackupConfigMenu: typeof import('./src/components/settings/BackupConfigMenu.vue')['default']
|
||||
BackupConfigSecurity: typeof import('./src/components/settings/BackupConfig/BackupConfigSecurity.vue')['default']
|
||||
BackupStatus: typeof import('./src/components/statusBar/BackupStatus.vue')['default']
|
||||
CloudDeleteDialog: typeof import('./src/components/cloud/CloudDeleteDialog.vue')['default']
|
||||
CloudList: typeof import('./src/components/cloud/CloudList.vue')['default']
|
||||
CloudListItem: typeof import('./src/components/cloud/CloudListItem.vue')['default']
|
||||
ConnectionStatus: typeof import('./src/components/statusBar/ConnectionStatus.vue')['default']
|
||||
HaList: typeof import('./src/components/homeAssistant/HaList.vue')['default']
|
||||
HaListItem: typeof import('./src/components/homeAssistant/HaListItem.vue')['default']
|
||||
HaListItemContent: typeof import('./src/components/homeAssistant/HaListItemContent.vue')['default']
|
||||
@ -26,7 +29,8 @@ declare module 'vue' {
|
||||
NavbarComponent: typeof import('./src/components/NavbarComponent.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StatusComponent: typeof import('./src/components/StatusComponent.vue')['default']
|
||||
StatusBar: typeof import('./src/components/statusBar/StatusBar.vue')['default']
|
||||
StatusComponent: typeof import('./src/components/statusBar/StatusComponent.vue')['default']
|
||||
WebdavConfigForm: typeof import('./src/components/settings/WebdavConfigForm.vue')['default']
|
||||
WebdavConfigMenu: typeof import('./src/components/settings/WebdavConfigMenu.vue')['default']
|
||||
}
|
||||
|
@ -5,17 +5,13 @@
|
||||
<webdav-config-menu @saved="cloudList?.refreshBackup"></webdav-config-menu>
|
||||
<backup-config-menu></backup-config-menu>
|
||||
<alert-manager></alert-manager>
|
||||
<v-main class="mx-12">
|
||||
<v-main class="mx-xl-16 mx-lg-10 mx-2">
|
||||
<StatusBar @state-updated="refreshLists"></StatusBar>
|
||||
<v-row>
|
||||
<v-col cols="4" offset="1">
|
||||
<status-component></status-component>
|
||||
<v-col cols="12" lg="6">
|
||||
<ha-list ref="haList"></ha-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<ha-list></ha-list>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-col cols="12" lg="6">
|
||||
<cloud-list ref="cloudList"></cloud-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -32,8 +28,14 @@ import MessageBar from "./components/MessageBar.vue";
|
||||
import NavbarComponent from "./components/NavbarComponent.vue";
|
||||
import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue";
|
||||
import WebdavConfigMenu from "./components/settings/WebdavConfigMenu.vue";
|
||||
import StatusComponent from "./components/StatusComponent.vue";
|
||||
import StatusBar from "./components/statusBar/StatusBar.vue";
|
||||
const cloudList = ref<InstanceType<typeof CloudList> | null>(null);
|
||||
const haList = ref<InstanceType<typeof HaList> | null>(null);
|
||||
|
||||
function refreshLists() {
|
||||
cloudList.value?.refreshBackup();
|
||||
haList.value?.refreshBackup();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="mt-5" border elevation="10">
|
||||
<v-card-title class="text-center">Status</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-card variant="elevated" border>
|
||||
<v-card-text class="align-center d-flex justify-center">
|
||||
<span class="me-auto">Home Assistant</span>
|
||||
<v-tooltip content-class="bg-black">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
variant="elevated"
|
||||
:prepend-icon="hassProps.icon"
|
||||
:color="hassProps.color"
|
||||
:text="hassProps.text"
|
||||
>
|
||||
</v-chip>
|
||||
</template>
|
||||
Last check:
|
||||
{{
|
||||
status?.hass.last_check
|
||||
? DateTime.fromISO(status.hass.last_check).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)
|
||||
: "UNKNOWN"
|
||||
}}
|
||||
</v-tooltip>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-card variant="elevated" border>
|
||||
<v-card-text class="align-center d-flex justify-center">
|
||||
<span class="me-auto">Cloud</span>
|
||||
<v-tooltip content-class="bg-black">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
variant="elevated"
|
||||
:prepend-icon="webdavProps.icon"
|
||||
:color="webdavProps.color"
|
||||
:text="webdavProps.text"
|
||||
>
|
||||
</v-chip>
|
||||
</template>
|
||||
<span>Login: </span>
|
||||
<span :class="'text-' + webdavLoggedProps.color">
|
||||
{{ webdavLoggedProps.text }}
|
||||
</span>
|
||||
<br />
|
||||
<span>Folder: </span>
|
||||
<span :class="'text-' + webdavFolderProps.color">
|
||||
{{ webdavFolderProps.text }}
|
||||
</span>
|
||||
<p>
|
||||
Last check:
|
||||
{{
|
||||
status?.webdav.last_check
|
||||
? DateTime.fromISO(
|
||||
status.webdav.last_check
|
||||
).toLocaleString(DateTime.DATETIME_MED)
|
||||
: "UNKNOWN"
|
||||
}}
|
||||
</p>
|
||||
</v-tooltip>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getStatus } from "@/services/statusService";
|
||||
import { Status } from "@/types/status";
|
||||
import { computed, ref, onBeforeUnmount } from "vue";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
const status = ref<Status | undefined>(undefined);
|
||||
|
||||
function refreshStatus() {
|
||||
getStatus().then((data) => {
|
||||
status.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
const webdavProps = computed(() => {
|
||||
if (status.value?.webdav.logged_in && status.value?.webdav.folder_created) {
|
||||
return { icon: "mdi-check", text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { icon: "mdi-alert", text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
|
||||
const webdavLoggedProps = computed(() => {
|
||||
if (status.value?.webdav.logged_in) {
|
||||
return { text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
|
||||
const webdavFolderProps = computed(() => {
|
||||
if (status.value?.webdav.folder_created) {
|
||||
return { text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
|
||||
const hassProps = computed(() => {
|
||||
if (status.value?.hass.ok) {
|
||||
return { icon: "mdi-check", text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { icon: "mdi-alert", text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
|
||||
refreshStatus();
|
||||
const interval = setInterval(refreshStatus, 2000);
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
@ -15,7 +15,7 @@
|
||||
></v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider></v-divider>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
|
@ -90,7 +90,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
<v-divider class="mx-4 border-opacity-25"></v-divider>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-tooltip text="Upload to Home Assitant" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
|
@ -15,7 +15,7 @@
|
||||
></v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider></v-divider>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
@ -60,15 +60,19 @@ const loading = ref<boolean>(true);
|
||||
|
||||
function refreshBackup() {
|
||||
loading.value = true;
|
||||
getBackups().then((value) => {
|
||||
backups.value = value;
|
||||
loading.value = false;
|
||||
}).catch(()=> {
|
||||
loading.value = false;
|
||||
})
|
||||
getBackups()
|
||||
.then((value) => {
|
||||
backups.value = value;
|
||||
loading.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
refreshBackup();
|
||||
|
||||
defineExpose({ refreshBackup });
|
||||
|
||||
// TODO Manage delete
|
||||
</script>
|
||||
|
@ -120,7 +120,7 @@
|
||||
<h3>Content</h3>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="mt-2"></v-divider>
|
||||
<v-divider class="mt-2 border-opacity-25"></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" lg="6">
|
||||
<div class="text-center text-white mt-2">
|
||||
@ -152,7 +152,7 @@
|
||||
</v-row>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-divider class="mx-4"></v-divider>
|
||||
<v-divider class="mx-4 border-opacity-25"></v-divider>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-tooltip text="Upload to Cloud" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
|
@ -55,7 +55,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-divider class="my-4 border-opacity-25"></v-divider>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<v-sheet border elevation="5" rounded class="py-1">
|
||||
@ -78,7 +78,7 @@
|
||||
<BackupConfigAutoStop :loading="loading"></BackupConfigAutoStop>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-divider class="my-4 border-opacity-25"></v-divider>
|
||||
<v-row class="mb-10">
|
||||
<v-col>
|
||||
<BackupConfigSecurity :loading="loading"></BackupConfigSecurity>
|
||||
|
@ -8,7 +8,7 @@
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-center">Backup Settings</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
<v-card-text>
|
||||
<backup-config-form
|
||||
ref="form"
|
||||
@ -18,7 +18,7 @@
|
||||
@loading="loading = true"
|
||||
></backup-config-form>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn
|
||||
color="red"
|
||||
@ -69,4 +69,4 @@ function saved() {
|
||||
alertStore.add("success", "Backup settings saved !");
|
||||
}
|
||||
</script>
|
||||
@/store/dialogStatus@/store/alert
|
||||
@/store/dialogStatus@/store/alert
|
||||
|
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-card border>
|
||||
<v-card-title class="text-center">Action</v-card-title>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
<v-card-text>
|
||||
<v-btn color="success" @click="launchBackup">Backup Now</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { backupNow } from "@/services/actionService";
|
||||
import { useAlertStore } from "@/store/alert";
|
||||
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
function launchBackup() {
|
||||
backupNow()
|
||||
.then(() => {
|
||||
alertStore.add("success", "Backup workflow started !");
|
||||
})
|
||||
.catch(() => {
|
||||
alertStore.add("error", "Fail to start backup workflow !");
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<v-card border>
|
||||
<v-card-item>
|
||||
<v-card-title class="text-center">Backup</v-card-title>
|
||||
</v-card-item>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col xl="6" lg="12" sm="6" cols="12">
|
||||
<div class="h-100 d-flex align-center">
|
||||
<span class="me-auto">Last</span>
|
||||
<v-tooltip content-class="bg-black">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
variant="elevated"
|
||||
:prepend-icon="lastBackupProps.icon"
|
||||
:color="lastBackupProps.color"
|
||||
:text="lastBackupProps.text"
|
||||
>
|
||||
</v-chip>
|
||||
</template>
|
||||
<p>
|
||||
Last try:
|
||||
{{
|
||||
status?.last_backup.last_try
|
||||
? DateTime.fromISO(
|
||||
status?.last_backup.last_try
|
||||
).toLocaleString(DateTime.DATETIME_MED)
|
||||
: "Unknown"
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
Last success:
|
||||
{{
|
||||
status?.last_backup.last_success
|
||||
? DateTime.fromISO(
|
||||
status?.last_backup.last_success
|
||||
).toLocaleString(DateTime.DATETIME_MED)
|
||||
: "Unknown"
|
||||
}}
|
||||
</p>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-divider vertical class="border-opacity-25 mt-n1"></v-divider>
|
||||
<v-col xl="6" lg="12" sm="6" cols="12">
|
||||
<div class="h-100 d-flex align-center">
|
||||
<span class="me-auto">Next</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-divider class="border-opacity-25 mx-n1"></v-divider>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-progress-linear
|
||||
height="25"
|
||||
:model-value="percent"
|
||||
:indeterminate="
|
||||
status?.progress == -1 && status.status != States.IDLE
|
||||
"
|
||||
class=""
|
||||
color="success"
|
||||
rounded
|
||||
>
|
||||
<strong>{{ status?.status }}</strong>
|
||||
</v-progress-linear>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { States, Status } from "@/types/status";
|
||||
import { DateTime } from "luxon";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
status?: Status;
|
||||
}>();
|
||||
|
||||
const percent = computed(() => {
|
||||
if (props.status?.status == States.IDLE || !props.status?.progress) {
|
||||
return 0;
|
||||
} else {
|
||||
return props.status.progress * 100;
|
||||
}
|
||||
});
|
||||
|
||||
const lastBackupProps = computed(() => {
|
||||
if (props.status?.last_backup.success == undefined) {
|
||||
return { icon: "mdi-help-circle", text: "Unknown", color: "" };
|
||||
} else if (props.status?.last_backup.success) {
|
||||
return { icon: "mdi-check", text: "Success", color: "green" };
|
||||
} else {
|
||||
return { icon: "mdi-alert", text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
</script>
|
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<v-card border elevation="10">
|
||||
<v-card-item>
|
||||
<v-card-title class="text-center">Status</v-card-title>
|
||||
</v-card-item>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
<v-card-text class="h-auto">
|
||||
<v-row align-content="space-around">
|
||||
<v-col xl="6" lg="12" sm="6" cols="12">
|
||||
<div class="h-100 d-flex align-center">
|
||||
<span class="me-auto">Home Assistant</span>
|
||||
<v-tooltip content-class="bg-black">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
variant="elevated"
|
||||
:prepend-icon="hassProps.icon"
|
||||
:color="hassProps.color"
|
||||
:text="hassProps.text"
|
||||
>
|
||||
</v-chip>
|
||||
</template>
|
||||
Last check:
|
||||
{{
|
||||
status?.hass.last_check
|
||||
? DateTime.fromISO(status.hass.last_check).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)
|
||||
: "UNKNOWN"
|
||||
}}
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-divider vertical class="border-opacity-25 my-n1"></v-divider>
|
||||
<v-col xl="6" lg="12" sm="6" cols="12">
|
||||
<div class="h-100 d-flex align-center">
|
||||
<span class="me-auto">Cloud</span>
|
||||
<v-tooltip content-class="bg-black">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
variant="elevated"
|
||||
:prepend-icon="webdavProps.icon"
|
||||
:color="webdavProps.color"
|
||||
:text="webdavProps.text"
|
||||
>
|
||||
</v-chip>
|
||||
</template>
|
||||
<span>Login: </span>
|
||||
<span :class="'text-' + webdavLoggedProps.color">
|
||||
{{ webdavLoggedProps.text }}
|
||||
</span>
|
||||
<br />
|
||||
<span>Folder: </span>
|
||||
<span :class="'text-' + webdavFolderProps.color">
|
||||
{{ webdavFolderProps.text }}
|
||||
</span>
|
||||
<p>
|
||||
Last check:
|
||||
{{
|
||||
status?.webdav.last_check
|
||||
? DateTime.fromISO(status.webdav.last_check).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)
|
||||
: "UNKNOWN"
|
||||
}}
|
||||
</p>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Status } from "@/types/status";
|
||||
import { DateTime } from "luxon";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
status?: Status;
|
||||
}>();
|
||||
|
||||
const webdavProps = computed(() => {
|
||||
if (
|
||||
props.status?.webdav.logged_in == undefined ||
|
||||
props.status?.webdav.folder_created == undefined
|
||||
) {
|
||||
return { icon: "mdi-help-circle", text: "Unknown", color: "" };
|
||||
} else if (
|
||||
props.status?.webdav.logged_in &&
|
||||
props.status?.webdav.folder_created
|
||||
) {
|
||||
return { icon: "mdi-check", text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { icon: "mdi-alert", text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
|
||||
const webdavLoggedProps = computed(() => {
|
||||
if (props.status?.webdav.logged_in) {
|
||||
return { text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
|
||||
const webdavFolderProps = computed(() => {
|
||||
if (props.status?.webdav.folder_created) {
|
||||
return { text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
|
||||
const hassProps = computed(() => {
|
||||
if (props.status?.hass.ok == undefined) {
|
||||
return { icon: "mdi-help-circle", text: "Unknown", color: "" };
|
||||
} else if (props.status?.hass.ok) {
|
||||
return { icon: "mdi-check", text: "Ok", color: "green" };
|
||||
} else {
|
||||
return { icon: "mdi-alert", text: "Fail", color: "red" };
|
||||
}
|
||||
});
|
||||
</script>
|
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<v-row class="mt-5 justify-space-around">
|
||||
<v-col cols="12" lg="4" xxl="3">
|
||||
<ConnectionStatus :status="status"></ConnectionStatus>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4" xxl="3">
|
||||
<BackupStatus :status="status"></BackupStatus>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" lg="4" xxl="3">
|
||||
<ActionComponent></ActionComponent>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<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 ConnectionStatus from "./ConnectionStatus.vue";
|
||||
import BackupStatus from "./BackupStatus.vue";
|
||||
import ActionComponent from "./ActionComponent.vue";
|
||||
|
||||
const status = ref<Status | undefined>(undefined);
|
||||
|
||||
let oldStatus: States | undefined = undefined;
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "stateUpdated", state: string): void;
|
||||
}>();
|
||||
|
||||
function refreshStatus() {
|
||||
getStatus().then((data) => {
|
||||
status.value = data;
|
||||
if (oldStatus != status.value.status) {
|
||||
oldStatus = status.value.status;
|
||||
emit("stateUpdated", status.value.status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshStatus();
|
||||
const interval = setInterval(refreshStatus, 500);
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
5
nextcloud_backup/frontend/src/services/actionService.ts
Normal file
5
nextcloud_backup/frontend/src/services/actionService.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import kyClient from "./kyClient";
|
||||
|
||||
export function backupNow() {
|
||||
return kyClient.post("action/backup");
|
||||
}
|
@ -17,7 +17,7 @@ export interface Status {
|
||||
last_backup: {
|
||||
success?: boolean;
|
||||
last_success?: string;
|
||||
message?: string;
|
||||
last_try?: string;
|
||||
};
|
||||
next_backup?: string;
|
||||
webdav: {
|
||||
|
Loading…
Reference in New Issue
Block a user