Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for functionality to download files."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 import os
7 import re
8 import threading
9 
10 import requests
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.core import HomeAssistant, ServiceCall
16 from homeassistant.helpers.service import async_register_admin_service
17 from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
18 
19 from .const import (
20  _LOGGER,
21  ATTR_FILENAME,
22  ATTR_OVERWRITE,
23  ATTR_SUBDIR,
24  ATTR_URL,
25  CONF_DOWNLOAD_DIR,
26  DOMAIN,
27  DOWNLOAD_COMPLETED_EVENT,
28  DOWNLOAD_FAILED_EVENT,
29  SERVICE_DOWNLOAD_FILE,
30 )
31 
32 
33 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
34  """Listen for download events to download files."""
35  download_path = entry.data[CONF_DOWNLOAD_DIR]
36 
37  # If path is relative, we assume relative to Home Assistant config dir
38  if not os.path.isabs(download_path):
39  download_path = hass.config.path(download_path)
40 
41  if not await hass.async_add_executor_job(os.path.isdir, download_path):
42  _LOGGER.error(
43  "Download path %s does not exist. File Downloader not active", download_path
44  )
45  return False
46 
47  def download_file(service: ServiceCall) -> None:
48  """Start thread to download file specified in the URL."""
49 
50  def do_download() -> None:
51  """Download the file."""
52  try:
53  url = service.data[ATTR_URL]
54 
55  subdir = service.data.get(ATTR_SUBDIR)
56 
57  filename = service.data.get(ATTR_FILENAME)
58 
59  overwrite = service.data.get(ATTR_OVERWRITE)
60 
61  if subdir:
62  # Check the path
63  raise_if_invalid_path(subdir)
64 
65  final_path = None
66 
67  req = requests.get(url, stream=True, timeout=10)
68 
69  if req.status_code != HTTPStatus.OK:
70  _LOGGER.warning(
71  "Downloading '%s' failed, status_code=%d", url, req.status_code
72  )
73  hass.bus.fire(
74  f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
75  {"url": url, "filename": filename},
76  )
77 
78  else:
79  if filename is None and "content-disposition" in req.headers:
80  match = re.findall(
81  r"filename=(\S+)", req.headers["content-disposition"]
82  )
83 
84  if match:
85  filename = match[0].strip("'\" ")
86 
87  if not filename:
88  filename = os.path.basename(url).strip()
89 
90  if not filename:
91  filename = "ha_download"
92 
93  # Check the filename
95 
96  # Do we want to download to subdir, create if needed
97  if subdir:
98  subdir_path = os.path.join(download_path, subdir)
99 
100  # Ensure subdir exist
101  os.makedirs(subdir_path, exist_ok=True)
102 
103  final_path = os.path.join(subdir_path, filename)
104 
105  else:
106  final_path = os.path.join(download_path, filename)
107 
108  path, ext = os.path.splitext(final_path)
109 
110  # If file exist append a number.
111  # We test filename, filename_2..
112  if not overwrite:
113  tries = 1
114  final_path = path + ext
115  while os.path.isfile(final_path):
116  tries += 1
117 
118  final_path = f"{path}_{tries}.{ext}"
119 
120  _LOGGER.debug("%s -> %s", url, final_path)
121 
122  with open(final_path, "wb") as fil:
123  for chunk in req.iter_content(1024):
124  fil.write(chunk)
125 
126  _LOGGER.debug("Downloading of %s done", url)
127  hass.bus.fire(
128  f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}",
129  {"url": url, "filename": filename},
130  )
131 
132  except requests.exceptions.ConnectionError:
133  _LOGGER.exception("ConnectionError occurred for %s", url)
134  hass.bus.fire(
135  f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
136  {"url": url, "filename": filename},
137  )
138 
139  # Remove file if we started downloading but failed
140  if final_path and os.path.isfile(final_path):
141  os.remove(final_path)
142  except ValueError:
143  _LOGGER.exception("Invalid value")
144  hass.bus.fire(
145  f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
146  {"url": url, "filename": filename},
147  )
148 
149  # Remove file if we started downloading but failed
150  if final_path and os.path.isfile(final_path):
151  os.remove(final_path)
152 
153  threading.Thread(target=do_download).start()
154 
156  hass,
157  DOMAIN,
158  SERVICE_DOWNLOAD_FILE,
159  download_file,
160  schema=vol.Schema(
161  {
162  vol.Optional(ATTR_FILENAME): cv.string,
163  vol.Optional(ATTR_SUBDIR): cv.string,
164  vol.Required(ATTR_URL): cv.url,
165  vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
166  }
167  ),
168  )
169 
170  return True
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:33
None open(self, **Any kwargs)
Definition: lock.py:86
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121
None raise_if_invalid_path(str path)
Definition: __init__.py:32
None raise_if_invalid_filename(str filename)
Definition: __init__.py:23