1414import voluptuous as vol
1515
1616from ..backups .backup import Backup
17+ from ..backups .const import LOCATION_CLOUD_BACKUP
1718from ..backups .validate import ALL_FOLDERS , FOLDER_HOMEASSISTANT , days_until_stale
1819from ..const import (
1920 ATTR_ADDONS ,
21+ ATTR_BACKUP ,
2022 ATTR_BACKUPS ,
2123 ATTR_COMPRESSED ,
2224 ATTR_CONTENT ,
2325 ATTR_DATE ,
2426 ATTR_DAYS_UNTIL_STALE ,
27+ ATTR_FILENAME ,
2528 ATTR_FOLDERS ,
2629 ATTR_HOMEASSISTANT ,
2730 ATTR_HOMEASSISTANT_EXCLUDE_DATABASE ,
28- ATTR_LOCATON ,
31+ ATTR_JOB_ID ,
32+ ATTR_LOCATION ,
2933 ATTR_NAME ,
3034 ATTR_PASSWORD ,
3135 ATTR_PROTECTED ,
3640 ATTR_TIMEOUT ,
3741 ATTR_TYPE ,
3842 ATTR_VERSION ,
43+ REQUEST_FROM ,
3944 BusEvent ,
4045 CoreState ,
4146)
4247from ..coresys import CoreSysAttributes
43- from ..exceptions import APIError
48+ from ..exceptions import APIError , APIForbidden
4449from ..jobs import JobSchedulerOptions
4550from ..mounts .const import MountUsage
4651from ..resolution .const import UnhealthyReason
47- from .const import ATTR_BACKGROUND , ATTR_JOB_ID , CONTENT_TYPE_TAR
52+ from .const import ATTR_BACKGROUND , ATTR_LOCATIONS , CONTENT_TYPE_TAR
4853from .utils import api_process , api_validate
4954
5055_LOGGER : logging .Logger = logging .getLogger (__name__ )
5156
5257RE_SLUGIFY_NAME = re .compile (r"[^A-Za-z0-9]+" )
58+ RE_FILENAME = re .compile (r"^[^\\\/]+" )
5359
5460# Backwards compatible
5561# Remove: 2022.08
7682 vol .Optional (ATTR_NAME ): str ,
7783 vol .Optional (ATTR_PASSWORD ): vol .Maybe (str ),
7884 vol .Optional (ATTR_COMPRESSED ): vol .Maybe (vol .Boolean ()),
79- vol .Optional (ATTR_LOCATON ): vol .Maybe (str ),
85+ vol .Optional (ATTR_LOCATION ): vol .Maybe (str ),
8086 vol .Optional (ATTR_HOMEASSISTANT_EXCLUDE_DATABASE ): vol .Boolean (),
8187 vol .Optional (ATTR_BACKGROUND , default = False ): vol .Boolean (),
8288 }
101107 vol .Optional (ATTR_TIMEOUT ): vol .All (int , vol .Range (min = 1 )),
102108 }
103109)
110+ SCHEMA_RELOAD = vol .Schema (
111+ {
112+ vol .Optional (ATTR_BACKUP , default = {}): vol .Schema (
113+ {
114+ vol .Required (ATTR_LOCATION ): vol .Maybe (str ),
115+ vol .Required (ATTR_FILENAME ): vol .Match (RE_FILENAME ),
116+ }
117+ )
118+ }
119+ )
104120
105121
106122class APIBackups (CoreSysAttributes ):
@@ -122,7 +138,8 @@ def _list_backups(self):
122138 ATTR_DATE : backup .date ,
123139 ATTR_TYPE : backup .sys_type ,
124140 ATTR_SIZE : backup .size ,
125- ATTR_LOCATON : backup .location ,
141+ ATTR_LOCATION : backup .location ,
142+ ATTR_LOCATIONS : backup .locations ,
126143 ATTR_PROTECTED : backup .protected ,
127144 ATTR_COMPRESSED : backup .compressed ,
128145 ATTR_CONTENT : {
@@ -132,6 +149,7 @@ def _list_backups(self):
132149 },
133150 }
134151 for backup in self .sys_backups .list_backups
152+ if backup .location != LOCATION_CLOUD_BACKUP
135153 ]
136154
137155 @api_process
@@ -164,10 +182,12 @@ async def options(self, request):
164182 self .sys_backups .save_data ()
165183
166184 @api_process
167- async def reload (self , _ ):
185+ async def reload (self , request : web . Request ):
168186 """Reload backup list."""
169- await asyncio .shield (self .sys_backups .reload ())
170- return True
187+ body = await api_validate (SCHEMA_RELOAD , request )
188+ backup = self ._location_to_mount (body [ATTR_BACKUP ])
189+
190+ return await asyncio .shield (self .sys_backups .reload (** backup ))
171191
172192 @api_process
173193 async def backup_info (self , request ):
@@ -195,7 +215,8 @@ async def backup_info(self, request):
195215 ATTR_PROTECTED : backup .protected ,
196216 ATTR_SUPERVISOR_VERSION : backup .supervisor_version ,
197217 ATTR_HOMEASSISTANT : backup .homeassistant_version ,
198- ATTR_LOCATON : backup .location ,
218+ ATTR_LOCATION : backup .location ,
219+ ATTR_LOCATIONS : backup .locations ,
199220 ATTR_ADDONS : data_addons ,
200221 ATTR_REPOSITORIES : backup .repositories ,
201222 ATTR_FOLDERS : backup .folders ,
@@ -204,17 +225,29 @@ async def backup_info(self, request):
204225
205226 def _location_to_mount (self , body : dict [str , Any ]) -> dict [str , Any ]:
206227 """Change location field to mount if necessary."""
207- if not body .get (ATTR_LOCATON ) :
228+ if not body .get (ATTR_LOCATION ) or body [ ATTR_LOCATION ] == LOCATION_CLOUD_BACKUP :
208229 return body
209230
210- body [ATTR_LOCATON ] = self .sys_mounts .get (body [ATTR_LOCATON ])
211- if body [ATTR_LOCATON ].usage != MountUsage .BACKUP :
231+ body [ATTR_LOCATION ] = self .sys_mounts .get (body [ATTR_LOCATION ])
232+ if body [ATTR_LOCATION ].usage != MountUsage .BACKUP :
212233 raise APIError (
213- f"Mount { body [ATTR_LOCATON ].name } is not used for backups, cannot backup to there"
234+ f"Mount { body [ATTR_LOCATION ].name } is not used for backups, cannot backup to there"
214235 )
215236
216237 return body
217238
239+ def _validate_cloud_backup_location (
240+ self , request : web .Request , location : str | None
241+ ) -> None :
242+ """Cloud backup location is only available to Home Assistant."""
243+ if (
244+ location == LOCATION_CLOUD_BACKUP
245+ and request .get (REQUEST_FROM ) != self .sys_homeassistant
246+ ):
247+ raise APIForbidden (
248+ f"Location { LOCATION_CLOUD_BACKUP } is only available for Home Assistant"
249+ )
250+
218251 async def _background_backup_task (
219252 self , backup_method : Callable , * args , ** kwargs
220253 ) -> tuple [asyncio .Task , str ]:
@@ -246,9 +279,10 @@ async def release_on_freeze(new_state: CoreState):
246279 self .sys_bus .remove_listener (listener )
247280
248281 @api_process
249- async def backup_full (self , request ):
282+ async def backup_full (self , request : web . Request ):
250283 """Create full backup."""
251284 body = await api_validate (SCHEMA_BACKUP_FULL , request )
285+ self ._validate_cloud_backup_location (request , body .get (ATTR_LOCATION ))
252286 background = body .pop (ATTR_BACKGROUND )
253287 backup_task , job_id = await self ._background_backup_task (
254288 self .sys_backups .do_backup_full , ** self ._location_to_mount (body )
@@ -266,9 +300,10 @@ async def backup_full(self, request):
266300 )
267301
268302 @api_process
269- async def backup_partial (self , request ):
303+ async def backup_partial (self , request : web . Request ):
270304 """Create a partial backup."""
271305 body = await api_validate (SCHEMA_BACKUP_PARTIAL , request )
306+ self ._validate_cloud_backup_location (request , body .get (ATTR_LOCATION ))
272307 background = body .pop (ATTR_BACKGROUND )
273308 backup_task , job_id = await self ._background_backup_task (
274309 self .sys_backups .do_backup_partial , ** self ._location_to_mount (body )
@@ -286,9 +321,10 @@ async def backup_partial(self, request):
286321 )
287322
288323 @api_process
289- async def restore_full (self , request ):
324+ async def restore_full (self , request : web . Request ):
290325 """Full restore of a backup."""
291326 backup = self ._extract_slug (request )
327+ self ._validate_cloud_backup_location (request , backup .location )
292328 body = await api_validate (SCHEMA_RESTORE_FULL , request )
293329 background = body .pop (ATTR_BACKGROUND )
294330 restore_task , job_id = await self ._background_backup_task (
@@ -303,9 +339,10 @@ async def restore_full(self, request):
303339 )
304340
305341 @api_process
306- async def restore_partial (self , request ):
342+ async def restore_partial (self , request : web . Request ):
307343 """Partial restore a backup."""
308344 backup = self ._extract_slug (request )
345+ self ._validate_cloud_backup_location (request , backup .location )
309346 body = await api_validate (SCHEMA_RESTORE_PARTIAL , request )
310347 background = body .pop (ATTR_BACKGROUND )
311348 restore_task , job_id = await self ._background_backup_task (
@@ -320,23 +357,24 @@ async def restore_partial(self, request):
320357 )
321358
322359 @api_process
323- async def freeze (self , request ):
360+ async def freeze (self , request : web . Request ):
324361 """Initiate manual freeze for external backup."""
325362 body = await api_validate (SCHEMA_FREEZE , request )
326363 await asyncio .shield (self .sys_backups .freeze_all (** body ))
327364
328365 @api_process
329- async def thaw (self , request ):
366+ async def thaw (self , request : web . Request ):
330367 """Begin thaw after manual freeze."""
331368 await self .sys_backups .thaw_all ()
332369
333370 @api_process
334- async def remove (self , request ):
371+ async def remove (self , request : web . Request ):
335372 """Remove a backup."""
336373 backup = self ._extract_slug (request )
374+ self ._validate_cloud_backup_location (request , backup .location )
337375 return self .sys_backups .remove (backup )
338376
339- async def download (self , request ):
377+ async def download (self , request : web . Request ):
340378 """Download a backup file."""
341379 backup = self ._extract_slug (request )
342380
@@ -349,7 +387,7 @@ async def download(self, request):
349387 return response
350388
351389 @api_process
352- async def upload (self , request ):
390+ async def upload (self , request : web . Request ):
353391 """Upload a backup file."""
354392 with TemporaryDirectory (dir = str (self .sys_config .path_tmp )) as temp_dir :
355393 tar_file = Path (temp_dir , "backup.tar" )
0 commit comments