diff --git a/src/data/backup.ts b/src/data/backup.ts index 129b76726d..5028ae0d05 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -34,3 +34,19 @@ export const generateBackup = (hass: HomeAssistant): Promise => hass.callWS({ type: "backup/generate", }); + +export const uploadBackup = async ( + hass: HomeAssistant, + file: File +): Promise => { + const fd = new FormData(); + fd.append("file", file); + const resp = await hass.fetchWithAuth("/api/backup/upload", { + method: "POST", + body: fd, + }); + + if (!resp.ok) { + throw new Error(`${resp.status} ${resp.statusText}`); + } +}; diff --git a/src/dialogs/backup/dialog-backup-upload.ts b/src/dialogs/backup/dialog-backup-upload.ts new file mode 100644 index 0000000000..e8eadf9183 --- /dev/null +++ b/src/dialogs/backup/dialog-backup-upload.ts @@ -0,0 +1,134 @@ +import { mdiClose, mdiFolderUpload } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-alert"; +import "../../components/ha-file-upload"; +import "../../components/ha-header-bar"; +import "../../components/ha-icon-button"; +import { haStyleDialog } from "../../resources/styles"; +import type { HomeAssistant } from "../../types"; +import { BackupUploadDialogParams } from "./show-dialog-backup-upload"; +import { HassDialog } from "../make-dialog-manager"; +import { showAlertDialog } from "../generic/show-dialog-box"; +import { uploadBackup } from "../../data/backup"; + +const SUPPORTED_FORMAT = "application/x-tar"; + +@customElement("dialog-backup-upload") +export class DialogBackupUpload + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _dialogParams?: BackupUploadDialogParams; + + @state() private _uploading = false; + + @state() private _error?: string; + + public async showDialog( + dialogParams: BackupUploadDialogParams + ): Promise { + this._dialogParams = dialogParams; + await this.updateComplete; + } + + public closeDialog(): void { + this._dialogParams = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._dialogParams || !this.hass) { + return nothing; + } + + return html` + +
+ + Upload backup + + +
+ + ${this._error + ? html`${this._error}` + : nothing} +
+ `; + } + + private async _uploadFile(ev: CustomEvent<{ files: File[] }>): Promise { + this._error = undefined; + const file = ev.detail.files[0]; + + if (file.type !== SUPPORTED_FORMAT) { + showAlertDialog(this, { + title: "Unsupported file format", + text: "Please choose a Home Assistant backup file (.tar)", + confirmText: "ok", + }); + return; + } + this._uploading = true; + try { + await uploadBackup(this.hass!, file); + this._dialogParams!.onUploadComplete(); + this.closeDialog(); + } catch (err: any) { + this._error = err.message; + } finally { + this._uploading = false; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + } + /* overrule the ha-style-dialog max-height on small screens */ + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-header-bar { + --mdc-theme-primary: var(--app-header-background-color); + --mdc-theme-on-primary: var(--app-header-text-color, white); + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-backup-upload": DialogBackupUpload; + } +} diff --git a/src/dialogs/backup/show-dialog-backup-upload.ts b/src/dialogs/backup/show-dialog-backup-upload.ts new file mode 100644 index 0000000000..e8aeec705e --- /dev/null +++ b/src/dialogs/backup/show-dialog-backup-upload.ts @@ -0,0 +1,17 @@ +import { fireEvent } from "../../common/dom/fire_event"; +import "./dialog-backup-upload"; + +export interface BackupUploadDialogParams { + onUploadComplete: () => void; +} + +export const showBackupUploadDialog = ( + element: HTMLElement, + dialogParams: BackupUploadDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-backup-upload", + dialogImport: () => import("./dialog-backup-upload"), + dialogParams, + }); +};