Repository: home-assistant/core

Issue:
Restoring a backup from a fresh instance fails
### The problem

When attempting to upload and restore a backup of a brand new instance of Home Assistant (without the backups directory) the uploader fails with a 500 status code.

No entries are seen in HA logs, however you can see the following in the HTTP request response:

```
Can't write backup file: [Errno 2] No such file or directory: '/workspaces/ha-googledrive-backup/config/backups/71cc4d50.tar'
```

As soon as the `backups` folder is manually created under /config the upload is successful and the rest of the upload flow resumes.

### What version of Home Assistant Core has the issue?

core-2025.5.0b5

### What was the last working version of Home Assistant Core?

_No response_

### What type of installation are you running?

Home Assistant Core

### Integration causing the issue

backup

### Link to integration documentation on our website

_No response_

### Diagnostics information

N/A

### Example YAML snippet

```yaml
N/A
```


### Anything in the logs that might be useful for us?

```txt
Can't write backup file: [Errno 2] No such file or directory: '/workspaces/ha-googledrive-backup/config/backups/71cc4d50.tar'
```


### Additional information

_No response_

Discussion hints:
home-assistant[bot]: 
Hey there @home-assistant/core, mind taking a look at this pull request as it has been labeled with an integration (`backup`) you are listed as a [code owner](https://github.com/home-assistant/core/blob/dev/CODEOWNERS#L183) for? Thanks!

<details>
<summary>Code owner commands</summary>


Code owners of `backup` can trigger bot actions by commenting:

- `@home-assistant close` Closes the pull request.
- `@home-assistant rename Awesome new title` Renames the pull request.
- `@home-assistant reopen` Reopen the pull request.
- `@home-assistant unassign backup` Removes the current integration label and assignees on the pull request, add the integration domain after the command.
- `@home-assistant add-label needs-more-information` Add a label (needs-more-information, problem in dependency, problem in custom component) to the pull request.
- `@home-assistant remove-label needs-more-information` Remove a label (needs-more-information, problem in dependency, problem in custom component) on the pull request.

</details>



Solution patch:
diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py
index 33405d97883..4d509003a21 100644
--- a/homeassistant/components/backup/manager.py
+++ b/homeassistant/components/backup/manager.py
@@ -1294,6 +1294,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
         if self._local_agent_id in agent_ids:
             local_agent = manager.local_backup_agents[self._local_agent_id]
             tar_file_path = local_agent.get_backup_path(backup.backup_id)
+            await async_add_executor_job(make_backup_dir, tar_file_path.parent)
             await async_add_executor_job(shutil.move, temp_file, tar_file_path)
         else:
             tar_file_path = temp_file


Test patch:
diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py
index 0797eef2274..ad90e2e23bf 100644
--- a/tests/components/backup/test_manager.py
+++ b/tests/components/backup/test_manager.py
@@ -1397,6 +1397,9 @@ async def test_receive_backup(
 
     with (
         patch("pathlib.Path.open", open_mock),
+        patch(
+            "homeassistant.components.backup.manager.make_backup_dir"
+        ) as make_backup_dir_mock,
         patch("shutil.move") as move_mock,
         patch(
             "homeassistant.components.backup.manager.read_backup",
@@ -1412,6 +1415,7 @@ async def test_receive_backup(
 
     assert resp.status == 201
     assert open_mock.call_count == open_call_count
+    assert make_backup_dir_mock.call_count == move_call_count + 1
     assert move_mock.call_count == move_call_count
     for index, name in enumerate(move_path_names):
         assert move_mock.call_args_list[index].args[1].name == name