mirror of
https://github.com/Sebclem/hassio-nextcloud-backup.git
synced 2024-11-10 11:32:58 +01:00
Compare commits
3 Commits
56cfb4ee78
...
96233dcd7b
Author | SHA1 | Date | |
---|---|---|---|
96233dcd7b | |||
d65e9e10b1 | |||
270b6c523f |
@ -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;
|
||||||
|
@ -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,36 +264,66 @@ 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();
|
||||||
}
|
}
|
||||||
backups.sort((a, b) => {
|
logger.info("Clean for Home Assistant");
|
||||||
return Date.parse(b.date) - Date.parse(a.date);
|
const status = statusTools.getStatus();
|
||||||
});
|
status.status = States.CLEAN_HA;
|
||||||
const toDel = backups.slice(numberToKeep);
|
status.progress = -1;
|
||||||
for (const i of toDel) {
|
statusTools.setStatus(status);
|
||||||
promises.push(delSnap(i.slug));
|
|
||||||
}
|
const numberToKeep = backupConfig.autoClean.homeAssistant.nbrToKeep || 5;
|
||||||
logger.info("Local clean done.");
|
return getBackups()
|
||||||
return Promise.allSettled(promises).then((values) => {
|
.then((response) => {
|
||||||
let errors = false;
|
const backups = response.body.data.backups;
|
||||||
for (const val of values) {
|
if (backups.length > numberToKeep) {
|
||||||
if (val.status == "rejected") {
|
backups.sort((a, b) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
return Date.parse(b.date) - Date.parse(a.date);
|
||||||
messageManager.error("Fail to delete backup", val.reason);
|
});
|
||||||
logger.error("Fail to delete backup");
|
const toDel = backups.slice(numberToKeep);
|
||||||
logger.error(val.reason);
|
logger.debug(`Number of backup to clean: ${toDel.length}`);
|
||||||
errors = true;
|
const promises = toDel.map((value) => delSnap(value.slug));
|
||||||
|
logger.info("Home Assistant clean done.");
|
||||||
|
return Promise.allSettled(promises);
|
||||||
|
} else {
|
||||||
|
logger.debug("Nothing to clean");
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
if (errors) {
|
.then(
|
||||||
messageManager.error("Fail to clean backups in Home Assistant");
|
(values) => {
|
||||||
logger.error("Fail to clean backups in Home Assistant");
|
const status = statusTools.getStatus();
|
||||||
return Promise.reject(new Error());
|
status.status = States.IDLE;
|
||||||
}
|
status.progress = undefined;
|
||||||
});
|
statusTools.setStatus(status);
|
||||||
|
|
||||||
|
let errors = false;
|
||||||
|
for (const val of values || []) {
|
||||||
|
if (val.status == "rejected") {
|
||||||
|
messageManager.error("Fail to delete backup", val.reason as string);
|
||||||
|
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");
|
||||||
|
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) {
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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",
|
||||||
|
@ -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:
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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");
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user