mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-29 12:24:52 +01:00
Add orchestrator, manual backup is functional 🎉
This commit is contained in:
parent
18446ca610
commit
bb68885872
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 configRouter from "./config.js";
|
||||||
import homeAssistant from "./homeAssistant.js"
|
import homeAssistant from "./homeAssistant.js";
|
||||||
import messageRouter from "./messages.js";
|
import messageRouter from "./messages.js";
|
||||||
import webdavRouter from "./webdav.js";
|
import webdavRouter from "./webdav.js";
|
||||||
import statusRouter from "./status.js";
|
import statusRouter from "./status.js";
|
||||||
|
import actionRouter from "./action.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use("/homeAssistant", homeAssistant)
|
router.use("/homeAssistant", homeAssistant);
|
||||||
router.use("/config", configRouter);
|
router.use("/config", configRouter);
|
||||||
router.use("/webdav", webdavRouter);
|
router.use("/webdav", webdavRouter);
|
||||||
router.use("/messages", messageRouter);
|
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 fs from "fs";
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import logger from "../config/winston.js";
|
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 backupConfigValidation from "../types/services/backupConfigValidation.js";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { WorkflowType } from "../types/services/orchecstrator.js";
|
||||||
|
|
||||||
const backupConfigPath = "/data/backupConfigV2.json";
|
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})");
|
let regexp = template.replace("{date}", "(?<date>\\d{4}-\\d{2}-\\d{2})");
|
||||||
regexp = regexp.replace("{hour}", "(?<hour>\\d{4})");
|
regexp = regexp.replace("{hour}", "(?<hour>\\d{4})");
|
||||||
regexp = regexp.replace("{hour_12}", "(?<hour12>\\d{4}(AM|PM))");
|
regexp = regexp.replace("{hour_12}", "(?<hour12>\\d{4}(AM|PM))");
|
||||||
regexp = regexp.replace("{type}", "(?<type>Auto|Manual|)")
|
regexp = regexp.replace("{type}", "(?<type>Auto|Manual|)");
|
||||||
regexp = regexp.replace("{type_low}", "(?<type>auto|manual|)")
|
regexp = regexp.replace("{type_low}", "(?<type>auto|manual|)");
|
||||||
return regexp.replace(
|
return regexp.replace(
|
||||||
"{ha_version}",
|
"{ha_version}",
|
||||||
"(?<version>\\d+\\.\\d+\\.\\d+(b\\d+)?)"
|
"(?<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 messageManager from "../tools/messageManager.js";
|
||||||
import * as settingsTools from "../tools/settingsTools.js";
|
import * as settingsTools from "../tools/settingsTools.js";
|
||||||
import * as statusTools from "../tools/status.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 {
|
import type {
|
||||||
AddonData,
|
AddonData,
|
||||||
AddonModel,
|
AddonModel,
|
||||||
@ -25,6 +25,7 @@ import type {
|
|||||||
} from "../types/services/ha_os_response.js";
|
} from "../types/services/ha_os_response.js";
|
||||||
import { States, type Status } from "../types/status.js";
|
import { States, type Status } from "../types/status.js";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
import { BackupType } from "../types/services/backupConfig.js";
|
||||||
|
|
||||||
const pipeline = promisify(stream.pipeline);
|
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>>> {
|
function getBackups(): Promise<Response<SupervisorResponse<BackupData>>> {
|
||||||
const option: OptionsOfJSONResponseBody = {
|
const option: OptionsOfJSONResponseBody = {
|
||||||
headers: { authorization: `Bearer ${token}` },
|
headers: { authorization: `Bearer ${token}` },
|
||||||
@ -232,25 +210,24 @@ function getBackupInfo(id: string) {
|
|||||||
|
|
||||||
function createNewBackup(
|
function createNewBackup(
|
||||||
name: string,
|
name: string,
|
||||||
addonSlugs: string[],
|
type: BackupType,
|
||||||
folders: string[]
|
passwordEnable: boolean,
|
||||||
|
password?: string,
|
||||||
|
addonSlugs?: string[],
|
||||||
|
folders?: string[]
|
||||||
) {
|
) {
|
||||||
const status = statusTools.getStatus();
|
const status = statusTools.getStatus();
|
||||||
status.status = States.BKUP_CREATION;
|
status.status = States.BKUP_CREATION;
|
||||||
status.progress = -1;
|
status.progress = -1;
|
||||||
statusTools.setStatus(status);
|
statusTools.setStatus(status);
|
||||||
logger.info("Creating new snapshot...");
|
logger.info("Creating new snapshot...");
|
||||||
const body: NewPartialBackupPayload = {
|
const body: NewBackupPayload = {
|
||||||
name: name,
|
name: name,
|
||||||
addons: addonSlugs,
|
password: passwordEnable ? password : undefined,
|
||||||
folders: folders,
|
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 = {
|
const option: OptionsOfJSONResponseBody = {
|
||||||
headers: { authorization: `Bearer ${token}` },
|
headers: { authorization: `Bearer ${token}` },
|
||||||
responseType: "json",
|
responseType: "json",
|
||||||
@ -259,31 +236,31 @@ function createNewBackup(
|
|||||||
},
|
},
|
||||||
json: body,
|
json: body,
|
||||||
};
|
};
|
||||||
return got
|
|
||||||
.post<SupervisorResponse<{ slug: string }>>(
|
const url =
|
||||||
`http://hassio/backups/new/partial`,
|
type == BackupType.PARTIAL
|
||||||
option
|
? "http://hassio/backups/new/partial"
|
||||||
)
|
: "http://hassio/backups/new/full";
|
||||||
.then(
|
return got.post<SupervisorResponse<{ slug: string }>>(url, option).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
logger.info(`Snapshot created with id ${result.body.data.slug}`);
|
logger.info(`Snapshot created with id ${result.body.data.slug}`);
|
||||||
const status = statusTools.getStatus();
|
const status = statusTools.getStatus();
|
||||||
status.status = States.IDLE;
|
status.status = States.IDLE;
|
||||||
status.progress = undefined;
|
status.progress = undefined;
|
||||||
statusTools.setStatus(status);
|
statusTools.setStatus(status);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
(reason) => {
|
(reason) => {
|
||||||
messageManager.error("Fail to create new backup.", reason.message);
|
messageManager.error("Fail to create new backup.", reason.message);
|
||||||
logger.error("Fail to create new backup");
|
logger.error("Fail to create new backup");
|
||||||
logger.error(reason);
|
logger.error(reason);
|
||||||
const status = statusTools.getStatus();
|
const status = statusTools.getStatus();
|
||||||
status.status = States.IDLE;
|
status.status = States.IDLE;
|
||||||
status.progress = undefined;
|
status.progress = undefined;
|
||||||
statusTools.setStatus(status);
|
statusTools.setStatus(status);
|
||||||
return Promise.reject(reason);
|
return Promise.reject(reason);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clean(backups: BackupModel[]) {
|
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 { default_root } from "../tools/pathTools.js";
|
||||||
import {
|
import {
|
||||||
type WebdavConfig,
|
type WebdavConfig,
|
||||||
WebdavEndpointType
|
WebdavEndpointType,
|
||||||
} from "../types/services/webdavConfig.js";
|
} from "../types/services/webdavConfig.js";
|
||||||
import WebdavConfigValidation from "../types/services/webdavConfigValidation.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 webdavConfigPath = "/data/webdavConfigV2.json";
|
||||||
const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username";
|
const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username";
|
||||||
@ -46,12 +50,24 @@ export function getEndpoint(config: WebdavConfig) {
|
|||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (endpoint.endsWith("/")) {
|
if (!endpoint.startsWith("/")) {
|
||||||
return endpoint.slice(0, -1);
|
endpoint = "/" + endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!endpoint.endsWith("/")) {
|
||||||
|
return endpoint + "/";
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
export function getWebdavDefaultConfig(): WebdavConfig {
|
||||||
return {
|
return {
|
||||||
url: "",
|
url: "",
|
||||||
|
@ -268,8 +268,7 @@ export function webdavUploadFile(
|
|||||||
},
|
},
|
||||||
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
https: { rejectUnauthorized: !config.allowSelfSignedCerts },
|
||||||
};
|
};
|
||||||
const url =
|
const url = config.url + getEndpoint(config) + webdavPath;
|
||||||
config.url + getEndpoint(config) + config.backupDir + webdavPath;
|
|
||||||
|
|
||||||
logger.debug(`...URI: ${encodeURI(url)}`);
|
logger.debug(`...URI: ${encodeURI(url)}`);
|
||||||
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
|
||||||
|
@ -39,7 +39,7 @@ function check_cron(conf: Settings) {
|
|||||||
if (conf.cron_custom != null) {
|
if (conf.cron_custom != null) {
|
||||||
try {
|
try {
|
||||||
// TODO Need to be destroy
|
// TODO Need to be destroy
|
||||||
new CronJob(conf.cron_custom, () => {
|
new CronJob(conf.cron_custom, () => {
|
||||||
//Do nothing
|
//Do nothing
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
export interface NewPartialBackupPayload {
|
export interface NewBackupPayload {
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
homeassistant?: boolean;
|
homeassistant?: boolean;
|
||||||
addons?: string[];
|
addons?: string[];
|
||||||
folders?: string[];
|
folders?: string[];
|
||||||
compressed?: boolean;
|
compressed?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum WorkflowType {
|
||||||
|
AUTO,
|
||||||
|
MANUAL,
|
||||||
|
}
|
@ -17,7 +17,7 @@ export interface Status {
|
|||||||
last_backup: {
|
last_backup: {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
last_success?: DateTime;
|
last_success?: DateTime;
|
||||||
message?: string;
|
last_try?: DateTime;
|
||||||
};
|
};
|
||||||
next_backup?: string;
|
next_backup?: string;
|
||||||
webdav: {
|
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' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ActionComponent: typeof import('./src/components/statusBar/ActionComponent.vue')['default']
|
||||||
AlertManager: typeof import('./src/components/AlertManager.vue')['default']
|
AlertManager: typeof import('./src/components/AlertManager.vue')['default']
|
||||||
BackupConfigAddon: typeof import('./src/components/settings/BackupConfig/BackupConfigAddon.vue')['default']
|
BackupConfigAddon: typeof import('./src/components/settings/BackupConfig/BackupConfigAddon.vue')['default']
|
||||||
BackupConfigAutoBackup: typeof import('./src/components/settings/BackupConfig/BackupConfigAutoBackup.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']
|
BackupConfigForm: typeof import('./src/components/settings/BackupConfigForm.vue')['default']
|
||||||
BackupConfigMenu: typeof import('./src/components/settings/BackupConfigMenu.vue')['default']
|
BackupConfigMenu: typeof import('./src/components/settings/BackupConfigMenu.vue')['default']
|
||||||
BackupConfigSecurity: typeof import('./src/components/settings/BackupConfig/BackupConfigSecurity.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']
|
CloudDeleteDialog: typeof import('./src/components/cloud/CloudDeleteDialog.vue')['default']
|
||||||
CloudList: typeof import('./src/components/cloud/CloudList.vue')['default']
|
CloudList: typeof import('./src/components/cloud/CloudList.vue')['default']
|
||||||
CloudListItem: typeof import('./src/components/cloud/CloudListItem.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']
|
HaList: typeof import('./src/components/homeAssistant/HaList.vue')['default']
|
||||||
HaListItem: typeof import('./src/components/homeAssistant/HaListItem.vue')['default']
|
HaListItem: typeof import('./src/components/homeAssistant/HaListItem.vue')['default']
|
||||||
HaListItemContent: typeof import('./src/components/homeAssistant/HaListItemContent.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']
|
NavbarComponent: typeof import('./src/components/NavbarComponent.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
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']
|
WebdavConfigForm: typeof import('./src/components/settings/WebdavConfigForm.vue')['default']
|
||||||
WebdavConfigMenu: typeof import('./src/components/settings/WebdavConfigMenu.vue')['default']
|
WebdavConfigMenu: typeof import('./src/components/settings/WebdavConfigMenu.vue')['default']
|
||||||
}
|
}
|
||||||
|
@ -5,17 +5,13 @@
|
|||||||
<webdav-config-menu @saved="cloudList?.refreshBackup"></webdav-config-menu>
|
<webdav-config-menu @saved="cloudList?.refreshBackup"></webdav-config-menu>
|
||||||
<backup-config-menu></backup-config-menu>
|
<backup-config-menu></backup-config-menu>
|
||||||
<alert-manager></alert-manager>
|
<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-row>
|
||||||
<v-col cols="4" offset="1">
|
<v-col cols="12" lg="6">
|
||||||
<status-component></status-component>
|
<ha-list ref="haList"></ha-list>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
<v-col cols="12" lg="6">
|
||||||
<v-row>
|
|
||||||
<v-col cols="6">
|
|
||||||
<ha-list></ha-list>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="6">
|
|
||||||
<cloud-list ref="cloudList"></cloud-list>
|
<cloud-list ref="cloudList"></cloud-list>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@ -32,8 +28,14 @@ import MessageBar from "./components/MessageBar.vue";
|
|||||||
import NavbarComponent from "./components/NavbarComponent.vue";
|
import NavbarComponent from "./components/NavbarComponent.vue";
|
||||||
import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue";
|
import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue";
|
||||||
import WebdavConfigMenu from "./components/settings/WebdavConfigMenu.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 cloudList = ref<InstanceType<typeof CloudList> | null>(null);
|
||||||
|
const haList = ref<InstanceType<typeof HaList> | null>(null);
|
||||||
|
|
||||||
|
function refreshLists() {
|
||||||
|
cloudList.value?.refreshBackup();
|
||||||
|
haList.value?.refreshBackup();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<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-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-divider></v-divider>
|
<v-divider class="border-opacity-25"></v-divider>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
|
@ -90,7 +90,7 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</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-card-actions class="justify-center">
|
||||||
<v-tooltip text="Upload to Home Assitant" location="bottom">
|
<v-tooltip text="Upload to Home Assitant" location="bottom">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
></v-btn>
|
></v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-divider></v-divider>
|
<v-divider class="border-opacity-25"></v-divider>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
@ -60,15 +60,19 @@ const loading = ref<boolean>(true);
|
|||||||
|
|
||||||
function refreshBackup() {
|
function refreshBackup() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
getBackups().then((value) => {
|
getBackups()
|
||||||
backups.value = value;
|
.then((value) => {
|
||||||
loading.value = false;
|
backups.value = value;
|
||||||
}).catch(()=> {
|
loading.value = false;
|
||||||
loading.value = false;
|
})
|
||||||
})
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshBackup();
|
refreshBackup();
|
||||||
|
|
||||||
|
defineExpose({ refreshBackup });
|
||||||
|
|
||||||
// TODO Manage delete
|
// TODO Manage delete
|
||||||
</script>
|
</script>
|
||||||
|
@ -120,7 +120,7 @@
|
|||||||
<h3>Content</h3>
|
<h3>Content</h3>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-divider class="mt-2"></v-divider>
|
<v-divider class="mt-2 border-opacity-25"></v-divider>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" lg="6">
|
<v-col cols="12" lg="6">
|
||||||
<div class="text-center text-white mt-2">
|
<div class="text-center text-white mt-2">
|
||||||
@ -152,7 +152,7 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
</template>
|
</template>
|
||||||
</v-card-text>
|
</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-card-actions class="justify-center">
|
||||||
<v-tooltip text="Upload to Cloud" location="bottom">
|
<v-tooltip text="Upload to Cloud" location="bottom">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-fade-transition>
|
</v-fade-transition>
|
||||||
<v-divider class="my-4"></v-divider>
|
<v-divider class="my-4 border-opacity-25"></v-divider>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col class="text-center">
|
<v-col class="text-center">
|
||||||
<v-sheet border elevation="5" rounded class="py-1">
|
<v-sheet border elevation="5" rounded class="py-1">
|
||||||
@ -78,7 +78,7 @@
|
|||||||
<BackupConfigAutoStop :loading="loading"></BackupConfigAutoStop>
|
<BackupConfigAutoStop :loading="loading"></BackupConfigAutoStop>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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-row class="mb-10">
|
||||||
<v-col>
|
<v-col>
|
||||||
<BackupConfigSecurity :loading="loading"></BackupConfigSecurity>
|
<BackupConfigSecurity :loading="loading"></BackupConfigSecurity>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="text-center">Backup Settings</v-card-title>
|
<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>
|
<v-card-text>
|
||||||
<backup-config-form
|
<backup-config-form
|
||||||
ref="form"
|
ref="form"
|
||||||
@ -18,7 +18,7 @@
|
|||||||
@loading="loading = true"
|
@loading="loading = true"
|
||||||
></backup-config-form>
|
></backup-config-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider></v-divider>
|
<v-divider class="border-opacity-25"></v-divider>
|
||||||
<v-card-actions class="justify-end">
|
<v-card-actions class="justify-end">
|
||||||
<v-btn
|
<v-btn
|
||||||
color="red"
|
color="red"
|
||||||
@ -69,4 +69,4 @@ function saved() {
|
|||||||
alertStore.add("success", "Backup settings saved !");
|
alertStore.add("success", "Backup settings saved !");
|
||||||
}
|
}
|
||||||
</script>
|
</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: {
|
last_backup: {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
last_success?: string;
|
last_success?: string;
|
||||||
message?: string;
|
last_try?: string;
|
||||||
};
|
};
|
||||||
next_backup?: string;
|
next_backup?: string;
|
||||||
webdav: {
|
webdav: {
|
||||||
|
Loading…
Reference in New Issue
Block a user