Compare commits

..

No commits in common. "56cfb4ee787825c9c15ebbab64f9df9de24fa0a7" and "2c03791db5b6ff0d79efae78aa6f4c441c7f243a" have entirely different histories.

20 changed files with 517 additions and 404 deletions

View File

@ -13,7 +13,8 @@ actionRouter.post("/backup", (req, res) => {
.catch(() => { .catch(() => {
logger.error("Something wrong !"); logger.error("Something wrong !");
}); });
res.sendStatus(202); res.statusCode = 200;
res.send();
}); });
export default actionRouter; export default actionRouter;

View File

@ -24,10 +24,12 @@ configRouter.put("/backup", (req, res) => {
validateBackupConfig(req.body as BackupConfig) validateBackupConfig(req.body as BackupConfig)
.then(() => { .then(() => {
saveBackupConfig(req.body as BackupConfig); saveBackupConfig(req.body as BackupConfig);
res.status(204).send(); res.status(204);
res.send();
}) })
.catch((error: ValidationError) => { .catch((error: ValidationError) => {
res.status(400).json(error.details); res.status(400);
res.json(error.details);
}); });
}); });
@ -42,7 +44,8 @@ configRouter.put("/webdav", (req, res) => {
}) })
.then(() => { .then(() => {
saveWebdavConfig(req.body as WebdavConfig); saveWebdavConfig(req.body as WebdavConfig);
res.status(204).send(); res.status(204);
res.send();
}) })
.catch((error: ValidationError) => { .catch((error: ValidationError) => {
res.status(400); res.status(400);

View File

@ -1,7 +1,5 @@
import express from "express"; import express from "express";
import * as haOsService from "../services/homeAssistantService.js"; import * as haOsService from "../services/homeAssistantService.js";
import { uploadToCloud } from "../services/orchestrator.js";
import logger from "../config/winston.js";
const homeAssistantRouter = express.Router(); const homeAssistantRouter = express.Router();
@ -16,7 +14,8 @@ homeAssistantRouter.get("/backups/", (req, res) => {
); );
}) })
.catch((reason) => { .catch((reason) => {
res.status(500).json(reason); res.status(500);
res.json(reason);
}); });
}); });
@ -27,32 +26,11 @@ homeAssistantRouter.get("/backup/:slug", (req, res) => {
res.json(value.body.data); res.json(value.body.data);
}) })
.catch((reason) => { .catch((reason) => {
res.status(500).json(reason); res.status(500);
res.json(reason);
}); });
}); });
homeAssistantRouter.delete("/backup/:slug", (req, res) => {
haOsService
.delSnap(req.params.slug)
.then((value) => {
res.json(value.body);
})
.catch((reason) => {
res.status(500).json(reason);
});
});
homeAssistantRouter.post("/backup/:slug/upload", (req, res) => {
uploadToCloud(req.params.slug)
.then(() => {
logger.info("All good !");
})
.catch(() => {
logger.error("Something wrong !");
});
res.sendStatus(202);
});
homeAssistantRouter.get("/addons", (req, res) => { homeAssistantRouter.get("/addons", (req, res) => {
haOsService haOsService
.getAddonList() .getAddonList()
@ -60,7 +38,8 @@ homeAssistantRouter.get("/addons", (req, res) => {
res.json(value.body.data); res.json(value.body.data);
}) })
.catch((reason) => { .catch((reason) => {
res.status(500).json(reason); res.status(500);
res.json(reason);
}); });
}); });

View File

@ -11,7 +11,7 @@ messageRouter.patch("/:messageId/readed", (req, res) => {
if (messageManager.markReaded(req.params.messageId)) { if (messageManager.markReaded(req.params.messageId)) {
res.json(messageManager.get()); res.json(messageManager.get());
} else { } else {
res.sendStatus(404); res.status(404).send();
} }
}); });

View File

@ -7,11 +7,8 @@ import {
} from "../services/webdavConfigService.js"; } from "../services/webdavConfigService.js";
import * as webdavService from "../services/webdavService.js"; import * as webdavService from "../services/webdavService.js";
import * as pathTools from "../tools/pathTools.js"; import * as pathTools from "../tools/pathTools.js";
import type { WebdavGenericPath } from "../types/services/webdav.js"; import type { WebdavDelete } from "../types/services/webdav.js";
import { WebdavDeleteValidation } from "../types/services/webdavValidation.js"; import { WebdavDeleteValidation } from "../types/services/webdavValidation.js";
import { restoreToHA } from "../services/orchestrator.js";
import path from "path";
import logger from "../config/winston.js";
const webdavRouter = express.Router(); const webdavRouter = express.Router();
@ -58,7 +55,7 @@ webdavRouter.get("/backup/manual", (req, res) => {
}); });
webdavRouter.delete("/", (req, res) => { webdavRouter.delete("/", (req, res) => {
const body = req.body as WebdavGenericPath; const body = req.body as WebdavDelete;
const validator = Joi.object(WebdavDeleteValidation); const validator = Joi.object(WebdavDeleteValidation);
const config = getWebdavConfig(); const config = getWebdavConfig();
validateWebdavConfig(config) validateWebdavConfig(config)
@ -83,21 +80,4 @@ webdavRouter.delete("/", (req, res) => {
}); });
}); });
webdavRouter.post("/restore", (req, res) => {
const body = req.body as WebdavGenericPath;
const validator = Joi.object(WebdavDeleteValidation);
validator
.validateAsync(body)
.then(() => {
return restoreToHA(body.path, path.basename(body.path));
})
.then(() => {
logger.info("All good !");
})
.catch(() => {
logger.error("Something wrong !");
});
res.sendStatus(202);
});
export default webdavRouter; export default webdavRouter;

View File

@ -4,6 +4,7 @@ import FormData from "form-data";
import got, { import got, {
RequestError, RequestError,
type OptionsOfJSONResponseBody, type OptionsOfJSONResponseBody,
type PlainResponse,
type Progress, type Progress,
type Response, type Response,
} from "got"; } from "got";
@ -161,7 +162,6 @@ function downloadSnapshot(id: string): Promise<string> {
} }
function delSnap(id: string) { function delSnap(id: string) {
logger.info(`Deleting Home Assistant backup ${id}`);
const option = { const option = {
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}; };
@ -295,42 +295,54 @@ function clean(backups: BackupModel[], numberToKeep: number) {
} }
function uploadSnapshot(path: string) { function uploadSnapshot(path: string) {
const status = statusTools.getStatus(); return new Promise((resolve, reject) => {
status.status = States.BKUP_UPLOAD_HA; const status = statusTools.getStatus();
status.progress = 0; status.status = States.BKUP_UPLOAD_HA;
statusTools.setStatus(status); status.progress = 0;
logger.info("Uploading backup..."); statusTools.setStatus(status);
const stream = fs.createReadStream(path); logger.info("Uploading backup...");
const form = new FormData(); const stream = fs.createReadStream(path);
form.append("file", stream); const form = new FormData();
form.append("file", stream);
const options = { const options = {
body: form, body: form,
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}; };
return got got.stream
.post(`http://hassio/backups/new/upload`, options) .post(`http://hassio/backups/new/upload`, options)
.on("uploadProgress", (e: Progress) => { .on("uploadProgress", (e: Progress) => {
const percent = e.percent; const percent = e.percent;
if (status.progress !== percent) { if (status.progress !== percent) {
status.progress = percent; status.progress = percent;
statusTools.setStatus(status); statusTools.setStatus(status);
} }
if (percent >= 1) { if (percent >= 1) {
logger.info("Upload done..."); logger.info("Upload done...");
} }
}) })
.then( .on("response", (res: PlainResponse) => {
(res) => { if (res.statusCode !== 200) {
logger.info(`...Upload finish ! (status: ${res.statusCode})`); messageManager.error(
const status = statusTools.getStatus(); "Fail to upload backup to Home Assistant",
status.status = States.IDLE; `Code: ${res.statusCode} Body: ${res.body as string}`
status.progress = undefined; );
statusTools.setStatus(status); logger.error("Fail to upload backup to Home Assistant");
fs.unlinkSync(path); logger.error(`Code: ${res.statusCode}`);
return res; logger.error(`Body: ${res.body as string}`);
}, fs.unlinkSync(path);
(err: RequestError) => { reject(new Error(res.statusCode.toString()));
} else {
logger.info(`...Upload finish ! (status: ${res.statusCode})`);
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
fs.unlinkSync(path);
resolve(res);
}
})
.on("error", (err: RequestError) => {
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.status = States.IDLE; status.status = States.IDLE;
status.progress = undefined; status.progress = undefined;
@ -342,9 +354,9 @@ function uploadSnapshot(path: string) {
); );
logger.error("Fail to upload backup to Home Assistant"); logger.error("Fail to upload backup to Home Assistant");
logger.error(err); logger.error(err);
logger.error(`Body: ${err.response?.body as string}`); reject(err);
} });
); });
} }
function stopAddons(addonSlugs: string[]) { function stopAddons(addonSlugs: string[]) {
@ -519,7 +531,6 @@ function publish_state() {
export { export {
clean, clean,
createNewBackup, createNewBackup,
delSnap,
downloadSnapshot, downloadSnapshot,
getAddonList, getAddonList,
getBackupInfo, getBackupInfo,

View File

@ -4,12 +4,7 @@ import logger from "../config/winston.js";
import messageManager from "../tools/messageManager.js"; import messageManager from "../tools/messageManager.js";
import * as statusTools from "../tools/status.js"; import * as statusTools from "../tools/status.js";
import { BackupType } from "../types/services/backupConfig.js"; import { BackupType } from "../types/services/backupConfig.js";
import type { import type { AddonModel } from "../types/services/ha_os_response.js";
AddonModel,
BackupDetailModel,
BackupUpload,
SupervisorResponse,
} from "../types/services/ha_os_response.js";
import { WorkflowType } from "../types/services/orchecstrator.js"; import { WorkflowType } from "../types/services/orchecstrator.js";
import * as backupConfigService from "./backupConfigService.js"; import * as backupConfigService from "./backupConfigService.js";
import * as homeAssistantService from "./homeAssistantService.js"; import * as homeAssistantService from "./homeAssistantService.js";
@ -94,10 +89,7 @@ export function doBackupWorkflow(type: WorkflowType) {
}) })
.then(() => { .then(() => {
logger.info("Backup workflow finished successfully !"); logger.info("Backup workflow finished successfully !");
messageManager.info( messageManager.info("Backup workflow finished successfully !");
"Backup workflow finished successfully !",
`name: ${name}`
);
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.last_backup.success = true; status.last_backup.success = true;
status.last_backup.last_try = DateTime.now(); status.last_backup.last_try = DateTime.now();
@ -113,55 +105,6 @@ export function doBackupWorkflow(type: WorkflowType) {
}); });
} }
export function uploadToCloud(slug: string) {
const webdavConfig = getWebdavConfig();
let tmpBackupFile = "";
let backupInfo = {} as BackupDetailModel;
return webDavService
.checkWebdavLogin(webdavConfig)
.then(() => {
return homeAssistantService.getBackupInfo(slug);
})
.then((response) => {
backupInfo = response.body.data;
return homeAssistantService.downloadSnapshot(slug);
})
.then((tmpFile) => {
tmpBackupFile = tmpFile;
if (webdavConfig.chunckedUpload) {
return webDavService.chunkedUpload(
tmpFile,
getBackupFolder(WorkflowType.MANUAL, webdavConfig) +
backupInfo.name +
".tar",
webdavConfig
);
} else {
return webDavService.webdavUploadFile(
tmpFile,
getBackupFolder(WorkflowType.MANUAL, webdavConfig) +
backupInfo.name +
".tar",
webdavConfig
);
}
})
.then(() => {
logger.info(`Successfully uploaded ${backupInfo.name} to cloud.`);
messageManager.info(
"Successfully uploaded backup to cloud.",
`Name: ${backupInfo.name}`
);
})
.catch(() => {
if (tmpBackupFile != "") {
unlinkSync(tmpBackupFile);
}
return Promise.reject(new Error());
});
}
// This methods remove addon that are no installed in HA from the conf array // This methods remove addon that are no installed in HA from the conf array
function sanitizeAddonList(addonInConf: string[], addonInHA: AddonModel[]) { function sanitizeAddonList(addonInConf: string[], addonInHA: AddonModel[]) {
return addonInConf.filter((value) => addonInHA.some((v) => v.slug == value)); return addonInConf.filter((value) => addonInHA.some((v) => v.slug == value));
@ -197,26 +140,3 @@ function backupFail() {
statusTools.setStatus(status); statusTools.setStatus(status);
messageManager.error("Last backup as failed !"); messageManager.error("Last backup as failed !");
} }
export function restoreToHA(webdavPath: string, filename: string) {
const webdavConfig = getWebdavConfig();
return webDavService
.checkWebdavLogin(webdavConfig)
.then(() => {
return webDavService.downloadFile(webdavPath, filename, webdavConfig);
})
.then((tmpFile) => {
return homeAssistantService.uploadSnapshot(tmpFile);
})
.then((res) => {
if (res) {
const body = JSON.parse(res.body) as SupervisorResponse<BackupUpload>;
logger.info(`Successfully uploaded ${filename} to Home Assistant.`);
messageManager.info(
"Successfully uploaded backup to Home Assistant.",
`Name: ${filename} slug: ${body.data.slug}
`
);
}
});
}

View File

@ -21,8 +21,6 @@ import type { WebdavConfig } from "../types/services/webdavConfig.js";
import { States } from "../types/status.js"; import { States } from "../types/status.js";
import { templateToRegexp } from "./backupConfigService.js"; import { templateToRegexp } from "./backupConfigService.js";
import { getChunkEndpoint, getEndpoint } from "./webdavConfigService.js"; import { getChunkEndpoint, getEndpoint } from "./webdavConfigService.js";
import { pipeline } from "stream/promises";
import { humanFileSize } from "../tools/toolbox.js";
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MiB Same as desktop client const CHUNK_SIZE = 5 * 1024 * 1024; // 5MiB Same as desktop client
const CHUNK_NUMBER_SIZE = 5; // To add landing "0" const CHUNK_NUMBER_SIZE = 5; // To add landing "0"
@ -454,7 +452,7 @@ export async function chunkedUpload(
} }
} }
function uploadChunk( export function uploadChunk(
url: string, url: string,
finalDestination: string, finalDestination: string,
body: fs.ReadStream, body: fs.ReadStream,
@ -499,7 +497,7 @@ function uploadChunk(
}); });
} }
function initChunkedUpload( export function initChunkedUpload(
url: string, url: string,
finalDestination: string, finalDestination: string,
config: WebdavConfig config: WebdavConfig
@ -519,7 +517,7 @@ function initChunkedUpload(
}); });
} }
function assembleChunkedUpload( export function assembleChunkedUpload(
url: string, url: string,
finalDestination: string, finalDestination: string,
totalLength: number, totalLength: number,
@ -541,63 +539,436 @@ function assembleChunkedUpload(
https: { rejectUnauthorized: !config.allowSelfSignedCerts }, https: { rejectUnauthorized: !config.allowSelfSignedCerts },
}); });
} }
// import fs from "fs";
// import got from "got";
// import https from "https";
// import path from "path";
// import stream from "stream";
// import { promisify } from "util";
// import { createClient, WebDAVClient } from "webdav";
export function downloadFile( // import { DateTime } from "luxon";
webdavPath: string, // import logger from "../config/winston.js";
filename: string, // import { WebdavSettings } from "../types/settings.js";
config: WebdavConfig // import * as pathTools from "../tools/pathTools.js";
) { // import * as settingsTools from "../tools/settingsTools.js";
logger.info(`Downloading ${webdavPath} from webdav...`); // import * as statusTools from "../tools/status.js";
if (!fs.existsSync("./temp/")) {
fs.mkdirSync("./temp/"); // const endpoint = "/remote.php/webdav";
} // const configPath = "/data/webdav_conf.json";
const tmp_file = `./temp/${filename}`;
const stream = fs.createWriteStream(tmp_file); // const pipeline = promisify(stream.pipeline);
const options = {
headers: { // class WebdavTools {
authorization: // host: string | undefined;
"Basic " + // client: WebDAVClient | undefined;
Buffer.from(config.username + ":" + config.password).toString("base64"), // baseUrl: string | undefined;
}, // username: string | undefined;
https: { rejectUnauthorized: !config.allowSelfSignedCerts }, // password: string | undefined;
};
const url = config.url + getEndpoint(config) + webdavPath; // init(
logger.debug(`...URI: ${encodeURI(url)}`); // ssl: boolean,
logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`); // host: string,
const status = statusTools.getStatus(); // username: string,
status.status = States.BKUP_DOWNLOAD_CLOUD; // password: string,
status.progress = 0; // accept_selfsigned_cert: boolean
statusTools.setStatus(status); // ) {
return pipeline( // return new Promise((resolve, reject) => {
got.stream.get(encodeURI(url), options).on("downloadProgress", (e) => { // this.host = host;
const percent = Math.round(e.percent * 100) / 100; // const status = statusTools.getStatus();
if (status.progress !== percent) { // logger.info("Initializing and checking webdav client...");
status.progress = percent; // this.baseUrl = (ssl ? "https" : "http") + "://" + host + endpoint;
statusTools.setStatus(status); // this.username = username;
} // this.password = password;
}), // const agent_option = ssl
stream // ? { rejectUnauthorized: !accept_selfsigned_cert }
).then( // : {};
() => { // try {
logger.info("Download success !"); // this.client = createClient(this.baseUrl, {
status.progress = 1; // username: username,
statusTools.setStatus(status); // password: password,
logger.debug( // httpsAgent: new https.Agent(agent_option),
`Backup dl size: ${humanFileSize(fs.statSync(tmp_file).size)}` // });
);
return tmp_file; // this.client
}, // .getDirectoryContents("/")
(reason: RequestError) => { // .then(() => {
if (fs.existsSync(tmp_file)) fs.unlinkSync(tmp_file); // if (status.error_code === 3) {
messageManager.error("Fail to download Cloud backup", reason.message); // status.status = "idle";
const status = statusTools.getStatus(); // status.message = undefined;
status.status = States.IDLE; // status.error_code = undefined;
status.progress = undefined; // statusTools.setStatus(status);
statusTools.setStatus(status); // }
return Promise.reject(reason); // logger.debug("Nextcloud connection: \x1b[32mSuccess !\x1b[0m");
} // this.initFolder().then(() => {
); // resolve(undefined);
} // });
// })
// .catch((error) => {
// this.__cant_connect_status(error);
// this.client = undefined;
// reject("Can't connect to Nextcloud (" + error + ") !");
// });
// } catch (err) {
// this.__cant_connect_status(err);
// this.client = undefined;
// reject("Can't connect to Nextcloud (" + err + ") !");
// }
// });
// }
// __cant_connect_status(err: any) {
// statusTools.setError(`Can't connect to Nextcloud (${err})`, 3);
// }
// async __createRoot() {
// const conf = this.getConf();
// if (this.client && conf) {
// const root_splited = conf.back_dir.split("/").splice(1);
// let path = "/";
// for (const elem of root_splited) {
// if (elem !== "") {
// path = path + elem + "/";
// try {
// await this.client.createDirectory(path);
// logger.debug(`Path ${path} created.`);
// } catch (error) {
// if ((error as any).status === 405)
// logger.debug(`Path ${path} already exist.`);
// else logger.error(error);
// }
// }
// }
// }
// }
// initFolder() {
// return new Promise((resolve, reject) => {
// this.__createRoot()
// .catch((err) => {
// logger.error(err);
// })
// .then(() => {
// if (!this.client) {
// return;
// }
// this.client
// .createDirectory(this.getConf()?.back_dir + pathTools.auto)
// .catch(() => {
// // Ignore
// })
// .then(() => {
// if (!this.client) {
// return;
// }
// this.client
// .createDirectory(this.getConf()?.back_dir + pathTools.manual)
// .catch(() => {
// // Ignore
// })
// .then(() => {
// resolve(undefined);
// });
// });
// });
// });
// }
// /**
// * Check if theh webdav config is valid, if yes, start init of webdav client
// */
// confIsValid() {
// return new Promise((resolve, reject) => {
// const status = statusTools.getStatus();
// const conf = this.getConf();
// if (conf != undefined) {
// if (
// conf.ssl != undefined &&
// conf.host != undefined &&
// conf.username != undefined &&
// conf.password != undefined
// ) {
// if (status.error_code === 2) {
// status.status = "idle";
// status.message = undefined;
// status.error_code = undefined;
// statusTools.setStatus(status);
// }
// // Check if self_signed option exist
// if (conf.self_signed == undefined) {
// conf.self_signed = false;
// this.setConf(conf);
// }
// this.init(
// conf.ssl,
// conf.host,
// conf.username,
// conf.password,
// conf.self_signed
// )
// .then(() => {
// resolve(undefined);
// })
// .catch((err) => {
// reject(err);
// });
// } else {
// status.status = "error";
// status.error_code = 2;
// status.message = "Nextcloud config invalid !";
// statusTools.setStatus(status);
// logger.error(status.message);
// reject("Nextcloud config invalid !");
// }
// if (conf.back_dir == null || conf.back_dir === "") {
// logger.info("Backup dir is null, initializing it.");
// conf.back_dir = pathTools.default_root;
// this.setConf(conf);
// } else {
// if (!conf.back_dir.startsWith("/")) {
// logger.warn("Backup dir not starting with '/', fixing this...");
// conf.back_dir = `/${conf.back_dir}`;
// this.setConf(conf);
// }
// if (!conf.back_dir.endsWith("/")) {
// logger.warn("Backup dir not ending with '/', fixing this...");
// conf.back_dir = `${conf.back_dir}/`;
// this.setConf(conf);
// }
// }
// } else {
// status.status = "error";
// status.error_code = 2;
// status.message = "Nextcloud config not found !";
// statusTools.setStatus(status);
// logger.error(status.message);
// reject("Nextcloud config not found !");
// }
// });
// }
// getConf(): WebdavSettings | undefined {
// if (fs.existsSync(configPath)) {
// return JSON.parse(fs.readFileSync(configPath).toString());
// } else return undefined;
// }
// setConf(conf: WebdavSettings) {
// fs.writeFileSync(configPath, JSON.stringify(conf));
// }
// uploadFile(id: string, path: string) {
// return new Promise((resolve, reject) => {
// if (this.client == null) {
// this.confIsValid()
// .then(() => {
// this._startUpload(id, path)
// .then(() => resolve(undefined))
// .catch((err) => reject(err));
// })
// .catch((err) => {
// reject(err);
// });
// } else
// this._startUpload(id, path)
// .then(() => resolve(undefined))
// .catch((err) => reject(err));
// });
// }
// _startUpload(id: string, path: string) {
// return new Promise((resolve, reject) => {
// const status = statusTools.getStatus();
// status.status = "upload";
// status.progress = 0;
// status.message = undefined;
// status.error_code = undefined;
// statusTools.setStatus(status);
// logger.info("Uploading snap...");
// const tmpFile = `./temp/${id}.tar`;
// const stats = fs.statSync(tmpFile);
// const stream = fs.createReadStream(tmpFile);
// const conf = this.getConf();
// const options = {
// body: stream,
// headers: {
// authorization:
// "Basic " +
// Buffer.from(this.username + ":" + this.password).toString("base64"),
// "content-length": String(stats.size),
// },
// https: undefined as any | undefined,
// };
// if (conf?.ssl) {
// options["https"] = { rejectUnauthorized: !conf.self_signed };
// }
// logger.debug(
// `...URI: ${encodeURI(
// this.baseUrl?.replace(this.host as string, "host.hiden") + path
// )}`
// );
// if (conf?.ssl)
// logger.debug(
// `...rejectUnauthorized: ${options.https?.rejectUnauthorized}`
// );
// got.stream
// .put(encodeURI(this.baseUrl + path), options)
// .on("uploadProgress", (e) => {
// const percent = e.percent;
// if (status.progress !== percent) {
// status.progress = percent;
// statusTools.setStatus(status);
// }
// if (percent >= 1) {
// logger.info("Upload done...");
// }
// })
// .on("response", (res) => {
// if (res.statusCode !== 201 && res.statusCode !== 204) {
// status.status = "error";
// status.error_code = 4;
// status.message = `Fail to upload snapshot to nextcloud (Status code: ${res.statusCode})!`;
// statusTools.setStatus(status);
// logger.error(status.message);
// fs.unlinkSync(tmpFile);
// reject(status.message);
// } else {
// logger.info(`...Upload finish ! (status: ${res.statusCode})`);
// status.status = "idle";
// status.progress = -1;
// status.message = undefined;
// status.error_code = undefined;
// status.last_backup = DateTime.now().toFormat("dd MMM yyyy, HH:mm");
// statusTools.setStatus(status);
// cleanTempFolder();
// const autoCleanCloud =
// settingsTools.getSettings().auto_clean_backup;
// if (autoCleanCloud != null && autoCleanCloud === "true") {
// this.clean().catch();
// }
// const autoCleanlocal = settingsTools.getSettings().auto_clean_local;
// if (autoCleanlocal != null && autoCleanlocal === "true") {
// hassioApiTools.clean().catch();
// }
// resolve(undefined);
// }
// })
// .on("error", (err) => {
// fs.unlinkSync(tmpFile);
// status.status = "error";
// status.error_code = 4;
// status.message = `Fail to upload snapshot to nextcloud (${err}) !`;
// statusTools.setStatus(status);
// logger.error(status.message);
// logger.error(err.stack);
// reject(status.message);
// });
// });
// }
// downloadFile(path: string) {
// return new Promise<string>((resolve, reject) => {
// if (this.client == null) {
// this.confIsValid()
// .then(() => {
// this._startDownload(path)
// .then((path) => resolve(path))
// .catch(() => reject());
// })
// .catch((err) => {
// reject(err);
// });
// } else
// this._startDownload(path)
// .then((path) => resolve(path))
// .catch(() => reject());
// });
// }
// _startDownload(path: string) {
// return new Promise<string>((resolve, reject) => {
// const status = statusTools.getStatus();
// status.status = "download-b";
// status.progress = 0;
// status.message = undefined;
// status.error_code = undefined;
// statusTools.setStatus(status);
// logger.info("Downloading backup...");
// if (!fs.existsSync("./temp/")) fs.mkdirSync("./temp/");
// const tmpFile = `./temp/restore_${DateTime.now().toFormat(
// "MMM-dd-yyyy_HH_mm"
// )}.tar`;
// const stream = fs.createWriteStream(tmpFile);
// const conf = this.getConf();
// const options = {
// headers: {
// authorization:
// "Basic " +
// Buffer.from(this.username + ":" + this.password).toString("base64"),
// },
// https: undefined as any | undefined,
// };
// if (conf?.ssl) {
// options["https"] = { rejectUnauthorized: !conf?.self_signed };
// }
// logger.debug(
// `...URI: ${encodeURI(
// this.baseUrl?.replace(this.host as string, "host.hiden") + path
// )}`
// );
// if (conf?.ssl)
// logger.debug(
// `...rejectUnauthorized: ${options.https?.rejectUnauthorized}`
// );
// pipeline(
// got.stream
// .get(encodeURI(this.baseUrl + path), options)
// .on("downloadProgress", (e) => {
// const percent = Math.round(e.percent * 100) / 100;
// if (status.progress !== percent) {
// status.progress = percent;
// statusTools.setStatus(status);
// }
// }),
// stream
// )
// .then((res) => {
// logger.info("Download success !");
// status.progress = 1;
// statusTools.setStatus(status);
// logger.debug(
// "Backup dl size : " + fs.statSync(tmpFile).size / 1024 / 1024
// );
// resolve(tmpFile);
// })
// .catch((err) => {
// if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
// status.status = "error";
// status.message =
// "Fail to download Hassio snapshot (" + err.message + ")";
// status.error_code = 7;
// statusTools.setStatus(status);
// logger.error(status.message);
// logger.error(err.stack);
// reject(err.message);
// });
// });
// }
// getFolderContent(path: string) {
// return new Promise((resolve, reject) => {
// if (this.client == null) {
// reject();
// return;
// }
// this.client
// .getDirectoryContents(path)
// .then((contents) => resolve(contents))
// .catch((error) => reject(error));
// });
// }
// clean() { // clean() {
// let limit = settingsTools.getSettings().auto_clean_backup_keep; // let limit = settingsTools.getSettings().auto_clean_backup_keep;

View File

@ -1,3 +1,4 @@
export interface SupervisorResponse<T> { export interface SupervisorResponse<T> {
result: string; result: string;
data: T; data: T;
@ -24,6 +25,7 @@ export interface AddonData {
addons: AddonModel[]; addons: AddonModel[];
} }
export interface AddonModel { export interface AddonModel {
name: string; name: string;
slug: string; slug: string;
@ -41,9 +43,10 @@ export interface AddonModel {
} }
export interface BackupData { export interface BackupData {
backups: BackupModel[]; backups: BackupModel[]
} }
export interface BackupModel { export interface BackupModel {
slug: string; slug: string;
date: string; date: string;
@ -77,8 +80,3 @@ export interface BackupDetailModel {
repositories: string[]; repositories: string[];
folders: string[]; folders: string[];
} }
export interface BackupUpload {
slug: string;
job_id: string;
}

View File

@ -10,6 +10,7 @@ export interface WebdavBackup {
creationDate?: DateTime; creationDate?: DateTime;
} }
export interface WebdavGenericPath {
export interface WebdavDelete {
path: string; path: string;
} }

View File

@ -22,7 +22,6 @@ declare module 'vue' {
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'] ConnectionStatus: typeof import('./src/components/statusBar/ConnectionStatus.vue')['default']
HaDeleteDialog: typeof import('./src/components/homeAssistant/HaDeleteDialog.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']

View File

@ -53,10 +53,8 @@ const alertVisible = computed(() => alertList.value.length > 0);
right: 20px; right: 20px;
z-index: 99999; z-index: 99999;
height: 100vh; height: 100vh;
pointer-events: none;
#alertContainer { #alertContainer {
position: sticky; position: sticky;
pointer-events: all;
top: 80px; top: 80px;
right: 20px; right: 20px;
} }

View File

@ -39,7 +39,6 @@
:item="item" :item="item"
:index="index" :index="index"
@delete="deleteBackup" @delete="deleteBackup"
@upload="restore"
> >
</cloud-list-item> </cloud-list-item>
</v-list> </v-list>
@ -68,7 +67,6 @@
:item="item" :item="item"
:index="index" :index="index"
@delete="deleteBackup" @delete="deleteBackup"
@upload="restore"
> >
</cloud-list-item> </cloud-list-item>
</v-list> </v-list>
@ -91,11 +89,9 @@ import type { WebdavBackup } from "@/types/webdav";
import { import {
getAutoBackupList, getAutoBackupList,
getManualBackupList, getManualBackupList,
restoreWebdavBackup,
} from "@/services/webdavService"; } from "@/services/webdavService";
import CloudDeleteDialog from "./CloudDeleteDialog.vue"; import CloudDeleteDialog from "./CloudDeleteDialog.vue";
import CloudListItem from "./CloudListItem.vue"; import CloudListItem from "./CloudListItem.vue";
import { useAlertStore } from "@/store/alert";
const deleteDialog = ref<InstanceType<typeof CloudDeleteDialog> | null>(null); const deleteDialog = ref<InstanceType<typeof CloudDeleteDialog> | null>(null);
const deleteItem = ref<WebdavBackup | null>(null); const deleteItem = ref<WebdavBackup | null>(null);
@ -103,7 +99,7 @@ const autoBackups = ref<WebdavBackup[]>([]);
const manualBackups = ref<WebdavBackup[]>([]); const manualBackups = ref<WebdavBackup[]>([]);
const loading = ref<boolean>(true); const loading = ref<boolean>(true);
const alertStore = useAlertStore();
function refreshBackup() { function refreshBackup() {
loading.value = true; loading.value = true;
getAutoBackupList() getAutoBackupList()
@ -124,16 +120,6 @@ function deleteBackup(item: WebdavBackup) {
deleteItem.value = item; deleteItem.value = item;
deleteDialog.value?.open(item); deleteDialog.value?.open(item);
} }
function restore(item: WebdavBackup) {
restoreWebdavBackup(item.path)
.then(() => {
alertStore.add("success", "Backup upload as started.");
})
.catch(() => {
alertStore.add("error", "Fail to start backup upload !");
});
}
refreshBackup(); refreshBackup();
defineExpose({ refreshBackup }); defineExpose({ refreshBackup });
</script> </script>

View File

@ -94,12 +94,7 @@
<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 }">
<v-btn <v-btn variant="outlined" color="success" v-bind="props">
variant="outlined"
color="success"
v-bind="props"
@click="emits('upload', item)"
>
<v-icon>mdi-upload</v-icon> <v-icon>mdi-upload</v-icon>
</v-btn> </v-btn>
</template> </template>
@ -113,20 +108,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { restoreWebdavBackup } from "@/services/webdavService";
import type { WebdavBackup } from "@/types/webdav"; import type { WebdavBackup } from "@/types/webdav";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { ref } from "vue"; import { ref } from "vue";
const detail = ref(false); const detail = ref(false);
const props = defineProps<{ defineProps<{
item: WebdavBackup; item: WebdavBackup;
index: number; index: number;
}>(); }>();
const emits = defineEmits<{ const emits = defineEmits<{
(e: "delete", item: WebdavBackup): void; (e: "delete", item: WebdavBackup): void;
(e: "upload", item: WebdavBackup): void;
}>(); }>();
</script> </script>

View File

@ -1,74 +0,0 @@
<template>
<div>
<v-dialog
v-model="dialog"
:width="width"
:fullscreen="isFullScreen"
:persistent="loading"
>
<v-card>
<v-card-title class="d-flex align-center">
<v-icon color="red" class="mr-2">mdi-trash-can</v-icon> Delete Home
Assistant backup
</v-card-title>
<v-divider></v-divider>
<v-card-text>
Delete <v-code tag="code">{{ item?.name }}</v-code> backup in Home
Assistant ?
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="justify-end">
<v-btn color="success" @click="dialog = false" :disabled="loading">
Close
</v-btn>
<v-btn color="red" @click="confirm()" :loading="loading">
<v-icon>mdi-trash-can</v-icon> Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
import { useMenuSize } from "@/composable/menuSize";
import { deleteHomeAssistantBackup } from "@/services/homeAssistantService";
import { deleteWebdabBackup } from "@/services/webdavService";
import { useAlertStore } from "@/store/alert";
import { BackupModel } from "@/types/homeAssistant";
import { ref } from "vue";
const dialog = ref(false);
const loading = ref(false);
const item = ref<BackupModel | null>(null);
const { width, isFullScreen } = useMenuSize();
const emit = defineEmits<{
(e: "deleted"): void;
}>();
const alertStore = useAlertStore();
function confirm() {
loading.value = true;
if (item.value) {
deleteHomeAssistantBackup(item.value.slug)
.then(() => {
loading.value = false;
dialog.value = false;
alertStore.add("success", "Backup deleted from Home Assistant");
emit("deleted");
})
.catch(() => {
loading.value = false;
alertStore.add("error", "Fail to deleted backup from Home Assistant");
});
}
}
function open(value: BackupModel) {
item.value = value;
dialog.value = true;
}
defineExpose({ open });
</script>

View File

@ -37,8 +37,6 @@
:key="item.slug" :key="item.slug"
:item="item" :item="item"
:index="index" :index="index"
@upload="upload"
@delete="deleteBackup"
> >
</ha-list-item> </ha-list-item>
</v-list> </v-list>
@ -49,32 +47,17 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
</div> </div>
<ha-delete-dialog
ref="deleteDialog"
@deleted="refreshBackup"
></ha-delete-dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { BackupModel } from "@/types/homeAssistant"; import type { BackupModel } from "@/types/homeAssistant";
import { ref, onBeforeUnmount } from "vue"; import { ref, onBeforeUnmount } from "vue";
import { import { getBackups } from "@/services/homeAssistantService";
getBackups,
uploadHomeAssistantBackup,
} from "@/services/homeAssistantService";
import HaListItem from "./HaListItem.vue"; import HaListItem from "./HaListItem.vue";
import { useAlertStore } from "@/store/alert";
import HaDeleteDialog from "./HaDeleteDialog.vue";
const deleteDialog = ref<InstanceType<typeof HaDeleteDialog> | null>(null);
const backups = ref<BackupModel[]>([]); const backups = ref<BackupModel[]>([]);
const deleteItem = ref<BackupModel | null>(null);
const loading = ref<boolean>(true); const loading = ref<boolean>(true);
const alertStore = useAlertStore();
function refreshBackup() { function refreshBackup() {
loading.value = true; loading.value = true;
getBackups() getBackups()
@ -87,22 +70,9 @@ function refreshBackup() {
}); });
} }
function upload(item: BackupModel) {
uploadHomeAssistantBackup(item.slug)
.then(() => {
alertStore.add("success", "Backup upload as started.");
})
.catch(() => {
alertStore.add("error", "Fail to start backup upload !");
});
}
function deleteBackup(item: BackupModel) {
deleteItem.value = item;
deleteDialog.value?.open(item);
}
refreshBackup(); refreshBackup();
defineExpose({ refreshBackup }); defineExpose({ refreshBackup });
// TODO Manage delete
</script> </script>

View File

@ -156,12 +156,7 @@
<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 }">
<v-btn <v-btn variant="outlined" color="success" v-bind="props">
variant="outlined"
color="success"
v-bind="props"
@click="emits('upload', item)"
>
<v-icon>mdi-cloud-upload</v-icon> <v-icon>mdi-cloud-upload</v-icon>
</v-btn> </v-btn>
</template> </template>
@ -198,6 +193,5 @@ watch(detail, (value) => {
const emits = defineEmits<{ const emits = defineEmits<{
(e: "delete", item: BackupModel): void; (e: "delete", item: BackupModel): void;
(e: "upload", item: BackupModel): void;
}>(); }>();
</script> </script>

View File

@ -129,3 +129,4 @@ const { save, loading } = useConfigForm(
); );
defineExpose({ save }); defineExpose({ save });
</script> </script>
@/store/backupConfig

View File

@ -21,11 +21,3 @@ export function getBackups() {
export function getBackupDetail(slug: string) { export function getBackupDetail(slug: string) {
return kyClient.get(`homeAssistant/backup/${slug}`).json<BackupDetailModel>(); return kyClient.get(`homeAssistant/backup/${slug}`).json<BackupDetailModel>();
} }
export function uploadHomeAssistantBackup(slug: string) {
return kyClient.post(`homeAssistant/backup/${slug}/upload`);
}
export function deleteHomeAssistantBackup(slug: string) {
return kyClient.delete(`homeAssistant/backup/${slug}`);
}

View File

@ -18,13 +18,3 @@ export function deleteWebdabBackup(path: string) {
}) })
.text(); .text();
} }
export function restoreWebdavBackup(path: string) {
return kyClient
.post("webdav/restore", {
json: {
path: path,
},
})
.text();
}