data.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. """Data handler for HACS."""
  2. import asyncio
  3. from datetime import datetime
  4. from homeassistant.core import callback
  5. from homeassistant.exceptions import HomeAssistantError
  6. from homeassistant.util import json as json_util
  7. from ..base import HacsBase
  8. from ..enums import HacsDisabledReason, HacsDispatchEvent, HacsGitHubRepo
  9. from ..repositories.base import TOPIC_FILTER, HacsManifest, HacsRepository
  10. from .logger import LOGGER
  11. from .path import is_safe
  12. from .store import async_load_from_store, async_save_to_store
  13. DEFAULT_BASE_REPOSITORY_DATA = (
  14. ("authors", []),
  15. ("category", ""),
  16. ("description", ""),
  17. ("domain", None),
  18. ("downloads", 0),
  19. ("etag_repository", None),
  20. ("full_name", ""),
  21. ("last_updated", 0),
  22. ("hide", False),
  23. ("new", False),
  24. ("stargazers_count", 0),
  25. ("topics", []),
  26. )
  27. DEFAULT_EXTENDED_REPOSITORY_DATA = (
  28. ("archived", False),
  29. ("config_flow", False),
  30. ("default_branch", None),
  31. ("description", ""),
  32. ("first_install", False),
  33. ("installed_commit", None),
  34. ("installed", False),
  35. ("last_commit", None),
  36. ("last_version", None),
  37. ("manifest_name", None),
  38. ("open_issues", 0),
  39. ("published_tags", []),
  40. ("pushed_at", ""),
  41. ("releases", False),
  42. ("selected_tag", None),
  43. ("show_beta", False),
  44. ("stargazers_count", 0),
  45. ("topics", []),
  46. )
  47. class HacsData:
  48. """HacsData class."""
  49. def __init__(self, hacs: HacsBase):
  50. """Initialize."""
  51. self.logger = LOGGER
  52. self.hacs = hacs
  53. self.content = {}
  54. async def async_force_write(self, _=None):
  55. """Force write."""
  56. await self.async_write(force=True)
  57. async def async_write(self, force: bool = False) -> None:
  58. """Write content to the store files."""
  59. if not force and self.hacs.system.disabled:
  60. return
  61. self.logger.debug("<HacsData async_write> Saving data")
  62. # Hacs
  63. await async_save_to_store(
  64. self.hacs.hass,
  65. "hacs",
  66. {
  67. "archived_repositories": self.hacs.common.archived_repositories,
  68. "renamed_repositories": self.hacs.common.renamed_repositories,
  69. "ignored_repositories": self.hacs.common.ignored_repositories,
  70. },
  71. )
  72. await self._async_store_content_and_repos()
  73. async def _async_store_content_and_repos(self, _=None): # bb: ignore
  74. """Store the main repos file and each repo that is out of date."""
  75. # Repositories
  76. self.content = {}
  77. for repository in self.hacs.repositories.list_all:
  78. if repository.data.category in self.hacs.common.categories:
  79. self.async_store_repository_data(repository)
  80. await async_save_to_store(self.hacs.hass, "repositories", self.content)
  81. for event in (HacsDispatchEvent.REPOSITORY, HacsDispatchEvent.CONFIG):
  82. self.hacs.async_dispatch(event, {})
  83. @callback
  84. def async_store_repository_data(self, repository: HacsRepository) -> dict:
  85. """Store the repository data."""
  86. data = {"repository_manifest": repository.repository_manifest.manifest}
  87. for key, default_value in DEFAULT_BASE_REPOSITORY_DATA:
  88. if (value := repository.data.__getattribute__(key)) != default_value:
  89. data[key] = value
  90. if repository.data.installed:
  91. for key, default_value in DEFAULT_EXTENDED_REPOSITORY_DATA:
  92. if (value := repository.data.__getattribute__(key)) != default_value:
  93. data[key] = value
  94. data["version_installed"] = repository.data.installed_version
  95. if repository.data.last_fetched:
  96. data["last_fetched"] = repository.data.last_fetched.timestamp()
  97. self.content[str(repository.data.id)] = data
  98. async def restore(self):
  99. """Restore saved data."""
  100. self.hacs.status.new = False
  101. try:
  102. hacs = await async_load_from_store(self.hacs.hass, "hacs") or {}
  103. except HomeAssistantError:
  104. hacs = {}
  105. try:
  106. repositories = await async_load_from_store(self.hacs.hass, "repositories") or {}
  107. except HomeAssistantError as exception:
  108. self.hacs.log.error(
  109. "Could not read %s, restore the file from a backup - %s",
  110. self.hacs.hass.config.path(".storage/hacs.repositories"),
  111. exception,
  112. )
  113. self.hacs.disable_hacs(HacsDisabledReason.RESTORE)
  114. return False
  115. if not hacs and not repositories:
  116. # Assume new install
  117. self.hacs.status.new = True
  118. self.logger.info("<HacsData restore> Loading base repository information")
  119. repositories = await self.hacs.hass.async_add_executor_job(
  120. json_util.load_json,
  121. f"{self.hacs.core.config_path}/custom_components/hacs/utils/default.repositories",
  122. )
  123. self.logger.info("<HacsData restore> Restore started")
  124. # Hacs
  125. self.hacs.common.archived_repositories = []
  126. self.hacs.common.ignored_repositories = []
  127. self.hacs.common.renamed_repositories = {}
  128. # Clear out doubble renamed values
  129. renamed = hacs.get("renamed_repositories", {})
  130. for entry in renamed:
  131. value = renamed.get(entry)
  132. if value not in renamed:
  133. self.hacs.common.renamed_repositories[entry] = value
  134. # Clear out doubble archived values
  135. for entry in hacs.get("archived_repositories", []):
  136. if entry not in self.hacs.common.archived_repositories:
  137. self.hacs.common.archived_repositories.append(entry)
  138. # Clear out doubble ignored values
  139. for entry in hacs.get("ignored_repositories", []):
  140. if entry not in self.hacs.common.ignored_repositories:
  141. self.hacs.common.ignored_repositories.append(entry)
  142. try:
  143. await self.register_unknown_repositories(repositories)
  144. for entry, repo_data in repositories.items():
  145. if entry == "0":
  146. # Ignore repositories with ID 0
  147. self.logger.debug(
  148. "<HacsData restore> Found repository with ID %s - %s", entry, repo_data
  149. )
  150. continue
  151. self.async_restore_repository(entry, repo_data)
  152. self.logger.info("<HacsData restore> Restore done")
  153. except BaseException as exception: # lgtm [py/catch-base-exception] pylint: disable=broad-except
  154. self.logger.critical(
  155. "<HacsData restore> [%s] Restore Failed!", exception, exc_info=exception
  156. )
  157. return False
  158. return True
  159. async def register_unknown_repositories(self, repositories):
  160. """Registry any unknown repositories."""
  161. register_tasks = [
  162. self.hacs.async_register_repository(
  163. repository_full_name=repo_data["full_name"],
  164. category=repo_data["category"],
  165. check=False,
  166. repository_id=entry,
  167. )
  168. for entry, repo_data in repositories.items()
  169. if entry != "0" and not self.hacs.repositories.is_registered(repository_id=entry)
  170. ]
  171. if register_tasks:
  172. await asyncio.gather(*register_tasks)
  173. @callback
  174. def async_restore_repository(self, entry, repository_data):
  175. """Restore repository."""
  176. full_name = repository_data["full_name"]
  177. if not (repository := self.hacs.repositories.get_by_full_name(full_name)):
  178. self.logger.error("<HacsData restore> Did not find %s (%s)", full_name, entry)
  179. return
  180. # Restore repository attributes
  181. self.hacs.repositories.set_repository_id(repository, entry)
  182. repository.data.authors = repository_data.get("authors", [])
  183. repository.data.description = repository_data.get("description", "")
  184. repository.data.downloads = repository_data.get("downloads", 0)
  185. repository.data.last_updated = repository_data.get("last_updated", 0)
  186. repository.data.etag_repository = repository_data.get("etag_repository")
  187. repository.data.topics = [
  188. topic for topic in repository_data.get("topics", []) if topic not in TOPIC_FILTER
  189. ]
  190. repository.data.domain = repository_data.get("domain")
  191. repository.data.stargazers_count = repository_data.get(
  192. "stargazers_count"
  193. ) or repository_data.get("stars", 0)
  194. repository.releases.last_release = repository_data.get("last_release_tag")
  195. repository.data.releases = repository_data.get("releases", False)
  196. repository.data.installed = repository_data.get("installed", False)
  197. repository.data.new = repository_data.get("new", False)
  198. repository.data.selected_tag = repository_data.get("selected_tag")
  199. repository.data.show_beta = repository_data.get("show_beta", False)
  200. repository.data.last_version = repository_data.get("last_version")
  201. repository.data.last_commit = repository_data.get("last_commit")
  202. repository.data.installed_version = repository_data.get("version_installed")
  203. repository.data.installed_commit = repository_data.get("installed_commit")
  204. repository.data.manifest_name = repository_data.get("manifest_name")
  205. if last_fetched := repository_data.get("last_fetched"):
  206. repository.data.last_fetched = datetime.fromtimestamp(last_fetched)
  207. repository.repository_manifest = HacsManifest.from_dict(
  208. repository_data.get("repository_manifest", {})
  209. )
  210. if repository.localpath is not None and is_safe(self.hacs, repository.localpath):
  211. # Set local path
  212. repository.content.path.local = repository.localpath
  213. if repository.data.installed:
  214. repository.data.first_install = False
  215. if full_name == HacsGitHubRepo.INTEGRATION:
  216. repository.data.installed_version = self.hacs.version
  217. repository.data.installed = True