diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index d91a15a8d..cc77f00e4 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -1,7 +1,6 @@ """Init file for Hass.io add-ons.""" from contextlib import suppress from copy import deepcopy -import json import logging from pathlib import Path, PurePath import re @@ -29,7 +28,7 @@ from ..const import ( STATE_STARTED, STATE_STOPPED) from ..coresys import CoreSysAttributes from ..docker.addon import DockerAddon -from ..exceptions import HostAppArmorError +from ..exceptions import HostAppArmorError, JsonFileError from ..utils import create_token from ..utils.apparmor import adjust_profile from ..utils.json import read_json_file, write_json_file @@ -616,8 +615,8 @@ class Addon(CoreSysAttributes): except vol.Invalid as ex: _LOGGER.error("Add-on %s have wrong options: %s", self._id, humanize_error(options, ex)) - except (OSError, json.JSONDecodeError) as err: - _LOGGER.error("Add-on %s can't write options: %s", self._id, err) + except JsonFileError: + _LOGGER.error("Add-on %s can't write options", self._id) else: return True @@ -892,8 +891,8 @@ class Addon(CoreSysAttributes): # Store local configs/state try: write_json_file(Path(temp, 'addon.json'), data) - except (OSError, json.JSONDecodeError) as err: - _LOGGER.error("Can't save meta for %s: %s", self._id, err) + except JsonFileError: + _LOGGER.error("Can't save meta for %s", self._id) return False # Store AppArmor Profile @@ -940,8 +939,8 @@ class Addon(CoreSysAttributes): # Read snapshot data try: data = read_json_file(Path(temp, 'addon.json')) - except (OSError, json.JSONDecodeError) as err: - _LOGGER.error("Can't read addon.json: %s", err) + except JsonFileError: + return False # Validate try: diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 455d96356..881493b0a 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -1,19 +1,25 @@ """Init file for Hass.io add-on data.""" import logging -import json from pathlib import Path import voluptuous as vol from voluptuous.humanize import humanize_error -from .utils import extract_hash_from_path -from .validate import ( - SCHEMA_ADDON_CONFIG, SCHEMA_ADDONS_FILE, SCHEMA_REPOSITORY_CONFIG) from ..const import ( - FILE_HASSIO_ADDONS, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON, - REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM) + ATTR_LOCATON, + ATTR_REPOSITORY, + ATTR_SLUG, + ATTR_SYSTEM, + ATTR_USER, + FILE_HASSIO_ADDONS, + REPOSITORY_CORE, + REPOSITORY_LOCAL, +) from ..coresys import CoreSysAttributes +from ..exceptions import JsonFileError from ..utils.json import JsonConfig, read_json_file +from .utils import extract_hash_from_path +from .validate import SCHEMA_ADDON_CONFIG, SCHEMA_ADDONS_FILE, SCHEMA_REPOSITORY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -54,12 +60,10 @@ class AddonsData(JsonConfig, CoreSysAttributes): self._repositories = {} # read core repository - self._read_addons_folder( - self.sys_config.path_addons_core, REPOSITORY_CORE) + self._read_addons_folder(self.sys_config.path_addons_core, REPOSITORY_CORE) # read local repository - self._read_addons_folder( - self.sys_config.path_addons_local, REPOSITORY_LOCAL) + self._read_addons_folder(self.sys_config.path_addons_local, REPOSITORY_LOCAL) # add built-in repositories information self._set_builtin_repositories() @@ -76,15 +80,12 @@ class AddonsData(JsonConfig, CoreSysAttributes): # exists repository json repository_file = Path(path, "repository.json") try: - repository_info = SCHEMA_REPOSITORY_CONFIG( - read_json_file(repository_file) + repository_info = SCHEMA_REPOSITORY_CONFIG(read_json_file(repository_file)) + except JsonFileError: + _LOGGER.warning( + "Can't read repository information from %s", repository_file ) - - except (OSError, json.JSONDecodeError, UnicodeDecodeError): - _LOGGER.warning("Can't read repository information from %s", - repository_file) return - except vol.Invalid: _LOGGER.warning("Repository parse error %s", repository_file) return @@ -98,23 +99,21 @@ class AddonsData(JsonConfig, CoreSysAttributes): for addon in path.glob("**/config.json"): try: addon_config = read_json_file(addon) - - except (OSError, json.JSONDecodeError, UnicodeDecodeError): - _LOGGER.warning("Can't read %s", addon) + except JsonFileError: + _LOGGER.warning("Can't read %s from repository %s", addon, repository) continue # validate try: addon_config = SCHEMA_ADDON_CONFIG(addon_config) - except vol.Invalid as ex: - _LOGGER.warning("Can't read %s: %s", addon, - humanize_error(addon_config, ex)) + _LOGGER.warning( + "Can't read %s: %s", addon, humanize_error(addon_config, ex) + ) continue # Generate slug - addon_slug = "{}_{}".format( - repository, addon_config[ATTR_SLUG]) + addon_slug = "{}_{}".format(repository, addon_config[ATTR_SLUG]) # store addon_config[ATTR_REPOSITORY] = repository @@ -126,14 +125,12 @@ class AddonsData(JsonConfig, CoreSysAttributes): try: builtin_file = Path(__file__).parent.joinpath("built-in.json") builtin_data = read_json_file(builtin_file) - except (OSError, json.JSONDecodeError) as err: - _LOGGER.warning("Can't read built-in json: %s", err) + except JsonFileError: + _LOGGER.warning("Can't read built-in json") return # core repository - self._repositories[REPOSITORY_CORE] = \ - builtin_data[REPOSITORY_CORE] + self._repositories[REPOSITORY_CORE] = builtin_data[REPOSITORY_CORE] # local repository - self._repositories[REPOSITORY_LOCAL] = \ - builtin_data[REPOSITORY_LOCAL] + self._repositories[REPOSITORY_LOCAL] = builtin_data[REPOSITORY_LOCAL] diff --git a/hassio/arch.py b/hassio/arch.py index e300fb8fe..7fe7b8d07 100644 --- a/hassio/arch.py +++ b/hassio/arch.py @@ -1,11 +1,10 @@ """Handle Arch for underlay maschine/platforms.""" -import json import logging from typing import List from pathlib import Path from .coresys import CoreSysAttributes, CoreSys -from .exceptions import HassioArchNotFound +from .exceptions import HassioArchNotFound, JsonFileError from .utils.json import read_json_file _LOGGER = logging.getLogger(__name__) @@ -38,10 +37,9 @@ class CpuArch(CoreSysAttributes): async def load(self) -> None: """Load data and initialize default arch.""" try: - arch_file = Path(__file__).parent.joinpath("arch.json") - arch_data = read_json_file(arch_file) - except (OSError, json.JSONDecodeError) as err: - _LOGGER.warning("Can't read arch json: %s", err) + arch_data = read_json_file(Path(__file__).parent.joinpath("arch.json")) + except JsonFileError: + _LOGGER.warning("Can't read arch json") return # Evaluate current CPU/Platform diff --git a/hassio/exceptions.py b/hassio/exceptions.py index fabef962d..fb44083b8 100644 --- a/hassio/exceptions.py +++ b/hassio/exceptions.py @@ -137,3 +137,10 @@ class AppArmorFileError(AppArmorError): class AppArmorInvalidError(AppArmorError): """AppArmor profile validate error.""" + + +# util/json + + +class JsonFileError(HassioError): + """Invalid json file.""" diff --git a/hassio/utils/json.py b/hassio/utils/json.py index dc6f8e006..fb7e2b8fd 100644 --- a/hassio/utils/json.py +++ b/hassio/utils/json.py @@ -1,82 +1,89 @@ """Tools file for Hass.io.""" import json import logging +from pathlib import Path +from typing import Any, Dict import voluptuous as vol from voluptuous.humanize import humanize_error +from ..exceptions import JsonFileError + _LOGGER = logging.getLogger(__name__) -def write_json_file(jsonfile, data): +def write_json_file(jsonfile: Path, data: Any) -> None: """Write a JSON file.""" - json_str = json.dumps(data, indent=2) - with jsonfile.open('w') as conf_file: - conf_file.write(json_str) + try: + jsonfile.write_text(json.dumps(data, indent=2)) + except (OSError, ValueError, TypeError) as err: + _LOGGER.error("Can't write %s: %s", jsonfile, err) + raise JsonFileError() from None -def read_json_file(jsonfile): +def read_json_file(jsonfile: Path) -> Any: """Read a JSON file and return a dict.""" - with jsonfile.open('r') as cfile: - return json.loads(cfile.read()) + try: + return json.loads(jsonfile.read_text()) + except (OSError, ValueError, TypeError, UnicodeDecodeError) as err: + _LOGGER.error("Can't read json from %s: %s", jsonfile, err) + raise JsonFileError() from None class JsonConfig: """Hass core object for handle it.""" - def __init__(self, json_file, schema): + def __init__(self, json_file: Path, schema: vol.Schema): """Initialize hass object.""" - self._file = json_file - self._schema = schema - self._data = {} + self._file: Path = json_file + self._schema: vol.Schema = schema + self._data: Dict[str, Any] = {} self.read_data() - def reset_data(self): + def reset_data(self) -> None: """Reset JSON file to default.""" try: self._data = self._schema({}) except vol.Invalid as ex: - _LOGGER.error("Can't reset %s: %s", - self._file, humanize_error(self._data, ex)) + _LOGGER.error( + "Can't reset %s: %s", self._file, humanize_error(self._data, ex) + ) - def read_data(self): + def read_data(self) -> None: """Read JSON file & validate.""" if self._file.is_file(): try: self._data = read_json_file(self._file) - except (OSError, json.JSONDecodeError, UnicodeDecodeError): - _LOGGER.warning("Can't read %s", self._file) + except JsonFileError: self._data = {} # Validate try: self._data = self._schema(self._data) except vol.Invalid as ex: - _LOGGER.error("Can't parse %s: %s", - self._file, humanize_error(self._data, ex)) + _LOGGER.error( + "Can't parse %s: %s", self._file, humanize_error(self._data, ex) + ) # Reset data to default _LOGGER.warning("Reset %s to default", self._file) self._data = self._schema({}) - def save_data(self): + def save_data(self) -> None: """Store data to configuration file.""" # Validate try: self._data = self._schema(self._data) except vol.Invalid as ex: - _LOGGER.error("Can't parse data: %s", - humanize_error(self._data, ex)) + _LOGGER.error("Can't parse data: %s", humanize_error(self._data, ex)) # Load last valid data _LOGGER.warning("Reset %s to last version", self._file) self.read_data() - return - - # write - try: - write_json_file(self._file, self._data) - except (OSError, json.JSONDecodeError) as err: - _LOGGER.error( - "Can't store configuration in %s: %s", self._file, err) + else: + # write + try: + write_json_file(self._file, self._data) + except JsonFileError: + pass