diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 30b792676a..24595eb942 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1075,12 +1075,9 @@ class ArchiveRequestHandler(BaseHandler): shutil.move(config_file, os.path.join(archive_path, configuration)) storage_json = StorageJSON.load(storage_path) - if storage_json is not None: + if storage_json is not None and storage_json.build_path: # Delete build folder (if exists) - name = storage_json.name - build_folder = os.path.join(settings.config_dir, name) - if build_folder is not None: - shutil.rmtree(build_folder, os.path.join(archive_path, name)) + shutil.rmtree(storage_json.build_path, ignore_errors=True) class UnArchiveRequestHandler(BaseHandler): diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 1684b2c143..605df4e02c 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -589,7 +589,7 @@ async def test_archive_request_handler_post( mock_ext_storage_path: MagicMock, tmp_path: Path, ) -> None: - """Test ArchiveRequestHandler.post method.""" + """Test ArchiveRequestHandler.post method without storage_json.""" # Set up temp directories config_dir = Path(get_fixture_path("conf")) @@ -616,6 +616,97 @@ async def test_archive_request_handler_post( ).read_text() == "esphome:\n name: test_archive\n" +@pytest.mark.asyncio +async def test_archive_handler_with_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + mock_storage_json: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json and build folder.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + build_dir = tmp_path / "build" + build_dir.mkdir() + + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + build_folder = build_dir / "test_device" + build_folder.mkdir() + (build_folder / "firmware.bin").write_text("binary content") + (build_folder / ".pioenvs").mkdir() + + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = str(test_config) + mock_archive_storage_path.return_value = str(archive_dir) + + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_storage.build_path = str(build_folder) + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + assert not test_config.exists() + assert (archive_dir / configuration).exists() + + assert not build_folder.exists() + assert not (archive_dir / "test_device").exists() + + +@pytest.mark.asyncio +async def test_archive_handler_no_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + mock_storage_json: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json but no build folder.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = str(test_config) + mock_archive_storage_path.return_value = str(archive_dir) + + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_storage.build_path = None + mock_storage_json.load.return_value = mock_storage + + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + assert not test_config.exists() + assert (archive_dir / configuration).exists() + assert not (archive_dir / "test_device").exists() + + @pytest.mark.skipif(os.name == "nt", reason="Unix sockets are not supported on Windows") @pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path") def test_start_web_server_with_unix_socket(tmp_path: Path) -> None: diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index e520db9e33..7d3b90794b 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -384,6 +384,9 @@ def test_preload_core_config_basic(setup_core: Path) -> None: assert platform == "esp32" assert KEY_CORE in CORE.data assert CONF_BUILD_PATH in config[CONF_ESPHOME] + # Verify default build path is "build/" + build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] + assert build_path.endswith(os.path.join("build", "test_device")) def test_preload_core_config_with_build_path(setup_core: Path) -> None: @@ -418,6 +421,12 @@ def test_preload_core_config_env_build_path(setup_core: Path) -> None: assert CONF_BUILD_PATH in config[CONF_ESPHOME] assert "test_device" in config[CONF_ESPHOME][CONF_BUILD_PATH] + # Verify it uses the env var path with device name appended + build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] + expected_path = os.path.join("/env/build", "test_device") + assert build_path == expected_path or build_path == expected_path.replace( + "/", os.sep + ) assert platform == "rp2040"