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 ,
2223 ATTR_CONTENT ,
2324 ATTR_DATE ,
2425 ATTR_DAYS_UNTIL_STALE ,
26+ ATTR_FILENAME ,
2527 ATTR_FOLDERS ,
2628 ATTR_HOMEASSISTANT ,
2729 ATTR_HOMEASSISTANT_EXCLUDE_DATABASE ,
28- ATTR_LOCATON ,
30+ ATTR_JOB_ID ,
31+ ATTR_LOCATION ,
2932 ATTR_NAME ,
3033 ATTR_PASSWORD ,
3134 ATTR_PROTECTED ,
3639 ATTR_TIMEOUT ,
3740 ATTR_TYPE ,
3841 ATTR_VERSION ,
42+ REQUEST_FROM ,
3943 BusEvent ,
4044 CoreState ,
4145)
4246from ..coresys import CoreSysAttributes
43- from ..exceptions import APIError
47+ from ..exceptions import APIError , APIForbidden
4448from ..jobs import JobSchedulerOptions
4549from ..mounts .const import MountUsage
4650from ..resolution .const import UnhealthyReason
47- from .const import ATTR_BACKGROUND , ATTR_JOB_ID , CONTENT_TYPE_TAR
51+ from .const import ATTR_BACKGROUND , ATTR_LOCATIONS , CONTENT_TYPE_TAR
4852from .utils import api_process , api_validate
4953
5054_LOGGER : logging .Logger = logging .getLogger (__name__ )
5155
5256RE_SLUGIFY_NAME = re .compile (r"[^A-Za-z0-9]+" )
57+ RE_BACKUP_FILENAME = re .compile (r"^[^\\\/]+\.tar$" )
5358
5459# Backwards compatible
5560# Remove: 2022.08
7681 vol .Optional (ATTR_NAME ): str ,
7782 vol .Optional (ATTR_PASSWORD ): vol .Maybe (str ),
7883 vol .Optional (ATTR_COMPRESSED ): vol .Maybe (vol .Boolean ()),
79- vol .Optional (ATTR_LOCATON ): vol .Maybe (str ),
84+ vol .Optional (ATTR_LOCATION ): vol .Maybe (str ),
8085 vol .Optional (ATTR_HOMEASSISTANT_EXCLUDE_DATABASE ): vol .Boolean (),
8186 vol .Optional (ATTR_BACKGROUND , default = False ): vol .Boolean (),
8287 }
101106 vol .Optional (ATTR_TIMEOUT ): vol .All (int , vol .Range (min = 1 )),
102107 }
103108)
109+ SCHEMA_RELOAD = vol .Schema (
110+ {
111+ vol .Inclusive (ATTR_LOCATION , "file" ): vol .Maybe (str ),
112+ vol .Inclusive (ATTR_FILENAME , "file" ): vol .Match (RE_BACKUP_FILENAME ),
113+ }
114+ )
104115
105116
106117class APIBackups (CoreSysAttributes ):
@@ -122,7 +133,8 @@ def _list_backups(self):
122133 ATTR_DATE : backup .date ,
123134 ATTR_TYPE : backup .sys_type ,
124135 ATTR_SIZE : backup .size ,
125- ATTR_LOCATON : backup .location ,
136+ ATTR_LOCATION : backup .location ,
137+ ATTR_LOCATIONS : backup .locations ,
126138 ATTR_PROTECTED : backup .protected ,
127139 ATTR_COMPRESSED : backup .compressed ,
128140 ATTR_CONTENT : {
@@ -132,6 +144,7 @@ def _list_backups(self):
132144 },
133145 }
134146 for backup in self .sys_backups .list_backups
147+ if backup .location != LOCATION_CLOUD_BACKUP
135148 ]
136149
137150 @api_process
@@ -164,10 +177,13 @@ async def options(self, request):
164177 self .sys_backups .save_data ()
165178
166179 @api_process
167- async def reload (self , _ ):
180+ async def reload (self , request : web . Request ):
168181 """Reload backup list."""
169- await asyncio .shield (self .sys_backups .reload ())
170- return True
182+ body = await api_validate (SCHEMA_RELOAD , request )
183+ self ._validate_cloud_backup_location (request , body .get (ATTR_LOCATION ))
184+ backup = self ._location_to_mount (body )
185+
186+ return await asyncio .shield (self .sys_backups .reload (** backup ))
171187
172188 @api_process
173189 async def backup_info (self , request ):
@@ -195,7 +211,8 @@ async def backup_info(self, request):
195211 ATTR_PROTECTED : backup .protected ,
196212 ATTR_SUPERVISOR_VERSION : backup .supervisor_version ,
197213 ATTR_HOMEASSISTANT : backup .homeassistant_version ,
198- ATTR_LOCATON : backup .location ,
214+ ATTR_LOCATION : backup .location ,
215+ ATTR_LOCATIONS : backup .locations ,
199216 ATTR_ADDONS : data_addons ,
200217 ATTR_REPOSITORIES : backup .repositories ,
201218 ATTR_FOLDERS : backup .folders ,
@@ -204,17 +221,29 @@ async def backup_info(self, request):
204221
205222 def _location_to_mount (self , body : dict [str , Any ]) -> dict [str , Any ]:
206223 """Change location field to mount if necessary."""
207- if not body .get (ATTR_LOCATON ) :
224+ if not body .get (ATTR_LOCATION ) or body [ ATTR_LOCATION ] == LOCATION_CLOUD_BACKUP :
208225 return body
209226
210- body [ATTR_LOCATON ] = self .sys_mounts .get (body [ATTR_LOCATON ])
211- if body [ATTR_LOCATON ].usage != MountUsage .BACKUP :
227+ body [ATTR_LOCATION ] = self .sys_mounts .get (body [ATTR_LOCATION ])
228+ if body [ATTR_LOCATION ].usage != MountUsage .BACKUP :
212229 raise APIError (
213- f"Mount { body [ATTR_LOCATON ].name } is not used for backups, cannot backup to there"
230+ f"Mount { body [ATTR_LOCATION ].name } is not used for backups, cannot backup to there"
214231 )
215232
216233 return body
217234
235+ def _validate_cloud_backup_location (
236+ self , request : web .Request , location : str | None
237+ ) -> None :
238+ """Cloud backup location is only available to Home Assistant."""
239+ if (
240+ location == LOCATION_CLOUD_BACKUP
241+ and request .get (REQUEST_FROM ) != self .sys_homeassistant
242+ ):
243+ raise APIForbidden (
244+ f"Location { LOCATION_CLOUD_BACKUP } is only available for Home Assistant"
245+ )
246+
218247 async def _background_backup_task (
219248 self , backup_method : Callable , * args , ** kwargs
220249 ) -> tuple [asyncio .Task , str ]:
@@ -246,9 +275,10 @@ async def release_on_freeze(new_state: CoreState):
246275 self .sys_bus .remove_listener (listener )
247276
248277 @api_process
249- async def backup_full (self , request ):
278+ async def backup_full (self , request : web . Request ):
250279 """Create full backup."""
251280 body = await api_validate (SCHEMA_BACKUP_FULL , request )
281+ self ._validate_cloud_backup_location (request , body .get (ATTR_LOCATION ))
252282 background = body .pop (ATTR_BACKGROUND )
253283 backup_task , job_id = await self ._background_backup_task (
254284 self .sys_backups .do_backup_full , ** self ._location_to_mount (body )
@@ -266,9 +296,10 @@ async def backup_full(self, request):
266296 )
267297
268298 @api_process
269- async def backup_partial (self , request ):
299+ async def backup_partial (self , request : web . Request ):
270300 """Create a partial backup."""
271301 body = await api_validate (SCHEMA_BACKUP_PARTIAL , request )
302+ self ._validate_cloud_backup_location (request , body .get (ATTR_LOCATION ))
272303 background = body .pop (ATTR_BACKGROUND )
273304 backup_task , job_id = await self ._background_backup_task (
274305 self .sys_backups .do_backup_partial , ** self ._location_to_mount (body )
@@ -286,9 +317,10 @@ async def backup_partial(self, request):
286317 )
287318
288319 @api_process
289- async def restore_full (self , request ):
320+ async def restore_full (self , request : web . Request ):
290321 """Full restore of a backup."""
291322 backup = self ._extract_slug (request )
323+ self ._validate_cloud_backup_location (request , backup .location )
292324 body = await api_validate (SCHEMA_RESTORE_FULL , request )
293325 background = body .pop (ATTR_BACKGROUND )
294326 restore_task , job_id = await self ._background_backup_task (
@@ -303,9 +335,10 @@ async def restore_full(self, request):
303335 )
304336
305337 @api_process
306- async def restore_partial (self , request ):
338+ async def restore_partial (self , request : web . Request ):
307339 """Partial restore a backup."""
308340 backup = self ._extract_slug (request )
341+ self ._validate_cloud_backup_location (request , backup .location )
309342 body = await api_validate (SCHEMA_RESTORE_PARTIAL , request )
310343 background = body .pop (ATTR_BACKGROUND )
311344 restore_task , job_id = await self ._background_backup_task (
@@ -320,23 +353,24 @@ async def restore_partial(self, request):
320353 )
321354
322355 @api_process
323- async def freeze (self , request ):
356+ async def freeze (self , request : web . Request ):
324357 """Initiate manual freeze for external backup."""
325358 body = await api_validate (SCHEMA_FREEZE , request )
326359 await asyncio .shield (self .sys_backups .freeze_all (** body ))
327360
328361 @api_process
329- async def thaw (self , request ):
362+ async def thaw (self , request : web . Request ):
330363 """Begin thaw after manual freeze."""
331364 await self .sys_backups .thaw_all ()
332365
333366 @api_process
334- async def remove (self , request ):
367+ async def remove (self , request : web . Request ):
335368 """Remove a backup."""
336369 backup = self ._extract_slug (request )
370+ self ._validate_cloud_backup_location (request , backup .location )
337371 return self .sys_backups .remove (backup )
338372
339- async def download (self , request ):
373+ async def download (self , request : web . Request ):
340374 """Download a backup file."""
341375 backup = self ._extract_slug (request )
342376
@@ -349,7 +383,7 @@ async def download(self, request):
349383 return response
350384
351385 @api_process
352- async def upload (self , request ):
386+ async def upload (self , request : web . Request ):
353387 """Upload a backup file."""
354388 with TemporaryDirectory (dir = str (self .sys_config .path_tmp )) as temp_dir :
355389 tar_file = Path (temp_dir , "backup.tar" )
0 commit comments