Compare commits

..

7 Commits

22 changed files with 453 additions and 119 deletions

View File

@ -8,4 +8,5 @@
"javascript.inlayHints.propertyDeclarationTypes.enabled": true, "javascript.inlayHints.propertyDeclarationTypes.enabled": true,
"javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true, "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true,
"javascript.inlayHints.parameterNames.enabled": "literals", "javascript.inlayHints.parameterNames.enabled": "literals",
"editor.formatOnSave": true
} }

View File

@ -69,11 +69,6 @@ function postInit() {
settingsTools.check(settingsTools.getSettings(), true); settingsTools.check(settingsTools.getSettings(), true);
// cronTools.init(); // cronTools.init();
messageManager.error("this is error");
messageManager.info("This is info");
messageManager.warn("re gerg rg ergrge r ");
messageManager.success("zefzegz gze gerg erg zegfze gerg erg aeferg erg erg er");
} }
export default postInit; export default postInit;

View File

@ -3,6 +3,7 @@ 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";
const router = express.Router(); const router = express.Router();
@ -10,6 +11,7 @@ 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);
export default router; export default router;

View File

@ -5,6 +5,7 @@ import {
validateBackupConfig, validateBackupConfig,
} from "../services/backupConfigService.js"; } from "../services/backupConfigService.js";
import { getWebdavConfig, saveWebdavConfig, validateWebdavConfig } from "../services/webdavConfigService.js"; import { getWebdavConfig, saveWebdavConfig, validateWebdavConfig } from "../services/webdavConfigService.js";
import { checkWebdavLogin } from "../services/webdavService.js";
const configRouter = express.Router(); const configRouter = express.Router();
@ -31,6 +32,9 @@ configRouter.get("/webdav", (req, res, next) => {
configRouter.put("/webdav", (req, res, next) => { configRouter.put("/webdav", (req, res, next) => {
validateWebdavConfig(req.body) validateWebdavConfig(req.body)
.then(() => {
return checkWebdavLogin(req.body, true)
})
.then(() => { .then(() => {
saveWebdavConfig(req.body); saveWebdavConfig(req.body);
res.status(204); res.status(204);
@ -38,7 +42,7 @@ configRouter.put("/webdav", (req, res, next) => {
}) })
.catch((error) => { .catch((error) => {
res.status(400); res.status(400);
res.json(error.details); res.json(error.details ? error.details : error);
}); });
}); });

View File

@ -0,0 +1,12 @@
import express from "express";
import { getStatus } from "../tools/status.js";
const statusRouter = express.Router();
statusRouter.get('/', (req, res) => {
res.json(getStatus());
})
export default statusRouter

View File

@ -16,6 +16,9 @@ webdavRouter.get("/backup/auto", (req, res, next) => {
const config = getWebdavConfig(); const config = getWebdavConfig();
const backupConf = getBackupConfig(); const backupConf = getBackupConfig();
validateWebdavConfig(config) validateWebdavConfig(config)
.then(() => {
return webdavService.checkWebdavLogin(config);
})
.then(async () => { .then(async () => {
const value = await webdavService const value = await webdavService
.getBackups(pathTools.auto, config, backupConf.nameTemplate); .getBackups(pathTools.auto, config, backupConf.nameTemplate);
@ -31,6 +34,9 @@ webdavRouter.get("/backup/manual", (req, res, next) => {
const config = getWebdavConfig(); const config = getWebdavConfig();
const backupConf = getBackupConfig(); const backupConf = getBackupConfig();
validateWebdavConfig(config) validateWebdavConfig(config)
.then(() => {
return webdavService.checkWebdavLogin(config);
})
.then(async () => { .then(async () => {
const value = await webdavService const value = await webdavService
.getBackups(pathTools.manual, config, backupConf.nameTemplate); .getBackups(pathTools.manual, config, backupConf.nameTemplate);
@ -49,6 +55,9 @@ webdavRouter.delete("/", (req, res, next) => {
validateWebdavConfig(config).then(() => { validateWebdavConfig(config).then(() => {
validator validator
.validateAsync(body) .validateAsync(body)
.then(() => {
return webdavService.checkWebdavLogin(config);
})
.then(() => { .then(() => {
webdavService.deleteBackup(body.path, config) webdavService.deleteBackup(body.path, config)
.then(()=>{ .then(()=>{

View File

@ -1,7 +1,12 @@
import fs from "fs"; import fs from "fs";
import FormData from "form-data"; import FormData from "form-data";
import got, { type OptionsOfJSONResponseBody, type Response } from "got"; import got, {
RequestError,
type OptionsOfJSONResponseBody,
type PlainResponse,
type Response,
} from "got";
import stream from "stream"; import stream from "stream";
import { promisify } from "util"; import { promisify } from "util";
import logger from "../config/winston.js"; import logger from "../config/winston.js";
@ -18,7 +23,8 @@ import type {
CoreInfoBody, CoreInfoBody,
SupervisorResponse, SupervisorResponse,
} from "../types/services/ha_os_response.js"; } from "../types/services/ha_os_response.js";
import type { Status } from "../types/status.js"; import { States, type Status } from "../types/status.js";
import { DateTime } from "luxon";
const pipeline = promisify(stream.pipeline); const pipeline = promisify(stream.pipeline);
@ -86,7 +92,6 @@ function getAddonToBackup(addons: AddonModel[]) {
return slugs; return slugs;
} }
function getFolderToBackup() { function getFolderToBackup() {
const excluded_folder = settingsTools.getSettings().exclude_folder; const excluded_folder = settingsTools.getSettings().exclude_folder;
const all_folder = getFolderList(); const all_folder = getFolderList();
@ -109,9 +114,17 @@ function getBackups(): Promise<Response<SupervisorResponse<BackupData>>> {
option option
).then( ).then(
(result) => { (result) => {
const status = statusTools.getStatus();
status.hass.ok = true;
status.hass.last_check = DateTime.now();
statusTools.setStatus(status);
return result; return result;
}, },
(error) => { (error) => {
const status = statusTools.getStatus();
status.hass.ok = false;
status.hass.last_check = DateTime.now();
statusTools.setStatus(status);
messageManager.error("Fail to fetch Hassio backups", error?.message); messageManager.error("Fail to fetch Hassio backups", error?.message);
return Promise.reject(error); return Promise.reject(error);
} }
@ -126,7 +139,7 @@ function downloadSnapshot(id: string): Promise<string> {
const tmp_file = `./temp/${id}.tar`; const tmp_file = `./temp/${id}.tar`;
const stream = fs.createWriteStream(tmp_file); const stream = fs.createWriteStream(tmp_file);
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.status = "download"; status.status = States.BKUP_DOWNLOAD_HA;
status.progress = 0; status.progress = 0;
statusTools.setStatus(status); statusTools.setStatus(status);
const option = { const option = {
@ -147,7 +160,9 @@ function downloadSnapshot(id: string): Promise<string> {
).then( ).then(
() => { () => {
logger.info("Download success !"); logger.info("Download success !");
status.progress = 1; const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status); statusTools.setStatus(status);
logger.debug( logger.debug(
"Snapshot dl size : " + fs.statSync(tmp_file).size / 1024 / 1024 "Snapshot dl size : " + fs.statSync(tmp_file).size / 1024 / 1024
@ -160,6 +175,10 @@ function downloadSnapshot(id: string): Promise<string> {
"Fail to download Home Assistant backup", "Fail to download Home Assistant backup",
reason.message reason.message
); );
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
return Promise.reject(reason); return Promise.reject(reason);
} }
); );
@ -217,7 +236,7 @@ function createNewBackup(
folders: string[] folders: string[]
) { ) {
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.status = "creating"; 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...");
@ -248,12 +267,20 @@ function createNewBackup(
.then( .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();
status.status = States.IDLE;
status.progress = undefined;
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();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
return Promise.reject(reason); return Promise.reject(reason);
} }
); );
@ -297,6 +324,7 @@ function clean(backups: BackupModel[]) {
function uploadSnapshot(path: string) { function uploadSnapshot(path: string) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.status = States.BKUP_UPLOAD_HA;
status.progress = 0; status.progress = 0;
statusTools.setStatus(status); statusTools.setStatus(status);
logger.info("Uploading backup..."); logger.info("Uploading backup...");
@ -320,22 +348,36 @@ function uploadSnapshot(path: string) {
logger.info("Upload done..."); logger.info("Upload done...");
} }
}) })
.on("response", (res) => { .on("response", (res: PlainResponse) => {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
logger.error(status.message); messageManager.error(
"Fail to upload backup to Home Assistant",
`Code: ${res.statusCode} Body: ${res.body}`
);
logger.error("Fail to upload backup to Home Assistant");
logger.error(`Code: ${res.statusCode}`);
logger.error(`Body: ${res.body}`);
fs.unlinkSync(path); fs.unlinkSync(path);
reject(status.message); reject(res.statusCode);
} else { } else {
logger.info(`...Upload finish ! (status: ${res.statusCode})`); logger.info(`...Upload finish ! (status: ${res.statusCode})`);
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
fs.unlinkSync(path); fs.unlinkSync(path);
resolve(res); resolve(res);
} }
}) })
.on("error", (err) => { .on("error", (err: RequestError) => {
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
fs.unlinkSync(path); fs.unlinkSync(path);
messageManager.error( messageManager.error(
"Fail to upload backup to Home Assistant", "Fail to upload backup to Home Assistant",
err?.message err.message
); );
logger.error("Fail to upload backup to Home Assistant"); logger.error("Fail to upload backup to Home Assistant");
logger.error(err); logger.error(err);
@ -346,6 +388,10 @@ function uploadSnapshot(path: string) {
function stopAddons(addonSlugs: string[]) { function stopAddons(addonSlugs: string[]) {
logger.info("Stopping addons..."); logger.info("Stopping addons...");
const status = statusTools.getStatus();
status.status = States.STOP_ADDON;
status.progress = -1;
statusTools.setStatus(status);
const promises = []; const promises = [];
const option: OptionsOfJSONResponseBody = { const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
@ -359,11 +405,16 @@ function stopAddons(addonSlugs: string[]) {
} }
return Promise.allSettled(promises).then((values) => { return Promise.allSettled(promises).then((values) => {
let errors = false; let errors = false;
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
for (const val of values) { for (const val of values) {
if (val.status == "rejected") { if (val.status == "rejected") {
messageManager.error("Fail to stop addon", val.reason); messageManager.error("Fail to stop addon", val.reason);
logger.error("Fail to stop addon"); logger.error("Fail to stop addon");
logger.error(val.reason); logger.error(val.reason);
logger.error;
errors = true; errors = true;
} }
} }
@ -378,7 +429,7 @@ function stopAddons(addonSlugs: string[]) {
function startAddons(addonSlugs: string[]) { function startAddons(addonSlugs: string[]) {
logger.info("Starting addons..."); logger.info("Starting addons...");
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.status = "starting"; status.status = States.START_ADDON;
status.progress = -1; status.progress = -1;
statusTools.setStatus(status); statusTools.setStatus(status);
const promises = []; const promises = [];
@ -393,6 +444,10 @@ function startAddons(addonSlugs: string[]) {
} }
} }
return Promise.allSettled(promises).then((values) => { return Promise.allSettled(promises).then((values) => {
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
let errors = false; let errors = false;
for (const val of values) { for (const val of values) {
if (val.status == "rejected") { if (val.status == "rejected") {
@ -435,7 +490,6 @@ export function getFolderList() {
]; ];
} }
function publish_state(state: Status) { function publish_state(state: Status) {
// let data_error_sensor = { // let data_error_sensor = {
// state: state.status == "error" ? "on" : "off", // state: state.status == "error" ? "on" : "off",
@ -511,5 +565,5 @@ export {
startAddons, startAddons,
clean, clean,
publish_state, publish_state,
getBackupInfo getBackupInfo,
}; };

View File

@ -1,6 +1,11 @@
import { XMLParser } from "fast-xml-parser"; import { XMLParser } from "fast-xml-parser";
import { createReadStream, statSync, unlinkSync } from "fs"; import { createReadStream, stat, statSync, unlinkSync } from "fs";
import got, { HTTPError, type Method } from "got"; import got, {
HTTPError,
RequestError,
type Method,
type PlainResponse,
} from "got";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import logger from "../config/winston.js"; import logger from "../config/winston.js";
import messageManager from "../tools/messageManager.js"; import messageManager from "../tools/messageManager.js";
@ -10,7 +15,7 @@ import type { WebdavBackup } from "../types/services/webdav.js";
import type { WebdavConfig } from "../types/services/webdavConfig.js"; import type { WebdavConfig } from "../types/services/webdavConfig.js";
import { templateToRegexp } from "./backupConfigService.js"; import { templateToRegexp } from "./backupConfigService.js";
import { getEndpoint } from "./webdavConfigService.js"; import { getEndpoint } from "./webdavConfigService.js";
import { WebdabStatus } from "../types/status.js"; import { States } from "../types/status.js";
const PROPFIND_BODY = const PROPFIND_BODY =
'<?xml version="1.0" encoding="utf-8" ?>\ '<?xml version="1.0" encoding="utf-8" ?>\
@ -24,7 +29,10 @@ const PROPFIND_BODY =
</d:prop>\ </d:prop>\
</d:propfind>'; </d:propfind>';
export function checkWebdavLogin(config: WebdavConfig) { export function checkWebdavLogin(
config: WebdavConfig,
silent: boolean = false
) {
const endpoint = getEndpoint(config); const endpoint = getEndpoint(config);
return got(config.url + endpoint, { return got(config.url + endpoint, {
method: "OPTIONS", method: "OPTIONS",
@ -35,16 +43,21 @@ export function checkWebdavLogin(config: WebdavConfig) {
}, },
}).then( }).then(
(response) => { (response) => {
const status = statusTools.getStatus();
status.webdav.logged_in = true;
status.webdav.last_check = DateTime.now();
return response; return response;
}, },
(reason) => { (reason) => {
if (!silent) {
messageManager.error("Fail to connect to Webdav", reason?.message); messageManager.error("Fail to connect to Webdav", reason?.message);
}
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.webdav = { status.webdav = {
state: WebdabStatus.LOGIN_FAIL, logged_in: false,
blocked: true, folder_created: status.webdav.folder_created,
last_check: DateTime.now() last_check: DateTime.now(),
} };
statusTools.setStatus(status); statusTools.setStatus(status);
logger.error(`Fail to connect to Webdav`); logger.error(`Fail to connect to Webdav`);
logger.error(reason); logger.error(reason);
@ -70,11 +83,8 @@ export async function createBackupFolder(conf: WebdavConfig) {
logger.error("Fail to create webdav root folder"); logger.error("Fail to create webdav root folder");
logger.error(error); logger.error(error);
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.webdav = { status.webdav.folder_created = false;
state: WebdabStatus.MK_FOLDER_FAIL, status.webdav.last_check = DateTime.now();
blocked: true,
last_check: DateTime.now()
}
statusTools.setStatus(status); statusTools.setStatus(status);
return Promise.reject(error); return Promise.reject(error);
} }
@ -92,10 +102,18 @@ export async function createBackupFolder(conf: WebdavConfig) {
messageManager.error("Fail to create webdav root folder"); messageManager.error("Fail to create webdav root folder");
logger.error("Fail to create webdav root folder"); logger.error("Fail to create webdav root folder");
logger.error(error); logger.error(error);
const status = statusTools.getStatus();
status.webdav.folder_created = false;
status.webdav.last_check = DateTime.now();
statusTools.setStatus(status);
return Promise.reject(error); return Promise.reject(error);
} }
} }
} }
const status = statusTools.getStatus();
status.webdav.folder_created = true;
status.webdav.last_check = DateTime.now();
statusTools.setStatus(status);
} }
function createDirectory(path: string, config: WebdavConfig) { function createDirectory(path: string, config: WebdavConfig) {
@ -115,6 +133,10 @@ export function getBackups(
config: WebdavConfig, config: WebdavConfig,
nameTemplate: string nameTemplate: string
) { ) {
const status = statusTools.getStatus();
if (!status.webdav.logged_in && !status.webdav.folder_created) {
return Promise.reject("Not logged in");
}
const endpoint = getEndpoint(config); const endpoint = getEndpoint(config);
return got(config.url + endpoint + config.backupDir + folder, { return got(config.url + endpoint + config.backupDir + folder, {
method: "PROPFIND" as Method, method: "PROPFIND" as Method,
@ -161,7 +183,7 @@ function extractBackupInfo(backups: WebdavBackup[], template: string) {
elem.creationDate = DateTime.fromFormat(date, format); elem.creationDate = DateTime.fromFormat(date, format);
} }
if (match?.groups?.version) { if (match?.groups?.version) {
elem.haVersion = match.groups.version elem.haVersion = match.groups.version;
} }
} }
return backups; return backups;
@ -224,13 +246,14 @@ function parseXmlBackupData(body: string, config: WebdavConfig) {
return backups; return backups;
} }
export function webdabUploadFile( export function webdavUploadFile(
localPath: string, localPath: string,
webdavPath: string, webdavPath: string,
config: WebdavConfig config: WebdavConfig
) { ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.info(`Uploading ${localPath} to webdav...`); logger.info(`Uploading ${localPath} to webdav...`);
const stats = statSync(localPath); const stats = statSync(localPath);
const stream = createReadStream(localPath); const stream = createReadStream(localPath);
const options = { const options = {
@ -251,6 +274,9 @@ export function webdabUploadFile(
logger.debug(`...URI: ${encodeURI(url)}`); logger.debug(`...URI: ${encodeURI(url)}`);
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`); logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`);
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.status = States.BKUP_UPLOAD_CLOUD;
status.progress = 0;
statusTools.setStatus(status);
got.stream got.stream
.put(encodeURI(url), options) .put(encodeURI(url), options)
.on("uploadProgress", (e) => { .on("uploadProgress", (e) => {
@ -263,16 +289,19 @@ export function webdabUploadFile(
logger.info("Upload done..."); logger.info("Upload done...");
} }
}) })
.on("response", (res) => { .on("response", (res: PlainResponse) => {
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
if (res.statusCode != 201 && res.statusCode != 204) { if (res.statusCode != 201 && res.statusCode != 204) {
messageManager.error( messageManager.error(
"Fail to upload file to webdav.", "Fail to upload file to Cloud.",
`Status code: ${res.statusCode}` `Code: ${res.statusCode} Body: ${res.body}`
); );
logger.error( logger.error(`Fail to upload file to Cloud`);
`Fail to upload file to webdav, Status code: ${res.statusCode}` logger.error(`Code: ${res.statusCode}`);
); logger.error(`Body: ${res.body}`);
logger.error(status.message);
unlinkSync(localPath); unlinkSync(localPath);
reject(res); reject(res);
} else { } else {
@ -281,10 +310,16 @@ export function webdabUploadFile(
resolve(undefined); resolve(undefined);
} }
}) })
.on("error", (err) => { .on("error", (err: RequestError) => {
logger.error(status.message); const status = statusTools.getStatus();
logger.error(err.stack); status.status = States.IDLE;
reject(status.message); status.progress = undefined;
statusTools.setStatus(status);
messageManager.error("Fail to upload backup to Cloud", err.message);
logger.error("Fail to upload backup to Cloud");
logger.error(err);
unlinkSync(localPath);
reject(err);
}); });
}); });
} }

View File

@ -1,25 +1,27 @@
import { publish_state } from "../services/homeAssistantService.js"; import { publish_state } from "../services/homeAssistantService.js";
import logger from "../config/winston.js"; import { States, type Status } from "../types/status.js";
import { type Status, WebdabStatus } from "../types/status.js";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
let status: Status = { let status: Status = {
status: "idle", status: States.IDLE,
last_backup: undefined, last_backup: {},
next_backup: undefined, next_backup: undefined,
progress: -1, progress: undefined,
webdav: { webdav: {
state: WebdabStatus.INIT, logged_in: false,
folder_created: false,
last_check: DateTime.now(),
},
hass: {
ok: false,
last_check: DateTime.now(), last_check: DateTime.now(),
blocked: true,
}, },
}; };
export function init() { export function init() {
if (status.status !== "idle") { if (status.status !== States.IDLE) {
status.status = "idle"; status.status = States.IDLE;
status.message = undefined; status.progress = undefined;
status.progress = -1;
} }
} }
@ -34,13 +36,3 @@ export function setStatus(new_state: Status) {
publish_state(status); publish_state(status);
} }
} }
export function setError(message: string, error_code: number) {
// Check if we don't have another error stored
if (status.status != "error") {
status.status = "error";
status.message = message;
status.error_code = error_code;
}
logger.error(message);
}

View File

@ -1,24 +1,32 @@
import type { DateTime } from "luxon"; import type { DateTime } from "luxon";
export enum WebdabStatus { export enum States {
OK = "OK", IDLE = "IDLE",
LOGIN_FAIL = "LOGIN_FAIL", BKUP_CREATION = "BKUP_CREATION",
UPLOAD_FAIL = "UPLOAD_FAIL", BKUP_DOWNLOAD_HA = "BKUP_DOWNLOAD_HA",
CON_ERROR = "CON_ERROR", BKUP_DOWNLOAD_CLOUD = "BKUP_DOWNLOAD_CLOUD",
INIT = "INIT", BKUP_UPLOAD_HA = "BKUP_UPLOAD_HA",
MK_FOLDER_FAIL = "MK_FOLDER_FAIL" BKUP_UPLOAD_CLOUD = "BKUP_UPLOAD_CLOUD",
STOP_ADDON = "STOP_ADDON",
START_ADDON = "START_ADDON",
} }
export interface Status { export interface Status {
status: string; status: States;
progress: number; progress?: number;
last_backup?: string; last_backup: {
next_backup?: string; success?: boolean;
last_success?: DateTime;
message?: string; message?: string;
error_code?: number; };
next_backup?: string;
webdav: { webdav: {
state: WebdabStatus; logged_in: boolean;
folder_created: boolean;
last_check: DateTime;
};
hass: {
ok: boolean;
last_check: DateTime; last_check: DateTime;
blocked: boolean;
}; };
} }

View File

@ -1,3 +1,3 @@
{ {
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] "recommendations": ["Vue.volar"]
} }

View File

@ -26,6 +26,7 @@ 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']
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']
} }

View File

@ -21,6 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.23.0", "@babel/types": "^7.23.0",
"@types/luxon": "^3.4.2",
"@types/node": "^20.10.0", "@types/node": "^20.10.0",
"@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue": "^4.5.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",

View File

@ -37,6 +37,9 @@ devDependencies:
'@babel/types': '@babel/types':
specifier: ^7.23.0 specifier: ^7.23.0
version: 7.23.9 version: 7.23.9
'@types/luxon':
specifier: ^3.4.2
version: 3.4.2
'@types/node': '@types/node':
specifier: ^20.10.0 specifier: ^20.10.0
version: 20.11.19 version: 20.11.19
@ -526,6 +529,10 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true dev: true
/@types/luxon@3.4.2:
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
dev: true
/@types/node@20.11.19: /@types/node@20.11.19:
resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==} resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==}
dependencies: dependencies:

View File

@ -2,16 +2,21 @@
<v-app> <v-app>
<navbar-component></navbar-component> <navbar-component></navbar-component>
<message-bar></message-bar> <message-bar></message-bar>
<webdav-settings-menu></webdav-settings-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-12">
<v-row>
<v-col cols="4" offset="1">
<status-component></status-component>
</v-col>
</v-row>
<v-row> <v-row>
<v-col cols="6"> <v-col cols="6">
<ha-list></ha-list> <ha-list></ha-list>
</v-col> </v-col>
<v-col cols="6"> <v-col cols="6">
<cloud-list></cloud-list> <cloud-list ref="cloudList"></cloud-list>
</v-col> </v-col>
</v-row> </v-row>
</v-main> </v-main>
@ -19,13 +24,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue";
import AlertManager from "./components/AlertManager.vue"; import AlertManager from "./components/AlertManager.vue";
import CloudList from "./components/cloud/CloudList.vue"; import CloudList from "./components/cloud/CloudList.vue";
import HaList from "./components/homeAssistant/HaList.vue"; import HaList from "./components/homeAssistant/HaList.vue";
import MessageBar from "./components/MessageBar.vue"; 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 WebdavSettingsMenu from "./components/settings/WebdavConfigMenu.vue"; import WebdavConfigMenu from "./components/settings/WebdavConfigMenu.vue";
import StatusComponent from "./components/StatusComponent.vue";
const cloudList = ref<InstanceType<typeof CloudList> | null>(null);
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -11,9 +11,7 @@
class="mb-2" class="mb-2"
> >
<v-row dense> <v-row dense>
<v-col> <v-col v-html="alert.message"></v-col>
{{ alert.message }}
</v-col>
<v-col cols="2"> <v-col cols="2">
<v-btn <v-btn
class="d-inline" class="d-inline"

View File

@ -0,0 +1,130 @@
<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>

View File

@ -1,7 +1,20 @@
<template> <template>
<div> <div>
<v-card elevation="10" class="mt-10" border> <v-card elevation="10" border>
<v-row align="center" justify="center">
<v-col offset="2">
<v-card-title class="text-center"> Cloud </v-card-title> <v-card-title class="text-center"> Cloud </v-card-title>
</v-col>
<v-col cols="2">
<v-btn
class="float-right mr-2"
icon="mdi-refresh"
variant="text"
@click="refreshBackup"
:loading="loading"
></v-btn>
</v-col>
</v-row>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text> <v-card-text>
<v-row> <v-row>
@ -81,12 +94,22 @@ const deleteDialog = ref<InstanceType<typeof CloudDeleteDialog> | null>(null);
const deleteItem = ref<WebdavBackup | null>(null); const deleteItem = ref<WebdavBackup | null>(null);
const autoBackups = ref<WebdavBackup[]>([]); const autoBackups = ref<WebdavBackup[]>([]);
const manualBackups = ref<WebdavBackup[]>([]); const manualBackups = ref<WebdavBackup[]>([]);
const loading = ref<boolean>(true);
function refreshBackup() { function refreshBackup() {
getAutoBackupList().then((value) => { loading.value = true;
getAutoBackupList()
.then((value) => {
autoBackups.value = value; autoBackups.value = value;
}); return getManualBackupList();
getManualBackupList().then((value) => { })
.then((value) => {
manualBackups.value = value; manualBackups.value = value;
loading.value = false;
})
.catch(() => {
loading.value = false;
}); });
} }
@ -95,10 +118,5 @@ function deleteBackup(item: WebdavBackup) {
deleteDialog.value?.open(item); deleteDialog.value?.open(item);
} }
refreshBackup(); refreshBackup();
defineExpose({ refreshBackup });
const interval = setInterval(refreshBackup, 2000);
onBeforeUnmount(() => {
clearInterval(interval);
});
</script> </script>

View File

@ -1,7 +1,20 @@
<template> <template>
<div> <div>
<v-card elevation="10" class="mt-10" border> <v-card elevation="10" border>
<v-row align="center" justify="center">
<v-col offset="2">
<v-card-title class="text-center"> Home Assistant </v-card-title> <v-card-title class="text-center"> Home Assistant </v-card-title>
</v-col>
<v-col cols="2">
<v-btn
class="float-right mr-2"
icon="mdi-refresh"
variant="text"
@click="refreshBackup"
:loading="loading"
></v-btn>
</v-col>
</v-row>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text> <v-card-text>
<v-row> <v-row>
@ -43,19 +56,19 @@ import { getBackups } from "@/services/homeAssistantService";
import HaListItem from "./HaListItem.vue"; import HaListItem from "./HaListItem.vue";
const backups = ref<BackupModel[]>([]); const backups = ref<BackupModel[]>([]);
const loading = ref<boolean>(true);
function refreshBackup() { function refreshBackup() {
loading.value = true;
getBackups().then((value) => { getBackups().then((value) => {
backups.value = value; backups.value = value;
}); loading.value = false;
}).catch(()=> {
loading.value = false;
})
} }
refreshBackup(); refreshBackup();
const interval = setInterval(refreshBackup, 2000);
onBeforeUnmount(() => {
clearInterval(interval);
});
// TODO Manage delete // TODO Manage delete
</script> </script>

View File

@ -53,6 +53,10 @@ let saveLoading = computed(() => {
return saving.value || loading.value; return saving.value || loading.value;
}); });
const emit = defineEmits<{
(e: "saved"): void;
}>();
function save() { function save() {
saving.value = true; saving.value = true;
form.value?.save(); form.value?.save();
@ -60,13 +64,16 @@ function save() {
function fail() { function fail() {
saving.value = false; saving.value = false;
alertStore.add("error", "Fail to save cloud settings !"); alertStore.add(
"error",
"Fail to connect to Cloud<br>Please check credentials !"
);
} }
function saved() { function saved() {
dialogStatusStore.webdav = false; dialogStatusStore.webdav = false;
saving.value = false; saving.value = false;
alertStore.add("success", "Cloud settings saved !"); alertStore.add("success", "Cloud settings saved !");
emit("saved");
} }
</script> </script>
@/store/alert@/store/dialogStatus

View File

@ -0,0 +1,7 @@
import type { WebdavBackup } from "@/types/webdav";
import kyClient from "./kyClient";
import { Status } from "@/types/status";
export function getStatus() {
return kyClient.get("status").json<Status>();
}

View File

@ -0,0 +1,32 @@
import type { DateTime } from "luxon";
export enum States {
IDLE = "IDLE",
BKUP_CREATION = "BKUP_CREATION",
BKUP_DOWNLOAD_HA = "BKUP_DOWNLOAD_HA",
BKUP_DOWNLOAD_CLOUD = "BKUP_DOWNLOAD_CLOUD",
BKUP_UPLOAD_HA = "BKUP_UPLOAD_HA",
BKUP_UPLOAD_CLOUD = "BKUP_UPLOAD_CLOUD",
STOP_ADDON = "STOP_ADDON",
START_ADDON = "START_ADDON",
}
export interface Status {
status: States;
progress?: number;
last_backup: {
success?: boolean;
last_success?: string;
message?: string;
};
next_backup?: string;
webdav: {
logged_in: boolean;
folder_created: boolean;
last_check: string;
};
hass: {
ok: boolean;
last_check: string;
};
}