From 630f048c13d6060df161d9d7bb1684d671862688 Mon Sep 17 00:00:00 2001 From: SebClem Date: Mon, 22 Jul 2024 17:22:24 +0200 Subject: [PATCH] Add restore to ha --- nextcloud_backup/backend/src/routes/webdav.ts | 24 +- .../src/services/homeAssistantService.ts | 91 ++-- .../backend/src/services/orchestrator.ts | 25 + .../backend/src/services/webdavService.ts | 493 +++--------------- .../src/types/services/ha_os_response.ts | 10 +- .../backend/src/types/services/webdav.ts | 5 +- .../src/components/cloud/CloudList.vue | 16 +- .../src/components/cloud/CloudListItem.vue | 11 +- .../frontend/src/services/webdavService.ts | 10 + 9 files changed, 189 insertions(+), 496 deletions(-) diff --git a/nextcloud_backup/backend/src/routes/webdav.ts b/nextcloud_backup/backend/src/routes/webdav.ts index 9d949d6..e821dd1 100644 --- a/nextcloud_backup/backend/src/routes/webdav.ts +++ b/nextcloud_backup/backend/src/routes/webdav.ts @@ -7,8 +7,11 @@ import { } from "../services/webdavConfigService.js"; import * as webdavService from "../services/webdavService.js"; import * as pathTools from "../tools/pathTools.js"; -import type { WebdavDelete } from "../types/services/webdav.js"; +import type { WebdavGenericPath } from "../types/services/webdav.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(); @@ -55,7 +58,7 @@ webdavRouter.get("/backup/manual", (req, res) => { }); webdavRouter.delete("/", (req, res) => { - const body = req.body as WebdavDelete; + const body = req.body as WebdavGenericPath; const validator = Joi.object(WebdavDeleteValidation); const config = getWebdavConfig(); validateWebdavConfig(config) @@ -80,4 +83,21 @@ 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; diff --git a/nextcloud_backup/backend/src/services/homeAssistantService.ts b/nextcloud_backup/backend/src/services/homeAssistantService.ts index c9cee72..3330b5c 100644 --- a/nextcloud_backup/backend/src/services/homeAssistantService.ts +++ b/nextcloud_backup/backend/src/services/homeAssistantService.ts @@ -4,7 +4,6 @@ import FormData from "form-data"; import got, { RequestError, type OptionsOfJSONResponseBody, - type PlainResponse, type Progress, type Response, } from "got"; @@ -296,54 +295,42 @@ function clean(backups: BackupModel[], numberToKeep: number) { } function uploadSnapshot(path: string) { - return new Promise((resolve, reject) => { - const status = statusTools.getStatus(); - status.status = States.BKUP_UPLOAD_HA; - status.progress = 0; - statusTools.setStatus(status); - logger.info("Uploading backup..."); - const stream = fs.createReadStream(path); - const form = new FormData(); - form.append("file", stream); + const status = statusTools.getStatus(); + status.status = States.BKUP_UPLOAD_HA; + 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) - .on("uploadProgress", (e: Progress) => { - const percent = e.percent; - if (status.progress !== percent) { - status.progress = percent; - statusTools.setStatus(status); - } - if (percent >= 1) { - logger.info("Upload done..."); - } - }) - .on("response", (res: PlainResponse) => { - if (res.statusCode !== 200) { - messageManager.error( - "Fail to upload backup to Home Assistant", - `Code: ${res.statusCode} Body: ${res.body as string}` - ); - logger.error("Fail to upload backup to Home Assistant"); - logger.error(`Code: ${res.statusCode}`); - logger.error(`Body: ${res.body as string}`); - fs.unlinkSync(path); - 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 options = { + body: form, + headers: { authorization: `Bearer ${token}` }, + }; + return got + .post(`http://hassio/backups/new/upload`, options) + .on("uploadProgress", (e: Progress) => { + const percent = e.percent; + if (status.progress !== percent) { + status.progress = percent; + statusTools.setStatus(status); + } + if (percent >= 1) { + logger.info("Upload done..."); + } + }) + .then( + (res) => { + logger.info(`...Upload finish ! (status: ${res.statusCode})`); + const status = statusTools.getStatus(); + status.status = States.IDLE; + status.progress = undefined; + statusTools.setStatus(status); + fs.unlinkSync(path); + return res; + }, + (err: RequestError) => { const status = statusTools.getStatus(); status.status = States.IDLE; status.progress = undefined; @@ -355,9 +342,9 @@ function uploadSnapshot(path: string) { ); logger.error("Fail to upload backup to Home Assistant"); logger.error(err); - reject(err); - }); - }); + logger.error(`Body: ${err.response?.body as string}`); + } + ); } function stopAddons(addonSlugs: string[]) { @@ -532,6 +519,7 @@ function publish_state() { export { clean, createNewBackup, + delSnap, downloadSnapshot, getAddonList, getBackupInfo, @@ -541,5 +529,4 @@ export { startAddons, stopAddons, uploadSnapshot, - delSnap, }; diff --git a/nextcloud_backup/backend/src/services/orchestrator.ts b/nextcloud_backup/backend/src/services/orchestrator.ts index 0eb7065..325706a 100644 --- a/nextcloud_backup/backend/src/services/orchestrator.ts +++ b/nextcloud_backup/backend/src/services/orchestrator.ts @@ -7,6 +7,8 @@ import { BackupType } from "../types/services/backupConfig.js"; import type { AddonModel, BackupDetailModel, + BackupUpload, + SupervisorResponse, } from "../types/services/ha_os_response.js"; import { WorkflowType } from "../types/services/orchecstrator.js"; import * as backupConfigService from "./backupConfigService.js"; @@ -195,3 +197,26 @@ function backupFail() { statusTools.setStatus(status); 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; + logger.info(`Successfully uploaded ${filename} to Home Assistant.`); + messageManager.info( + "Successfully uploaded backup to Home Assistant.", + `Name: ${filename} slug: ${body.data.slug} + ` + ); + } + }); +} diff --git a/nextcloud_backup/backend/src/services/webdavService.ts b/nextcloud_backup/backend/src/services/webdavService.ts index 70d23fa..76ffd42 100644 --- a/nextcloud_backup/backend/src/services/webdavService.ts +++ b/nextcloud_backup/backend/src/services/webdavService.ts @@ -21,6 +21,8 @@ import type { WebdavConfig } from "../types/services/webdavConfig.js"; import { States } from "../types/status.js"; import { templateToRegexp } from "./backupConfigService.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_NUMBER_SIZE = 5; // To add landing "0" @@ -452,7 +454,7 @@ export async function chunkedUpload( } } -export function uploadChunk( +function uploadChunk( url: string, finalDestination: string, body: fs.ReadStream, @@ -497,7 +499,7 @@ export function uploadChunk( }); } -export function initChunkedUpload( +function initChunkedUpload( url: string, finalDestination: string, config: WebdavConfig @@ -517,7 +519,7 @@ export function initChunkedUpload( }); } -export function assembleChunkedUpload( +function assembleChunkedUpload( url: string, finalDestination: string, totalLength: number, @@ -539,436 +541,63 @@ export function assembleChunkedUpload( 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"; -// import { DateTime } from "luxon"; -// import logger from "../config/winston.js"; -// import { WebdavSettings } from "../types/settings.js"; -// import * as pathTools from "../tools/pathTools.js"; -// import * as settingsTools from "../tools/settingsTools.js"; -// import * as statusTools from "../tools/status.js"; - -// const endpoint = "/remote.php/webdav"; -// const configPath = "/data/webdav_conf.json"; - -// const pipeline = promisify(stream.pipeline); - -// class WebdavTools { -// host: string | undefined; -// client: WebDAVClient | undefined; -// baseUrl: string | undefined; -// username: string | undefined; -// password: string | undefined; - -// init( -// ssl: boolean, -// host: string, -// username: string, -// password: string, -// accept_selfsigned_cert: boolean -// ) { -// return new Promise((resolve, reject) => { -// this.host = host; -// const status = statusTools.getStatus(); -// logger.info("Initializing and checking webdav client..."); -// this.baseUrl = (ssl ? "https" : "http") + "://" + host + endpoint; -// this.username = username; -// this.password = password; -// const agent_option = ssl -// ? { rejectUnauthorized: !accept_selfsigned_cert } -// : {}; -// try { -// this.client = createClient(this.baseUrl, { -// username: username, -// password: password, -// httpsAgent: new https.Agent(agent_option), -// }); - -// this.client -// .getDirectoryContents("/") -// .then(() => { -// if (status.error_code === 3) { -// status.status = "idle"; -// status.message = undefined; -// status.error_code = undefined; -// statusTools.setStatus(status); -// } -// 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((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((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)); -// }); -// } +export function downloadFile( + webdavPath: string, + filename: string, + config: WebdavConfig +) { + logger.info(`Downloading ${webdavPath} from webdav...`); + if (!fs.existsSync("./temp/")) { + fs.mkdirSync("./temp/"); + } + const tmp_file = `./temp/${filename}`; + const stream = fs.createWriteStream(tmp_file); + const options = { + headers: { + authorization: + "Basic " + + Buffer.from(config.username + ":" + config.password).toString("base64"), + }, + https: { rejectUnauthorized: !config.allowSelfSignedCerts }, + }; + const url = config.url + getEndpoint(config) + webdavPath; + logger.debug(`...URI: ${encodeURI(url)}`); + logger.debug(`...rejectUnauthorized: ${options.https?.rejectUnauthorized}`); + const status = statusTools.getStatus(); + status.status = States.BKUP_DOWNLOAD_CLOUD; + status.progress = 0; + statusTools.setStatus(status); + return pipeline( + got.stream.get(encodeURI(url), options).on("downloadProgress", (e) => { + const percent = Math.round(e.percent * 100) / 100; + if (status.progress !== percent) { + status.progress = percent; + statusTools.setStatus(status); + } + }), + stream + ).then( + () => { + logger.info("Download success !"); + status.progress = 1; + statusTools.setStatus(status); + logger.debug( + `Backup dl size: ${humanFileSize(fs.statSync(tmp_file).size)}` + ); + return tmp_file; + }, + (reason: RequestError) => { + if (fs.existsSync(tmp_file)) fs.unlinkSync(tmp_file); + messageManager.error("Fail to download Cloud backup", reason.message); + const status = statusTools.getStatus(); + status.status = States.IDLE; + status.progress = undefined; + statusTools.setStatus(status); + return Promise.reject(reason); + } + ); +} // clean() { // let limit = settingsTools.getSettings().auto_clean_backup_keep; diff --git a/nextcloud_backup/backend/src/types/services/ha_os_response.ts b/nextcloud_backup/backend/src/types/services/ha_os_response.ts index e546d10..8c59f5d 100644 --- a/nextcloud_backup/backend/src/types/services/ha_os_response.ts +++ b/nextcloud_backup/backend/src/types/services/ha_os_response.ts @@ -1,4 +1,3 @@ - export interface SupervisorResponse { result: string; data: T; @@ -25,7 +24,6 @@ export interface AddonData { addons: AddonModel[]; } - export interface AddonModel { name: string; slug: string; @@ -43,10 +41,9 @@ export interface AddonModel { } export interface BackupData { - backups: BackupModel[] + backups: BackupModel[]; } - export interface BackupModel { slug: string; date: string; @@ -80,3 +77,8 @@ export interface BackupDetailModel { repositories: string[]; folders: string[]; } + +export interface BackupUpload { + slug: string; + job_id: string; +} diff --git a/nextcloud_backup/backend/src/types/services/webdav.ts b/nextcloud_backup/backend/src/types/services/webdav.ts index de3a773..a8ba8e9 100644 --- a/nextcloud_backup/backend/src/types/services/webdav.ts +++ b/nextcloud_backup/backend/src/types/services/webdav.ts @@ -10,7 +10,6 @@ export interface WebdavBackup { creationDate?: DateTime; } - -export interface WebdavDelete { +export interface WebdavGenericPath { path: string; -} \ No newline at end of file +} diff --git a/nextcloud_backup/frontend/src/components/cloud/CloudList.vue b/nextcloud_backup/frontend/src/components/cloud/CloudList.vue index 916e14b..5191b4b 100644 --- a/nextcloud_backup/frontend/src/components/cloud/CloudList.vue +++ b/nextcloud_backup/frontend/src/components/cloud/CloudList.vue @@ -39,6 +39,7 @@ :item="item" :index="index" @delete="deleteBackup" + @upload="restore" > @@ -67,6 +68,7 @@ :item="item" :index="index" @delete="deleteBackup" + @upload="restore" > @@ -89,9 +91,11 @@ import type { WebdavBackup } from "@/types/webdav"; import { getAutoBackupList, getManualBackupList, + restoreWebdavBackup, } from "@/services/webdavService"; import CloudDeleteDialog from "./CloudDeleteDialog.vue"; import CloudListItem from "./CloudListItem.vue"; +import { useAlertStore } from "@/store/alert"; const deleteDialog = ref | null>(null); const deleteItem = ref(null); @@ -99,7 +103,7 @@ const autoBackups = ref([]); const manualBackups = ref([]); const loading = ref(true); - +const alertStore = useAlertStore(); function refreshBackup() { loading.value = true; getAutoBackupList() @@ -120,6 +124,16 @@ function deleteBackup(item: WebdavBackup) { deleteItem.value = 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(); defineExpose({ refreshBackup }); diff --git a/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue b/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue index cc3fc26..9deba4f 100644 --- a/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue +++ b/nextcloud_backup/frontend/src/components/cloud/CloudListItem.vue @@ -94,7 +94,12 @@ @@ -108,18 +113,20 @@ diff --git a/nextcloud_backup/frontend/src/services/webdavService.ts b/nextcloud_backup/frontend/src/services/webdavService.ts index c22e195..a8b82c1 100644 --- a/nextcloud_backup/frontend/src/services/webdavService.ts +++ b/nextcloud_backup/frontend/src/services/webdavService.ts @@ -18,3 +18,13 @@ export function deleteWebdabBackup(path: string) { }) .text(); } + +export function restoreWebdavBackup(path: string) { + return kyClient + .post("webdav/restore", { + json: { + path: path, + }, + }) + .text(); +}