hassio-nextcloud-backup/nextcloud_backup/backend/src/services/homeAssistantService.ts

544 lines
16 KiB
TypeScript
Raw Normal View History

2022-09-26 23:11:43 +02:00
import fs from "fs";
import FormData from "form-data";
2024-04-18 10:47:21 +02:00
import got, {
RequestError,
type OptionsOfJSONResponseBody,
type PlainResponse,
2024-07-11 15:47:27 +02:00
type Progress,
2024-04-18 10:47:21 +02:00
type Response,
} from "got";
2024-07-11 15:47:27 +02:00
import { DateTime } from "luxon";
2022-09-26 23:11:43 +02:00
import stream from "stream";
import { promisify } from "util";
import logger from "../config/winston.js";
import messageManager from "../tools/messageManager.js";
import * as statusTools from "../tools/status.js";
2024-07-11 15:47:27 +02:00
import { BackupType } from "../types/services/backupConfig.js";
import type { NewBackupPayload } from "../types/services/ha_os_payload.js";
import type {
2022-09-27 23:38:40 +02:00
AddonData,
BackupData,
2022-09-26 23:11:43 +02:00
BackupDetailModel,
BackupModel,
2022-09-27 23:38:40 +02:00
CoreInfoBody,
SupervisorResponse,
2022-09-26 23:11:43 +02:00
} from "../types/services/ha_os_response.js";
2024-07-11 15:47:27 +02:00
import { States } from "../types/status.js";
2022-09-26 23:11:43 +02:00
const pipeline = promisify(stream.pipeline);
const token = process.env.SUPERVISOR_TOKEN;
// Default timeout to 90min
const create_snap_timeout = process.env.CREATE_BACKUP_TIMEOUT
? parseInt(process.env.CREATE_BACKUP_TIMEOUT)
: 90 * 60 * 1000;
2022-09-27 23:38:40 +02:00
function getVersion(): Promise<Response<SupervisorResponse<CoreInfoBody>>> {
return got<SupervisorResponse<CoreInfoBody>>("http://hassio/core/info", {
2022-09-26 23:11:43 +02:00
headers: { authorization: `Bearer ${token}` },
responseType: "json",
}).then(
(result) => {
return result;
},
2024-07-11 15:47:27 +02:00
(error: Error) => {
2022-09-26 23:11:43 +02:00
messageManager.error(
"Fail to fetch Home Assistant version",
error?.message
);
logger.error(`Fail to fetch Home Assistant version`);
logger.error(error);
2022-09-27 23:38:40 +02:00
return Promise.reject(error);
2022-09-26 23:11:43 +02:00
}
);
}
2022-09-27 23:38:40 +02:00
function getAddonList(): Promise<Response<SupervisorResponse<AddonData>>> {
2022-09-26 23:11:43 +02:00
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
responseType: "json",
};
2022-09-27 23:38:40 +02:00
return got<SupervisorResponse<AddonData>>(
"http://hassio/addons",
option
).then(
2022-09-26 23:11:43 +02:00
(result) => {
2022-09-27 23:38:40 +02:00
result.body.data.addons.sort((a, b) => {
2022-09-26 23:11:43 +02:00
const textA = a.name.toUpperCase();
const textB = b.name.toUpperCase();
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
return result;
},
2024-07-11 15:47:27 +02:00
(error: Error) => {
2022-09-26 23:11:43 +02:00
messageManager.error("Fail to fetch addons list", error?.message);
logger.error(`Fail to fetch addons list (${error?.message})`);
logger.error(error);
2022-09-27 23:38:40 +02:00
return Promise.reject(error);
2022-09-26 23:11:43 +02:00
}
);
}
2022-09-27 23:38:40 +02:00
function getBackups(): Promise<Response<SupervisorResponse<BackupData>>> {
2022-09-26 23:11:43 +02:00
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
responseType: "json",
};
2022-09-27 23:38:40 +02:00
return got<SupervisorResponse<BackupData>>(
"http://hassio/backups",
option
).then(
2022-09-26 23:11:43 +02:00
(result) => {
2024-04-18 13:41:52 +02:00
const status = statusTools.getStatus();
status.hass.ok = true;
status.hass.last_check = DateTime.now();
statusTools.setStatus(status);
2022-09-26 23:11:43 +02:00
return result;
},
2024-07-11 15:47:27 +02:00
(error: Error) => {
2024-04-18 13:41:52 +02:00
const status = statusTools.getStatus();
status.hass.ok = false;
status.hass.last_check = DateTime.now();
statusTools.setStatus(status);
2022-09-26 23:11:43 +02:00
messageManager.error("Fail to fetch Hassio backups", error?.message);
2022-09-27 23:38:40 +02:00
return Promise.reject(error);
2022-09-26 23:11:43 +02:00
}
);
}
function downloadSnapshot(id: string): Promise<string> {
logger.info(`Downloading snapshot ${id}...`);
if (!fs.existsSync("./temp/")) {
fs.mkdirSync("./temp/");
}
const tmp_file = `./temp/${id}.tar`;
const stream = fs.createWriteStream(tmp_file);
const status = statusTools.getStatus();
2024-04-18 10:47:21 +02:00
status.status = States.BKUP_DOWNLOAD_HA;
2022-09-26 23:11:43 +02:00
status.progress = 0;
statusTools.setStatus(status);
const option = {
headers: { Authorization: `Bearer ${token}` },
};
return pipeline(
got.stream
.get(`http://hassio/backups/${id}/download`, option)
2024-07-11 15:47:27 +02:00
.on("downloadProgress", (e: Progress) => {
2022-09-26 23:11:43 +02:00
const percent = Math.round(e.percent * 100) / 100;
if (status.progress !== percent) {
status.progress = percent;
statusTools.setStatus(status);
}
}),
stream
).then(
() => {
logger.info("Download success !");
2024-04-18 10:47:21 +02:00
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
2022-09-26 23:11:43 +02:00
statusTools.setStatus(status);
logger.debug(
"Snapshot dl size : " + fs.statSync(tmp_file).size / 1024 / 1024
);
return tmp_file;
},
2024-07-11 15:47:27 +02:00
(reason: Error) => {
2022-09-26 23:11:43 +02:00
fs.unlinkSync(tmp_file);
messageManager.error(
"Fail to download Home Assistant backup",
reason.message
);
2024-04-18 10:47:21 +02:00
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
2022-09-27 23:38:40 +02:00
return Promise.reject(reason);
2022-09-26 23:11:43 +02:00
}
);
}
2022-09-27 23:38:40 +02:00
function delSnap(id: string) {
2022-09-26 23:11:43 +02:00
const option = {
headers: { authorization: `Bearer ${token}` },
};
return got.delete(`http://hassio/backups/${id}`, option).then(
(result) => {
return result;
},
2024-07-11 15:47:27 +02:00
(reason: RequestError) => {
2022-09-26 23:11:43 +02:00
messageManager.error(
"Fail to delete Homme assistant backup detail.",
reason.message
);
logger.error("Fail to retrive Homme assistant backup detail.");
logger.error(reason);
2022-09-27 23:38:40 +02:00
return Promise.reject(reason);
2022-09-26 23:11:43 +02:00
}
);
}
function getBackupInfo(id: string) {
2022-09-26 23:11:43 +02:00
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
responseType: "json",
};
2022-09-27 23:38:40 +02:00
return got<SupervisorResponse<BackupDetailModel>>(
2022-09-26 23:11:43 +02:00
`http://hassio/backups/${id}/info`,
option
).then(
(result) => {
logger.info("Backup found !");
2022-09-27 23:38:40 +02:00
logger.debug(`Backup size: ${result.body.data.size}`);
2022-09-26 23:11:43 +02:00
return result;
},
2024-07-11 15:47:27 +02:00
(reason: RequestError) => {
2022-09-26 23:11:43 +02:00
messageManager.error(
"Fail to retrive Homme assistant backup detail.",
reason.message
);
logger.error("Fail to retrive Homme assistant backup detail");
logger.error(reason);
2022-09-27 23:38:40 +02:00
return Promise.reject(reason);
2022-09-26 23:11:43 +02:00
}
);
}
function createNewBackup(
name: string,
type: BackupType,
passwordEnable: boolean,
password?: string,
addonSlugs?: string[],
folders?: string[]
2022-09-27 23:38:40 +02:00
) {
2022-09-26 23:11:43 +02:00
const status = statusTools.getStatus();
2024-04-18 10:47:21 +02:00
status.status = States.BKUP_CREATION;
2022-09-26 23:11:43 +02:00
status.progress = -1;
statusTools.setStatus(status);
logger.info("Creating new snapshot...");
const body: NewBackupPayload = {
2022-09-26 23:11:43 +02:00
name: name,
password: passwordEnable ? password : undefined,
addons: type == BackupType.PARTIAL ? addonSlugs : undefined,
folders: type == BackupType.PARTIAL ? folders : undefined,
2022-09-26 23:11:43 +02:00
};
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
responseType: "json",
timeout: {
response: create_snap_timeout,
},
json: body,
};
const url =
type == BackupType.PARTIAL
? "http://hassio/backups/new/partial"
: "http://hassio/backups/new/full";
return got.post<SupervisorResponse<{ slug: string }>>(url, option).then(
(result) => {
logger.info(`Snapshot created with id ${result.body.data.slug}`);
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
return result;
},
2024-07-11 15:47:27 +02:00
(reason: RequestError) => {
messageManager.error("Fail to create new backup.", reason.message);
logger.error("Fail to create new backup");
logger.error(reason);
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
return Promise.reject(reason);
}
);
2022-09-26 23:11:43 +02:00
}
2024-07-11 15:47:27 +02:00
function clean(backups: BackupModel[], numberToKeep: number) {
2022-09-26 23:11:43 +02:00
const promises = [];
2024-07-11 15:47:27 +02:00
if (backups.length < numberToKeep) {
2022-09-26 23:11:43 +02:00
return;
}
backups.sort((a, b) => {
return Date.parse(b.date) - Date.parse(a.date);
2022-09-26 23:11:43 +02:00
});
2024-07-11 15:47:27 +02:00
const toDel = backups.slice(numberToKeep);
2022-09-26 23:11:43 +02:00
for (const i of toDel) {
promises.push(delSnap(i.slug));
}
logger.info("Local clean done.");
return Promise.allSettled(promises).then((values) => {
let errors = false;
for (const val of values) {
if (val.status == "rejected") {
2024-07-11 15:47:27 +02:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2022-09-26 23:11:43 +02:00
messageManager.error("Fail to delete backup", val.reason);
logger.error("Fail to delete backup");
logger.error(val.reason);
errors = true;
}
}
if (errors) {
messageManager.error("Fail to clean backups in Home Assistant");
logger.error("Fail to clean backups in Home Assistant");
2024-07-11 15:47:27 +02:00
return Promise.reject(new Error());
2022-09-26 23:11:43 +02:00
}
});
}
function uploadSnapshot(path: string) {
return new Promise((resolve, reject) => {
const status = statusTools.getStatus();
2024-04-18 10:47:21 +02:00
status.status = States.BKUP_UPLOAD_HA;
2022-09-26 23:11:43 +02:00
status.progress = 0;
statusTools.setStatus(status);
logger.info("Uploading backup...");
const stream = fs.createReadStream(path);
const form = new FormData();
form.append("file", stream);
const options = {
body: form,
headers: { authorization: `Bearer ${token}` },
};
got.stream
.post(`http://hassio/backups/new/upload`, options)
2024-07-11 15:47:27 +02:00
.on("uploadProgress", (e: Progress) => {
2022-09-26 23:11:43 +02:00
const percent = e.percent;
if (status.progress !== percent) {
status.progress = percent;
statusTools.setStatus(status);
}
if (percent >= 1) {
logger.info("Upload done...");
}
})
2024-04-18 10:47:21 +02:00
.on("response", (res: PlainResponse) => {
2022-09-26 23:11:43 +02:00
if (res.statusCode !== 200) {
2024-04-18 10:47:21 +02:00
messageManager.error(
"Fail to upload backup to Home Assistant",
2024-07-11 15:47:27 +02:00
`Code: ${res.statusCode} Body: ${res.body as string}`
2024-04-18 10:47:21 +02:00
);
logger.error("Fail to upload backup to Home Assistant");
logger.error(`Code: ${res.statusCode}`);
2024-07-11 15:47:27 +02:00
logger.error(`Body: ${res.body as string}`);
2022-09-26 23:11:43 +02:00
fs.unlinkSync(path);
2024-07-11 15:47:27 +02:00
reject(new Error(res.statusCode.toString()));
2022-09-26 23:11:43 +02:00
} else {
logger.info(`...Upload finish ! (status: ${res.statusCode})`);
2024-04-18 10:47:21 +02:00
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
2022-09-26 23:11:43 +02:00
fs.unlinkSync(path);
resolve(res);
}
})
2024-04-18 10:47:21 +02:00
.on("error", (err: RequestError) => {
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
2022-09-26 23:11:43 +02:00
fs.unlinkSync(path);
messageManager.error(
"Fail to upload backup to Home Assistant",
2024-04-18 10:47:21 +02:00
err.message
2022-09-26 23:11:43 +02:00
);
logger.error("Fail to upload backup to Home Assistant");
logger.error(err);
reject(err);
});
});
}
function stopAddons(addonSlugs: string[]) {
logger.info("Stopping addons...");
2024-04-18 10:47:21 +02:00
const status = statusTools.getStatus();
status.status = States.STOP_ADDON;
status.progress = -1;
statusTools.setStatus(status);
2022-09-26 23:11:43 +02:00
const promises = [];
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
responseType: "json",
};
for (const addon of addonSlugs) {
if (addon !== "") {
logger.debug(`... Stopping addon ${addon}`);
promises.push(got.post(`http://hassio/addons/${addon}/stop`, option));
}
}
return Promise.allSettled(promises).then((values) => {
let errors = false;
2024-04-18 10:47:21 +02:00
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
2022-09-26 23:11:43 +02:00
for (const val of values) {
if (val.status == "rejected") {
2024-07-11 15:47:27 +02:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2022-09-26 23:11:43 +02:00
messageManager.error("Fail to stop addon", val.reason);
logger.error("Fail to stop addon");
logger.error(val.reason);
errors = true;
}
}
if (errors) {
messageManager.error("Fail to stop addon");
logger.error("Fail to stop addon");
2024-07-11 15:47:27 +02:00
return Promise.reject(new Error());
2022-09-26 23:11:43 +02:00
}
});
}
function startAddons(addonSlugs: string[]) {
logger.info("Starting addons...");
const status = statusTools.getStatus();
2024-04-18 10:47:21 +02:00
status.status = States.START_ADDON;
2022-09-26 23:11:43 +02:00
status.progress = -1;
statusTools.setStatus(status);
const promises = [];
const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` },
responseType: "json",
};
for (const addon of addonSlugs) {
if (addon !== "") {
logger.debug(`... Starting addon ${addon}`);
promises.push(got.post(`http://hassio/addons/${addon}/start`, option));
}
}
return Promise.allSettled(promises).then((values) => {
2024-04-18 10:47:21 +02:00
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
2022-09-26 23:11:43 +02:00
let errors = false;
for (const val of values) {
if (val.status == "rejected") {
2024-07-11 15:47:27 +02:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2022-09-26 23:11:43 +02:00
messageManager.error("Fail to start addon", val.reason);
logger.error("Fail to start addon");
logger.error(val.reason);
errors = true;
}
}
if (errors) {
messageManager.error("Fail to start addon");
logger.error("Fail to start addon");
2024-07-11 15:47:27 +02:00
return Promise.reject(new Error());
2022-09-26 23:11:43 +02:00
}
});
}
2023-01-13 16:18:27 +01:00
export function getFolderList() {
return [
{
name: "Home Assistant configuration",
slug: "homeassistant",
},
{
name: "SSL",
slug: "ssl",
},
{
name: "Share",
slug: "share",
},
{
name: "Media",
slug: "media",
},
{
name: "Local add-ons",
slug: "addons/local",
},
];
}
2024-07-11 15:47:27 +02:00
function publish_state() {
2022-09-26 23:11:43 +02:00
// let data_error_sensor = {
// state: state.status == "error" ? "on" : "off",
// attributes: {
// friendly_name: "Nexcloud Backup Error",
// device_class: "problem",
// error_code: state.error_code,
// message: state.message,
// icon: state.status == "error" ? "mdi:cloud-alert" : "mdi:cloud-check"
// },
// }
// let option = {
// headers: { "authorization": `Bearer ${token}` },
// responseType: "json",
// json: data_error_sensor
// };
// got.post(`http://hassio/core/api/states/binary_sensor.nextcloud_backup_error`, option)
// .then((result) => {
// logger.debug('Home assistant sensor updated (error status)');
// })
// .catch((error) => {
// logger.error(error);
// });
// let icon = ""
// switch(state.status){
// case "error":
// icon = "mdi:cloud-alert";
// break;
// case "download":
// case "download-b":
// icon = "mdi:cloud-download";
// break;
// case "upload":
// case "upload-b":
// icon = "mdi:cloud-upload";
// break;
// case "idle":
// icon = "mdi:cloud-check";
// break;
// default:
// icon = "mdi:cloud-sync";
// break;
// }
// let data_state_sensor = {
// state: state.status,
// attributes: {
// friendly_name: "Nexcloud Backup Status",
// error_code: state.error_code,
// message: state.message,
// icon: icon,
// last_backup: state.last_backup == null || state.last_backup == "" ? "" : new Date(state.last_backup).toISOString(),
// next_backup: state.next_backup == null || state.next_backup == "" ? "" : new Date(state.next_backup).toISOString()
// },
// }
// option.json = data_state_sensor
// got.post(`http://hassio/core/api/states/sensor.nextcloud_backup_status`, option)
// .then((result) => {
// logger.debug('Home assistant sensor updated (status)');
// })
// .catch((error) => {
// logger.error(error);
// });
}
export {
2024-07-11 15:47:27 +02:00
clean,
createNewBackup,
downloadSnapshot,
2022-09-26 23:11:43 +02:00
getAddonList,
2024-07-11 15:47:27 +02:00
getBackupInfo,
2022-09-26 23:11:43 +02:00
getBackups,
2024-07-11 15:47:27 +02:00
getVersion,
2022-09-26 23:11:43 +02:00
publish_state,
2024-07-11 15:47:27 +02:00
startAddons,
stopAddons,
uploadSnapshot,
2022-09-26 23:11:43 +02:00
};