1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00
Files
frontend/src/data/backup.ts
2025-06-12 07:29:58 +00:00

476 lines
12 KiB
TypeScript

import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import checkValidDate from "../common/datetime/check_valid_date";
import {
formatDateTime,
formatDateTimeNumeric,
} from "../common/datetime/format_date_time";
import { formatTime } from "../common/datetime/format_time";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { handleFetchPromise } from "../util/hass-call-api";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
export const enum BackupScheduleRecurrence {
NEVER = "never",
DAILY = "daily",
CUSTOM_DAYS = "custom_days",
}
export type BackupDay = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
export const BACKUP_DAYS: BackupDay[] = [
"mon",
"tue",
"wed",
"thu",
"fri",
"sat",
"sun",
];
export const sortWeekdays = (weekdays) =>
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
export interface Retention {
copies?: number | null;
days?: number | null;
}
export interface BackupConfig {
automatic_backups_configured: boolean;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
next_automatic_backup: string | null;
next_automatic_backup_additional?: boolean;
create_backup: {
agent_ids: string[];
include_addons: string[] | null;
include_all_addons: boolean;
include_database: boolean;
include_folders: string[] | null;
name: string | null;
password: string | null;
};
retention: Retention;
schedule: {
recurrence: BackupScheduleRecurrence;
time?: string | null;
days: BackupDay[];
};
agents: BackupAgentsConfig;
}
export interface BackupMutableConfig {
automatic_backups_configured?: boolean;
create_backup?: {
agent_ids?: string[];
include_addons?: string[];
include_all_addons?: boolean;
include_database?: boolean;
include_folders?: string[];
name?: string | null;
password?: string | null;
};
retention?: Retention;
schedule?: {
recurrence: BackupScheduleRecurrence;
time?: string | null;
days?: BackupDay[] | null;
};
agents?: BackupAgentsConfig;
}
export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
export interface BackupAgentConfig {
protected?: boolean;
retention?: Retention | null;
}
export interface BackupAgent {
agent_id: string;
name: string;
}
export interface BackupContentAgent {
size: number;
protected: boolean;
}
export interface AddonInfo {
name: string | null;
slug: string;
version: string | null;
}
export interface BackupContent {
backup_id: string;
date: string;
name: string;
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
failed_addons?: AddonInfo[];
failed_folders?: string[];
extra_metadata?: {
"supervisor.addon_update"?: string;
};
with_automatic_settings: boolean;
}
export interface BackupData {
addons: BackupAddon[];
database_included: boolean;
folders: string[];
homeassistant_version: string;
homeassistant_included: boolean;
}
export interface BackupAddon {
name: string;
slug: string;
version: string;
}
export interface BackupContentExtended extends BackupContent, BackupData {}
export interface BackupInfo {
backups: BackupContent[];
agent_errors: Record<string, string>;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
last_action_event: ManagerStateEvent | null;
next_automatic_backup: string | null;
next_automatic_backup_additional: boolean;
state: BackupManagerState;
}
export interface BackupDetails {
backup: BackupContentExtended;
}
export interface BackupAgentsInfo {
agents: BackupAgent[];
}
export interface GenerateBackupParams {
agent_ids: string[];
include_addons?: string[];
include_all_addons?: boolean;
include_database?: boolean;
include_folders?: string[];
include_homeassistant?: boolean;
name?: string;
password?: string;
}
export interface RestoreBackupParams {
backup_id: string;
agent_id: string;
password?: string;
restore_addons?: string[];
restore_database?: boolean;
restore_folders?: string[];
restore_homeassistant?: boolean;
}
export const fetchBackupConfig = (hass: HomeAssistant) =>
hass.callWS<{ config: BackupConfig }>({ type: "backup/config/info" });
export const updateBackupConfig = (
hass: HomeAssistant,
config: BackupMutableConfig
) => hass.callWS({ type: "backup/config/update", ...config });
export const getBackupDownloadUrl = (
id: string,
agentId: string,
password?: string | null
) =>
`/api/backup/download/${id}?agent_id=${agentId}${password ? `&password=${password}` : ""}`;
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupInfo> =>
hass.callWS({
type: "backup/info",
});
export const fetchBackupDetails = (
hass: HomeAssistant,
id: string
): Promise<BackupDetails> =>
hass.callWS({
type: "backup/details",
backup_id: id,
});
export const fetchBackupAgentsInfo = (
hass: HomeAssistant
): Promise<BackupAgentsInfo> =>
hass.callWS({
type: "backup/agents/info",
});
export const deleteBackup = (hass: HomeAssistant, id: string): Promise<void> =>
hass.callWS({
type: "backup/delete",
backup_id: id,
});
export const generateBackup = (
hass: HomeAssistant,
params: GenerateBackupParams
): Promise<{ backup_id: string }> =>
hass.callWS({
type: "backup/generate",
...params,
});
export const generateBackupWithAutomaticSettings = (
hass: HomeAssistant
): Promise<void> =>
hass.callWS({
type: "backup/generate_with_automatic_settings",
});
export const restoreBackup = (
hass: HomeAssistant,
params: RestoreBackupParams
): Promise<{ backup_id: string }> =>
hass.callWS({
type: "backup/restore",
...params,
});
export const uploadBackup = async (
hass: HomeAssistant,
file: File,
agentIds: string[]
): Promise<{ backup_id: string }> => {
const fd = new FormData();
fd.append("file", file);
const params = new URLSearchParams();
agentIds.forEach((agentId) => {
params.append("agent_id", agentId);
});
return handleFetchPromise(
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
method: "POST",
body: fd,
})
);
};
export const getPreferredAgentForDownload = (agents: string[]) => {
const localAgent = agents.find(isLocalAgent);
if (localAgent) {
return localAgent;
}
const networkMountAgent = agents.find(isNetworkMountAgent);
if (networkMountAgent) {
return networkMountAgent;
}
return agents[0];
};
export const canDecryptBackupOnDownload = (
hass: HomeAssistant,
backup_id: string,
agent_id: string,
password: string
) =>
hass.callWS({
type: "backup/can_decrypt_on_download",
backup_id,
agent_id,
password,
});
export const CORE_LOCAL_AGENT = "backup.local";
export const HASSIO_LOCAL_AGENT = "hassio.local";
export const CLOUD_AGENT = "cloud.cloud";
export const isLocalAgent = (agentId: string) =>
[CORE_LOCAL_AGENT, HASSIO_LOCAL_AGENT].includes(agentId);
export const isNetworkMountAgent = (agentId: string) => {
const [domain, name] = agentId.split(".");
return domain === "hassio" && name !== "local";
};
export const computeBackupAgentName = (
localize: LocalizeFunc,
agentId: string,
agents: BackupAgent[]
) => {
if (isLocalAgent(agentId)) {
return localize("ui.panel.config.backup.agents.local_agent");
}
const agent = agents.find((a) => a.agent_id === agentId);
const domain = agentId.split(".")[0];
const name = agent ? agent.name : agentId.split(".")[1];
// If it's a network mount agent, only show the name
if (isNetworkMountAgent(agentId)) {
return name;
}
const domainName = domainToName(localize, domain);
// If there are multiple agents for a domain, show the name
const showName =
agents.filter((a) => a.agent_id.split(".")[0] === domain).length > 1;
return showName ? `${domainName}: ${name}` : domainName;
};
export const computeBackupSize = (backup: BackupContent) =>
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
export type BackupType = "automatic" | "manual" | "addon_update";
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "addon_update", "manual"];
export const getBackupTypes = memoize((isHassio: boolean) =>
isHassio
? BACKUP_TYPE_ORDER
: BACKUP_TYPE_ORDER.filter((type) => type !== "addon_update")
);
export const computeBackupType = (
backup: BackupContent,
isHassio: boolean
): BackupType => {
if (backup.with_automatic_settings) {
return "automatic";
}
if (isHassio && backup.extra_metadata?.["supervisor.addon_update"] != null) {
return "addon_update";
}
return "manual";
};
export const compareAgents = (a: string, b: string) => {
const isLocalA = isLocalAgent(a);
const isLocalB = isLocalAgent(b);
const isNetworkMountAgentA = isNetworkMountAgent(a);
const isNetworkMountAgentB = isNetworkMountAgent(b);
const getPriority = (isLocal: boolean, isNetworkMount: boolean) => {
if (isLocal) return 1;
if (isNetworkMount) return 2;
return 3;
};
const priorityA = getPriority(isLocalA, isNetworkMountAgentA);
const priorityB = getPriority(isLocalB, isNetworkMountAgentB);
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return a.localeCompare(b);
};
export const generateEncryptionKey = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const pattern = "xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx";
let result = "";
const randomArray = new Uint8Array(pattern.length);
crypto.getRandomValues(randomArray);
randomArray.forEach((number, index) => {
result += pattern[index] === "-" ? "-" : chars[number % chars.length];
});
return result;
};
export const generateEmergencyKit = (
hass: HomeAssistant,
encryptionKey: string
) =>
"data:text/plain;charset=utf-8," +
encodeURIComponent(`${hass.localize("ui.panel.config.backup.emergency_kit_file.title")}
${hass.localize("ui.panel.config.backup.emergency_kit_file.description")}
${hass.localize("ui.panel.config.backup.emergency_kit_file.date")} ${formatDateTime(new Date(), hass.locale, hass.config)}
${hass.localize("ui.panel.config.backup.emergency_kit_file.instance")}
${hass.config.location_name}
${hass.localize("ui.panel.config.backup.emergency_kit_file.url")}
${hass.auth.data.hassUrl}
${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")}
${encryptionKey}
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: "https://www.home-assistant.io/more-info/backup-emergency-kit" })}`);
export const geneateEmergencyKitFileName = (
hass: HomeAssistant,
append?: string
) =>
`home_assistant_backup_emergency_kit_${append ? `${append}_` : ""}${formatDateTimeNumeric(new Date(), hass.locale, hass.config).replace(",", "").replace(" ", "_")}.txt`;
export const downloadEmergencyKit = (
hass: HomeAssistant,
key: string,
appendFileName?: string
) =>
fileDownload(
generateEmergencyKit(hass, key),
geneateEmergencyKitFileName(hass, appendFileName)
);
export const DEFAULT_OPTIMIZED_BACKUP_START_TIME = setMinutes(
setHours(new Date(), 4),
45
);
export const DEFAULT_OPTIMIZED_BACKUP_END_TIME = setMinutes(
setHours(new Date(), 5),
45
);
export const getFormattedBackupTime = memoizeOne(
(
locale: FrontendLocaleData,
config: HassConfig,
backupTime?: Date | string | null
) => {
if (checkValidDate(backupTime as Date)) {
return formatTime(backupTime as Date, locale, config);
}
if (typeof backupTime === "string" && backupTime) {
const splitted = backupTime.split(":");
const date = setMinutes(
setHours(new Date(), parseInt(splitted[0])),
parseInt(splitted[1])
);
return formatTime(date, locale, config);
}
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
}
);
export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";
export interface BackupUploadFileFormData {
file?: File;
}
export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
file: undefined,
};