From 3a053afac6a77904f158836fa278d61b623ff8f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Feb 2024 10:08:22 -0600 Subject: [PATCH] Create backups files without having to copy inner tarballs (#110267) --- homeassistant/components/backup/manager.py | 29 +++++++++++----------- tests/components/backup/test_manager.py | 29 +++++++++++----------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index fe0d494a650..4bf92449bd7 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -4,11 +4,12 @@ from __future__ import annotations import asyncio from dataclasses import asdict, dataclass import hashlib +import io import json from pathlib import Path import tarfile from tarfile import TarError -from tempfile import TemporaryDirectory +import time from typing import Any, Protocol, cast from securetar import SecureTarFile, atomic_contents_add @@ -17,7 +18,7 @@ from homeassistant.const import __version__ as HAVERSION from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import integration_platform -from homeassistant.helpers.json import save_json +from homeassistant.helpers.json import json_bytes from homeassistant.util import dt as dt_util from homeassistant.util.json import json_loads_object @@ -228,18 +229,18 @@ class BackupManager: LOGGER.debug("Creating backup directory") self.backup_dir.mkdir() - with TemporaryDirectory() as tmp_dir, SecureTarFile( + outer_secure_tarfile = SecureTarFile( tar_file_path, "w", gzip=False, bufsize=BUF_SIZE - ) as tar_file: - tmp_dir_path = Path(tmp_dir) - save_json( - tmp_dir_path.joinpath("./backup.json").as_posix(), - backup_data, - ) - with SecureTarFile( - tmp_dir_path.joinpath("./homeassistant.tar.gz").as_posix(), - "w", - bufsize=BUF_SIZE, + ) + with outer_secure_tarfile as outer_secure_tarfile_tarfile: + raw_bytes = json_bytes(backup_data) + fileobj = io.BytesIO(raw_bytes) + tar_info = tarfile.TarInfo(name="./backup.json") + tar_info.size = len(raw_bytes) + tar_info.mtime = int(time.time()) + outer_secure_tarfile_tarfile.addfile(tar_info, fileobj=fileobj) + with outer_secure_tarfile.create_inner_tar( + "./homeassistant.tar.gz", gzip=True ) as core_tar: atomic_contents_add( tar_file=core_tar, @@ -247,7 +248,7 @@ class BackupManager: excludes=EXCLUDE_FROM_BACKUP, arcname="data", ) - tar_file.add(tmp_dir_path, arcname=".") + return tar_file_path.stat().st_size diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index e23f86e545b..528ba40112a 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -29,11 +29,11 @@ async def _mock_backup_generation(manager: BackupManager): Path(".storage"), ] - with patch("tarfile.open", MagicMock()) as mocked_tarfile, patch( - "pathlib.Path.iterdir", _mock_iterdir - ), patch("pathlib.Path.stat", MagicMock(st_size=123)), patch( - "pathlib.Path.is_file", lambda x: x.name != ".storage" - ), patch( + with patch( + "homeassistant.components.backup.manager.SecureTarFile" + ) as mocked_tarfile, patch("pathlib.Path.iterdir", _mock_iterdir), patch( + "pathlib.Path.stat", MagicMock(st_size=123) + ), patch("pathlib.Path.is_file", lambda x: x.name != ".storage"), patch( "pathlib.Path.is_dir", lambda x: x.name == ".storage", ), patch( @@ -46,21 +46,20 @@ async def _mock_backup_generation(manager: BackupManager): "pathlib.Path.mkdir", MagicMock(), ), patch( - "homeassistant.components.backup.manager.save_json" - ) as mocked_save_json, patch( + "homeassistant.components.backup.manager.json_bytes", + return_value=b"{}", # Empty JSON + ) as mocked_json_bytes, patch( "homeassistant.components.backup.manager.HAVERSION", "2025.1.0", ): await manager.generate_backup() - assert mocked_save_json.call_count == 1 - assert mocked_save_json.call_args[0][1]["homeassistant"] == { - "version": "2025.1.0" - } - - assert ( - manager.backup_dir.as_posix() - in mocked_tarfile.call_args_list[0].kwargs["name"] + assert mocked_json_bytes.call_count == 1 + backup_json_dict = mocked_json_bytes.call_args[0][0] + assert isinstance(backup_json_dict, dict) + assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"} + assert manager.backup_dir.as_posix() in str( + mocked_tarfile.call_args_list[0][0][0] )