Compare commits

...

3 Commits

Author SHA1 Message Date
96233dcd7b
Hide progress bar if idle 2024-08-02 16:36:18 +02:00
d65e9e10b1
Format action section 2024-08-02 16:07:58 +02:00
270b6c523f
Add cleanup 2024-08-02 15:56:07 +02:00
10 changed files with 192 additions and 74 deletions

View File

@ -2,6 +2,10 @@ import express from "express";
import { doBackupWorkflow } from "../services/orchestrator.js"; import { doBackupWorkflow } from "../services/orchestrator.js";
import { WorkflowType } from "../types/services/orchecstrator.js"; import { WorkflowType } from "../types/services/orchecstrator.js";
import logger from "../config/winston.js"; import logger from "../config/winston.js";
import { clean as webdavClean } from "../services/webdavService.js";
import { getBackupConfig } from "../services/backupConfigService.js";
import { getWebdavConfig } from "../services/webdavConfigService.js";
import { clean } from "../services/homeAssistantService.js";
const actionRouter = express.Router(); const actionRouter = express.Router();
@ -16,4 +20,20 @@ actionRouter.post("/backup", (req, res) => {
res.sendStatus(202); res.sendStatus(202);
}); });
actionRouter.post("/clean", (req, res) => {
const backupConfig = getBackupConfig();
const webdavConfig = getWebdavConfig();
webdavClean(backupConfig, webdavConfig)
.then(() => {
return clean(backupConfig);
})
.then(() => {
logger.info("All good !");
})
.catch(() => {
logger.error("Something wrong !");
});
res.sendStatus(202);
});
export default actionRouter; export default actionRouter;

View File

@ -13,13 +13,15 @@ import { promisify } from "util";
import logger from "../config/winston.js"; 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,
type BackupConfig,
} from "../types/services/backupConfig.js";
import type { NewBackupPayload } from "../types/services/ha_os_payload.js"; import type { NewBackupPayload } from "../types/services/ha_os_payload.js";
import type { import type {
AddonData, AddonData,
BackupData, BackupData,
BackupDetailModel, BackupDetailModel,
BackupModel,
CoreInfoBody, CoreInfoBody,
SupervisorResponse, SupervisorResponse,
} from "../types/services/ha_os_response.js"; } from "../types/services/ha_os_response.js";
@ -161,7 +163,7 @@ function downloadSnapshot(id: string): Promise<string> {
} }
function delSnap(id: string) { function delSnap(id: string) {
logger.info(`Deleting Home Assistant backup ${id}`); logger.debug(`Deleting Home Assistant backup ${id}`);
const option = { const option = {
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
}; };
@ -262,25 +264,45 @@ function createNewBackup(
); );
} }
function clean(backups: BackupModel[], numberToKeep: number) { function clean(backupConfig: BackupConfig) {
const promises = []; if (!backupConfig.autoClean.homeAssistant.enabled) {
if (backups.length < numberToKeep) { logger.debug("Clean disabled for Home Assistant");
return; return Promise.resolve();
} }
logger.info("Clean for Home Assistant");
const status = statusTools.getStatus();
status.status = States.CLEAN_HA;
status.progress = -1;
statusTools.setStatus(status);
const numberToKeep = backupConfig.autoClean.homeAssistant.nbrToKeep || 5;
return getBackups()
.then((response) => {
const backups = response.body.data.backups;
if (backups.length > numberToKeep) {
backups.sort((a, b) => { backups.sort((a, b) => {
return Date.parse(b.date) - Date.parse(a.date); return Date.parse(b.date) - Date.parse(a.date);
}); });
const toDel = backups.slice(numberToKeep); const toDel = backups.slice(numberToKeep);
for (const i of toDel) { logger.debug(`Number of backup to clean: ${toDel.length}`);
promises.push(delSnap(i.slug)); const promises = toDel.map((value) => delSnap(value.slug));
logger.info("Home Assistant clean done.");
return Promise.allSettled(promises);
} else {
logger.debug("Nothing to clean");
} }
logger.info("Local clean done."); })
return Promise.allSettled(promises).then((values) => { .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") {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument messageManager.error("Fail to delete backup", val.reason as string);
messageManager.error("Fail to delete backup", val.reason);
logger.error("Fail to delete backup"); logger.error("Fail to delete backup");
logger.error(val.reason); logger.error(val.reason);
errors = true; errors = true;
@ -291,7 +313,17 @@ function clean(backups: BackupModel[], numberToKeep: number) {
logger.error("Fail to clean backups in Home Assistant"); logger.error("Fail to clean backups in Home Assistant");
return Promise.reject(new Error()); return Promise.reject(new Error());
} }
}); return Promise.resolve();
},
(reason: RequestError) => {
logger.error("Fail to clean Home Assistant backup", reason.message);
messageManager.error(
"Fail to clean Home Assistant backup",
reason.message
);
return Promise.reject(reason);
}
);
} }
function uploadSnapshot(path: string) { function uploadSnapshot(path: string) {

View File

@ -92,6 +92,12 @@ export function doBackupWorkflow(type: WorkflowType) {
.then(() => { .then(() => {
return homeAssistantService.startAddons(addonsToStartStop); return homeAssistantService.startAddons(addonsToStartStop);
}) })
.then(() => {
return homeAssistantService.clean(backupConfig);
})
.then(() => {
return webDavService.clean(backupConfig, webdavConfig);
})
.then(() => { .then(() => {
logger.info("Backup workflow finished successfully !"); logger.info("Backup workflow finished successfully !");
messageManager.info( messageManager.info(

View File

@ -23,6 +23,7 @@ import { templateToRegexp } from "./backupConfigService.js";
import { getChunkEndpoint, getEndpoint } from "./webdavConfigService.js"; import { getChunkEndpoint, getEndpoint } from "./webdavConfigService.js";
import { pipeline } from "stream/promises"; import { pipeline } from "stream/promises";
import { humanFileSize } from "../tools/toolbox.js"; import { humanFileSize } from "../tools/toolbox.js";
import type { BackupConfig } from "../types/services/backupConfig.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"
@ -201,6 +202,7 @@ function extractBackupInfo(backups: WebdavBackup[], template: string) {
} }
export function deleteBackup(path: string, config: WebdavConfig) { export function deleteBackup(path: string, config: WebdavConfig) {
logger.debug(`Deleting Cloud backup ${path}`);
const endpoint = getEndpoint(config); const endpoint = getEndpoint(config);
return got return got
.delete(config.url + endpoint + path, { .delete(config.url + endpoint + path, {
@ -589,6 +591,7 @@ export function downloadFile(
}, },
(reason: RequestError) => { (reason: RequestError) => {
if (fs.existsSync(tmp_file)) fs.unlinkSync(tmp_file); if (fs.existsSync(tmp_file)) fs.unlinkSync(tmp_file);
logger.error("Fail to download Cloud backup", reason.message);
messageManager.error("Fail to download Cloud backup", reason.message); messageManager.error("Fail to download Cloud backup", reason.message);
const status = statusTools.getStatus(); const status = statusTools.getStatus();
status.status = States.IDLE; status.status = States.IDLE;
@ -599,39 +602,59 @@ export function downloadFile(
); );
} }
// clean() { export function clean(backupConfig: BackupConfig, webdavConfig: WebdavConfig) {
// let limit = settingsTools.getSettings().auto_clean_backup_keep; if (!backupConfig.autoClean.webdav.enabled) {
// if (limit == null) limit = 5; logger.debug("Clean disabled for Cloud");
// return new Promise((resolve, reject) => { return Promise.resolve();
// this.getFolderContent(this.getConf()?.back_dir + pathTools.auto) }
// .then(async (contents: any) => { logger.info("Clean for cloud");
// if (contents.length < limit) { const status = statusTools.getStatus();
// resolve(undefined); status.status = States.CLEAN_CLOUD;
// return; status.progress = -1;
// } statusTools.setStatus(status);
// contents.sort((a: any, b: any) => { const limit = backupConfig.autoClean.homeAssistant.nbrToKeep || 5;
// return a.date < b.date ? 1 : -1; return getBackups(pathTools.auto, webdavConfig, backupConfig.nameTemplate)
// }); .then((backups) => {
if (backups.length > limit) {
const toDel = backups.splice(limit);
logger.debug(`Number of backup to clean: ${toDel.length}`);
const promises = toDel.map((value) =>
deleteBackup(value.path, webdavConfig)
);
return Promise.allSettled(promises);
} else {
logger.debug("Nothing to clean");
}
})
.then(
(values) => {
const status = statusTools.getStatus();
status.status = States.IDLE;
status.progress = undefined;
statusTools.setStatus(status);
// const toDel = contents.slice(limit); let errors = false;
// for (const i in toDel) { for (const val of values || []) {
// await this.client?.deleteFile(toDel[i].filename); if (val.status == "rejected") {
// } messageManager.error("Fail to delete backup", val.reason);
// logger.info("Cloud clean done."); logger.error("Fail to delete backup");
// resolve(undefined); logger.error(val.reason);
// }) errors = true;
// .catch((error) => { }
// const status = statusTools.getStatus(); }
// status.status = "error";
// status.error_code = 6;
// status.message = "Fail to clean Nexcloud (" + error + ") !";
// statusTools.setStatus(status);
// logger.error(status.message);
// reject(status.message);
// });
// });
// }
// }
// const INSTANCE = new WebdavTools(); if (errors) {
// export default INSTANCE; messageManager.error("Fail to clean backups in Cloud");
logger.error("Fail to clean backups in Cloud");
return Promise.reject(new Error());
}
return Promise.resolve();
},
(reason: RequestError) => {
logger.error("Fail to clean cloud backup", reason.message);
messageManager.error("Fail to clean cloud backup", reason.message);
return Promise.reject(reason);
}
);
}

View File

@ -9,6 +9,8 @@ export enum States {
BKUP_UPLOAD_CLOUD = "BKUP_UPLOAD_CLOUD", BKUP_UPLOAD_CLOUD = "BKUP_UPLOAD_CLOUD",
STOP_ADDON = "STOP_ADDON", STOP_ADDON = "STOP_ADDON",
START_ADDON = "START_ADDON", START_ADDON = "START_ADDON",
CLEAN_CLOUD = "CLEAN_CLOUD",
CLEAN_HA = "CLEAN_HA",
} }
export interface Status { export interface Status {

View File

@ -9,7 +9,7 @@
"type-check": "vue-tsc --noEmit" "type-check": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "7.0.96", "@mdi/font": "7.4.47",
"core-js": "^3.34.0", "core-js": "^3.34.0",
"ky": "^1.2.0", "ky": "^1.2.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",

View File

@ -6,8 +6,8 @@ settings:
dependencies: dependencies:
'@mdi/font': '@mdi/font':
specifier: 7.0.96 specifier: 7.4.47
version: 7.0.96 version: 7.4.47
core-js: core-js:
specifier: ^3.34.0 specifier: ^3.34.0
version: 3.36.0 version: 3.36.0
@ -391,8 +391,8 @@ packages:
/@jridgewell/sourcemap-codec@1.4.15: /@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
/@mdi/font@7.0.96: /@mdi/font@7.4.47:
resolution: {integrity: sha512-rzlxTfR64hqY8yiBzDjmANfcd8rv+T5C0Yedv/TWk2QyAQYdc66e0kaN1ipmnYU3RukHRTRcBARHzzm+tIhL7w==} resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
dev: false dev: false
/@nodelib/fs.scandir@2.1.5: /@nodelib/fs.scandir@2.1.5:

View File

@ -3,13 +3,34 @@
<v-card-title class="text-center">Action</v-card-title> <v-card-title class="text-center">Action</v-card-title>
<v-divider class="border-opacity-25"></v-divider> <v-divider class="border-opacity-25"></v-divider>
<v-card-text> <v-card-text>
<v-btn color="success" @click="launchBackup">Backup Now</v-btn> <v-row>
<v-col class="d-flex justify-center">
<v-btn
block
color="success"
@click="launchBackup"
prepend-icon="mdi-cloud-plus"
>
Backup Now
</v-btn>
</v-col>
<v-col class="d-flex justify-center">
<v-btn
block
color="orange-darken-3"
@click="launchClean"
prepend-icon="mdi-broom"
>
Clean
</v-btn>
</v-col>
</v-row>
</v-card-text> </v-card-text>
</v-card> </v-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { backupNow } from "@/services/actionService"; import { backupNow, clean } from "@/services/actionService";
import { useAlertStore } from "@/store/alert"; import { useAlertStore } from "@/store/alert";
const alertStore = useAlertStore(); const alertStore = useAlertStore();
@ -23,4 +44,14 @@ function launchBackup() {
alertStore.add("error", "Fail to start backup workflow !"); alertStore.add("error", "Fail to start backup workflow !");
}); });
} }
function launchClean() {
clean()
.then(() => {
alertStore.add("success", "Backup workflow started !");
})
.catch(() => {
alertStore.add("error", "Fail to start backup workflow !");
});
}
</script> </script>

View File

@ -50,10 +50,10 @@
</div> </div>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row v-if="status?.status != States.IDLE">
<v-divider class="border-opacity-25 mx-n1"></v-divider> <v-divider class="border-opacity-25 mx-n1"></v-divider>
</v-row> </v-row>
<v-row> <v-row v-if="status?.status != States.IDLE">
<v-col> <v-col>
<v-progress-linear <v-progress-linear
height="25" height="25"

View File

@ -3,3 +3,7 @@ import kyClient from "./kyClient";
export function backupNow() { export function backupNow() {
return kyClient.post("action/backup"); return kyClient.post("action/backup");
} }
export function clean() {
return kyClient.post("action/clean");
}