New Addon

Google BK
This commit is contained in:
2023-04-23 18:17:03 +07:00
parent 1c7fd2b476
commit 702d080a1e
248 changed files with 72745 additions and 0 deletions
@@ -0,0 +1,11 @@
# flake8: noqa
from .config import Config, GenConfig, UPGRADE_OPTIONS
from .settings import Setting, _DEFAULTS, _VALIDATORS, _LOOKUP, VERSION, PRIVATE, isStaging, addon_config, _CONFIG
from .createoptions import CreateOptions
from .boolvalidator import BoolValidator
from .startable import Startable
from .listvalidator import ListValidator
from .durationasstringvalidator import DurationAsStringValidator
from .bytesizeasstringvalidator import BytesizeAsStringValidator
from .version import Version
from .durationparser import DurationParser
@@ -0,0 +1,18 @@
from .validator import Validator
from ..logger import getLogger
logger = getLogger(__name__)
class BoolValidator(Validator):
def __init__(self, name):
super().__init__(name)
def validate(self, value):
if value is None or (type(value) == str and len(value) == 0):
return None
return BoolValidator.strToBool(value)
@classmethod
def strToBool(cls, value) -> bool:
return str(value).lower() in ['true', 't', 'on', 'yes', 'y', '1', 'hai', 'si', 'omgyesplease']
@@ -0,0 +1,56 @@
import re
from injector import inject, singleton
SECOND_IDENTIFIERS = ["s", "sec", "secs", "second", "seconds"]
MINUTE_IDENTIFIERS = ["m", "min", "mins", "minute", "minutes"]
HOUR_IDENTIFIERS = ["h", "hr", "hour", "hours"]
DAY_IDENTIFIERS = ["d", "day", "days"]
NUMBER_REGEX = "^([0-9]*[.])?[0-9]+"
VALID_REGEX = "^[ ]*([0-9,]*\\.?[0-9]*)[ ]*(b|B|k|K|m|M|g|G|t|T|p|P|e|E|z|Z|y|Y)[a-zA-Z ]*[ ]*$"
BYTES_BASE = 1024
PREFIX_VALUES = {
"b": 1,
"k": BYTES_BASE,
"m": pow(BYTES_BASE, 2),
"g": pow(BYTES_BASE, 3),
"t": pow(BYTES_BASE, 4),
"p": pow(BYTES_BASE, 5),
"e": pow(BYTES_BASE, 6),
"z": pow(BYTES_BASE, 7),
"y": pow(BYTES_BASE, 8)
}
PREFIX_CANONICAL = ["", "K", "M", "G", "T", "P", "E", "Z", "Y"]
@singleton
class ByteFormatter():
@inject
def __init__(self):
pass
def parse(self, source: str):
source = source.lower()
match = re.match(VALID_REGEX, source.lower())
if not match:
raise ValueError()
number, prefix = match.group(1, 2)
if prefix not in PREFIX_VALUES:
raise ValueError()
return float(number) * PREFIX_VALUES[prefix]
def format(self, bytes):
for prefix in PREFIX_CANONICAL:
if bytes < BYTES_BASE:
if int(bytes) == bytes:
return f"{int(bytes)} {prefix}B"
else:
return f"{bytes} {prefix}B"
bytes /= BYTES_BASE
bytes *= BYTES_BASE
if int(bytes) == bytes:
return f"{int(bytes)} YB"
else:
return f"{bytes} YB"
@@ -0,0 +1,30 @@
from .byteformatter import ByteFormatter
from .validator import Validator
class BytesizeAsStringValidator(Validator):
def __init__(self, name, minimum=None, maximum=None):
super().__init__(name)
self.min = minimum
self.max = maximum
def validate(self, value):
if type(value) is str:
value = value.strip()
if value is None or (type(value) == str and len(value) == 0):
return None
try:
if type(value) == str:
value = ByteFormatter().parse(value)
value = float(value)
except ValueError:
self.raiseForValue(value)
if self.max is not None and value > self.max:
self.raiseForValue(value)
if self.min is not None and value < self.min:
self.raiseForValue(value)
return value
def formatForUi(self, value):
return ByteFormatter().format(value)
@@ -0,0 +1,307 @@
import json
import os
import os.path
import uuid
from typing import Any, Dict, List, Optional
from yarl import URL
from .settings import _LOOKUP, Setting, _VALIDATORS
from ..logger import getLogger
from backup.file import JsonFileSaver
logger = getLogger(__name__)
ALWAYS_KEEP = {
Setting.DAYS_BETWEEN_BACKUPS,
Setting.MAX_BACKUPS_IN_HA,
Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE,
}
KEEP_DEFAULT = {
Setting.SEND_ERROR_REPORTS,
Setting.IGNORE_UPGRADE_BACKUPS
}
# these are the options that should trigger a restart of the server
SERVER_OPTIONS = {
Setting.USE_SSL,
Setting.REQUIRE_LOGIN,
Setting.CERTFILE,
Setting.KEYFILE,
Setting.EXPOSE_EXTRA_SERVER
}
NON_UI_SETTING = {
Setting.SUPERVISOR_URL,
Setting.TOKEN_SERVER_HOSTS,
Setting.DRIVE_AUTHORIZE_URL,
Setting.DRIVE_DEVICE_CODE_URL,
Setting.DEFAULT_DRIVE_CLIENT_ID,
Setting.NEW_BACKUP_TIMEOUT_SECONDS,
Setting.LOG_LEVEL,
Setting.CONSOLE_LOG_LEVEL,
Setting.DEFAULT_SYNC_INTERVAL_VARIATION,
Setting.CACHE_WARMUP_MAX_SECONDS,
Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS,
Setting.WATCH_BACKUP_DIRECTORY,
Setting.TRACE_REQUESTS,
Setting.MAX_BACKOFF_SECONDS
}
UPGRADE_OPTIONS = {
Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: Setting.MAX_BACKUPS_IN_HA,
Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE,
Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS: Setting.DAYS_BETWEEN_BACKUPS,
Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS: Setting.IGNORE_OTHER_BACKUPS,
Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS: Setting.IGNORE_UPGRADE_BACKUPS,
Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP: Setting.DELETE_BEFORE_NEW_BACKUP,
Setting.DEPRECTAED_BACKUP_NAME: Setting.BACKUP_NAME,
Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: Setting.BACKUP_TIME_OF_DAY,
Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER: Setting.SPECIFY_BACKUP_FOLDER,
Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS: Setting.NOTIFY_FOR_STALE_BACKUPS,
Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR: Setting.ENABLE_BACKUP_STALE_SENSOR,
Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR: Setting.ENABLE_BACKUP_STATE_SENSOR,
Setting.DEPRECATED_BACKUP_PASSWORD: Setting.BACKUP_PASSWORD
}
EMPTY_IS_DEFAULT = {
Setting.ACCENT_COLOR,
Setting.BACKGROUND_COLOR,
}
class GenConfig():
def __init__(self, days=0, weeks=0, months=0, years=0, day_of_week='mon', day_of_month=1, day_of_year=1, aggressive=False):
self.days = days
self.weeks = weeks
self.months = months
self.years = years
self.day_of_week = day_of_week
self.day_of_month = day_of_month
self.day_of_year = day_of_year
self.aggressive = aggressive
self._config_was_upgraded = False
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, GenConfig):
return self.__dict__ == other.__dict__
return NotImplemented
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
class Config():
@classmethod
def fromFile(cls, config_path):
return Config(JsonFileSaver.read(config_path))
@classmethod
def withOverrides(cls, overrides):
config = Config()
for key in overrides.keys():
config.override(key, overrides[key])
return config
@classmethod
def withFileOverrides(cls, override_path):
data = JsonFileSaver.read(override_path)
overrides = {}
for key in data.keys():
overrides[_LOOKUP[key]] = data[key]
return Config.withOverrides(overrides)
@classmethod
def fromEnvironment(cls):
config = {}
for key in os.environ:
if key in _LOOKUP:
config[_LOOKUP[key]] = _VALIDATORS[_LOOKUP[key]].validate(os.environ[key])
elif str.lower(key) in _LOOKUP:
config[_LOOKUP[str.lower(key)]] = _VALIDATORS[_LOOKUP[str.lower(key)]].validate(os.environ[key])
return Config(config)
def __init__(self, data=None):
self.overrides = {}
if data is None:
self.config = {}
else:
self.config = data
self._legacy_ignored_behavior = False
self._subscriptions = []
self._clientIdentifier = None
self.retained = self._loadRetained()
self._gen_config_cache = self.getGenerationalConfig()
# Tracks when hosts have been seen to be offline to retry on different hosts.
self._commFailure = {}
def getConfigFor(self, options):
new_config = Config()
new_config.overrides = self.overrides.copy()
new_config.update(options)
return new_config
def validateUpdate(self, additions):
new_config = self.config.copy()
new_config.update(additions)
validated, upgraded = self.validate(new_config)
return validated
def validate(self, new_config) -> Dict[str, Any]:
final_config = {}
upgraded = False
# validate each item
for key in new_config:
if type(key) == str:
if key not in _LOOKUP:
# its not in the schema, just ignore it
continue
setting = _LOOKUP[key]
else:
setting = key
value = setting.validator().validate(new_config[key])
if setting in UPGRADE_OPTIONS:
upgraded = True
if isinstance(value, str) and len(value) == 0 and setting in EMPTY_IS_DEFAULT:
value = setting.default()
if value is not None and (setting in KEEP_DEFAULT or value != setting.default()):
if setting in UPGRADE_OPTIONS and (UPGRADE_OPTIONS[setting] not in new_config or new_config[UPGRADE_OPTIONS[setting]] == UPGRADE_OPTIONS[setting].default()):
upgraded = True
final_config[UPGRADE_OPTIONS[setting]] = value
elif setting not in UPGRADE_OPTIONS:
final_config[setting] = value
if upgraded:
final_config[Setting.CALL_BACKUP_SNAPSHOT] = True
# add in non-ui settings
for setting in NON_UI_SETTING:
if self.get(setting) != setting.default() and not (setting in new_config or setting.key in new_config) and setting not in self.overrides:
final_config[setting] = self.get(setting)
# add defaults
for key in ALWAYS_KEEP:
if key not in final_config:
final_config[key] = key.default()
if not final_config.get(Setting.USE_SSL, False):
for key in [Setting.CERTFILE, Setting.KEYFILE]:
if key in final_config:
del final_config[key]
return final_config, upgraded
def update(self, new_config):
validated, upgraded = self.validate(new_config)
self._config_was_upgraded = upgraded
self.config = validated
self._gen_config_cache = self.getGenerationalConfig()
for sub in self._subscriptions:
sub()
def getServerOptions(self):
ret = {}
for setting in SERVER_OPTIONS:
ret[setting] = self.get(setting)
return ret
def subscribe(self, func):
self._subscriptions.append(func)
def clientIdentifier(self) -> str:
if self._clientIdentifier is None:
try:
if JsonFileSaver.exists(self.get(Setting.ID_FILE_PATH)):
self._clientIdentifier = JsonFileSaver.read(self.get(Setting.ID_FILE_PATH))['id']
else:
self._clientIdentifier = str(uuid.uuid4())
JsonFileSaver.write(self.get(Setting.ID_FILE_PATH), {'id': self._clientIdentifier})
except Exception:
self._clientIdentifier = str(uuid.uuid4())
return self._clientIdentifier
def getGenerationalConfig(self) -> Optional[Dict[str, Any]]:
days = self.get(Setting.GENERATIONAL_DAYS)
weeks = self.get(Setting.GENERATIONAL_WEEKS)
months = self.get(Setting.GENERATIONAL_MONTHS)
years = self.get(Setting.GENERATIONAL_YEARS)
if days + weeks + months + years == 0:
return None
base = GenConfig(
days=days,
weeks=weeks,
months=months,
years=years,
day_of_week=self.get(Setting.GENERATIONAL_DAY_OF_WEEK),
day_of_month=self.get(Setting.GENERATIONAL_DAY_OF_MONTH),
day_of_year=self.get(Setting.GENERATIONAL_DAY_OF_YEAR),
aggressive=self.get(Setting.GENERATIONAL_DELETE_EARLY)
)
if base.days <= 1:
# must always be >= 1, otherwise we'll just create and delete backups constantly.
base.days = 1
return base
def _loadRetained(self) -> List[str]:
if JsonFileSaver.exists(self.get(Setting.RETAINED_FILE_PATH)):
try:
return JsonFileSaver.read(self.get(Setting.RETAINED_FILE_PATH))['retained']
except json.decoder.JSONDecodeError:
logger.error("Unable to parse retained backup settings")
return []
return []
def isRetained(self, slug):
return slug in self.retained
def setRetained(self, slug, retain):
if retain and slug not in self.retained:
self.retained.append(slug)
JsonFileSaver.write(self.get(Setting.RETAINED_FILE_PATH), {'retained': self.retained})
elif not retain and slug in self.retained:
self.retained.remove(slug)
JsonFileSaver.write(self.get(Setting.RETAINED_FILE_PATH), {'retained': self.retained})
def isExplicit(self, setting):
return setting in self.config or setting.value in self.config
def override(self, setting: Setting, value):
self.overrides[setting] = value
return self
def get(self, setting: Setting) -> Any:
if setting in self.overrides:
return self.overrides[setting]
if setting in self.config:
return self.config[setting]
if setting.key() in self.config:
return self.config[setting.key()]
else:
if setting == Setting.IGNORE_UPGRADE_BACKUPS and self._legacy_ignored_behavior:
# Use the old behavior, rather than the new one
return False
return setting.default()
def getForUi(self, setting: Setting):
return _VALIDATORS[setting].formatForUi(self.get(setting))
def getTokenServers(self, path: str = "") -> List[URL]:
return list(map(lambda s: URL(s).with_path(path), self.get(Setting.TOKEN_SERVER_HOSTS).split(",")))
def mustSaveUpgradeChanges(self):
return self._config_was_upgraded
def getAllConfig(self) -> Dict[Setting, Any]:
return self.config.copy()
def persistedChanges(self):
self._config_was_upgraded = False
def useLegacyIgnoredBehavior(self, value: bool):
"""If the user upgrades from an old version and hasn't explicitely said they want to include upgrade backups, then this reverts them to the old behavior where they aren't ignored"""
self._legacy_ignored_behavior = value
@@ -0,0 +1,13 @@
from datetime import datetime
from typing import Dict
from ..logger import getLogger
logger = getLogger(__name__)
class CreateOptions(object):
def __init__(self, when: datetime, name_template: str, retain_sources: Dict[str, bool] = {}, note: str = None):
self.when: datetime = when
self.name_template: str = name_template
self.retain_sources: Dict[str, bool] = retain_sources
self.note = note
@@ -0,0 +1,37 @@
from datetime import timedelta
from .durationparser import DurationParser
from .validator import Validator
class DurationAsStringValidator(Validator):
def __init__(self, name, minimum=None, maximum=None, base_seconds=1, default_as_empty=None):
super().__init__(name)
self.min = minimum
self.max = maximum
self.base_seconds = base_seconds
self.default_as_empty = default_as_empty
def validate(self, value):
if value is None or (type(value) == str and len(value) == 0):
return None
try:
if type(value) == str:
if self.default_as_empty is not None and value == "":
value = self.default_as_empty
else:
value = DurationParser().parse(value).total_seconds() / self.base_seconds
value = float(value)
except ValueError:
self.raiseForValue(value)
if self.max is not None and value > self.max:
self.raiseForValue(value)
if self.min is not None and value < self.min:
self.raiseForValue(value)
return value
def formatForUi(self, value):
if self.default_as_empty is not None and value == self.default_as_empty:
return ""
else:
return DurationParser().format(timedelta(seconds=value * self.base_seconds))
@@ -0,0 +1,80 @@
import re
from datetime import timedelta
from injector import inject, singleton
SECOND_IDENTIFIERS = ["s", "sec", "secs", "second", "seconds"]
MINUTE_IDENTIFIERS = ["m", "min", "mins", "minute", "minutes"]
HOUR_IDENTIFIERS = ["h", "hr", "hour", "hours"]
DAY_IDENTIFIERS = ["d", "day", "days"]
NUMBER_REGEX = "^([0-9]*[.])?[0-9]+"
VALID_REGEX = "^([ ]*([0-9]*[.])?[0-9]+[ ]*(seconds|second|secs|sec|s|minutes|minute|mins|min|m|hours|hour|hr|h|days|day|d)?[ ,]*)*"
@singleton
class DurationParser():
@inject
def __init__(self):
pass
def parse(self, source: str):
source = source.lower()
total_match = re.match(VALID_REGEX, source)
if not total_match or total_match.group(0) != source:
raise ValueError()
parts = source.split()
i = 0
total = timedelta(seconds=0)
while (i < len(parts)):
part = parts[i].strip().strip(',')
match = re.match(NUMBER_REGEX, part)
i += 1
if not match:
raise ValueError()
length = float(match.group(0))
if match.group(0) == part:
if i < len(parts):
next_part = parts[i].strip().strip(',')
if next_part in SECOND_IDENTIFIERS or next_part in MINUTE_IDENTIFIERS or next_part in HOUR_IDENTIFIERS or next_part in DAY_IDENTIFIERS:
identifier = next_part
i += 1
else:
identifier = SECOND_IDENTIFIERS[0]
else:
identifier = "s"
else:
identifier = part[len(match.group(0)):]
if identifier in SECOND_IDENTIFIERS:
total += timedelta(seconds=length)
elif identifier in MINUTE_IDENTIFIERS:
total += timedelta(minutes=length)
elif identifier in HOUR_IDENTIFIERS:
total += timedelta(hours=length)
elif identifier in DAY_IDENTIFIERS:
total += timedelta(days=length)
else:
raise ValueError()
return total
def format(self, duration: timedelta):
parts = []
if duration >= timedelta(days=1):
days = int(duration.days)
parts.append("{} days".format(days))
duration = duration - timedelta(days=days)
if duration >= timedelta(hours=1):
hours = int(duration.seconds / (60 * 60))
parts.append("{} hours".format(hours))
duration = duration - timedelta(hours=hours)
if duration >= timedelta(minutes=1):
minutes = int(duration.seconds / 60)
parts.append("{} minutes".format(minutes))
duration = duration - timedelta(minutes=minutes)
if duration >= timedelta(seconds=1):
seconds = int(duration.seconds)
parts.append("{} seconds".format(seconds))
duration = duration - timedelta(seconds=seconds)
if len(parts) > 0:
return ", ".join(parts)
else:
return "0 seconds"
@@ -0,0 +1,25 @@
from .validator import Validator
from ..logger import getLogger
logger = getLogger(__name__)
class FloatValidator(Validator):
def __init__(self, name, minimum=None, maximum=None):
super().__init__(name)
self.min = minimum
self.max = maximum
def validate(self, value):
if value is None or (type(value) == str and len(value) == 0):
return None
try:
value = float(value)
except ValueError:
self.raiseForValue(value)
if self.max is not None and value > self.max:
self.raiseForValue(value)
if self.min is not None and value < self.min:
self.raiseForValue(value)
return value
@@ -0,0 +1,25 @@
from .validator import Validator
from ..logger import getLogger
logger = getLogger(__name__)
class IntValidator(Validator):
def __init__(self, name, minimum=None, maximum=None):
super().__init__(name)
self.min = minimum
self.max = maximum
def validate(self, value):
if value is None or (type(value) == str and len(value) == 0):
return None
try:
value = int(value)
except ValueError:
self.raiseForValue(value)
if self.max is not None and value > self.max:
self.raiseForValue(value)
if self.min is not None and value < self.min:
self.raiseForValue(value)
return value
@@ -0,0 +1,15 @@
from .validator import Validator
from ..logger import getLogger
logger = getLogger(__name__)
class ListValidator(Validator):
def __init__(self, name, values):
super().__init__(name)
self.values = values
def validate(self, value):
if value not in self.values:
self.raiseForValue(value)
return value
@@ -0,0 +1,19 @@
from .validator import Validator
import re
from ..logger import getLogger
logger = getLogger(__name__)
class RegexValidator(Validator):
def __init__(self, name, regex):
super().__init__(name)
self.re = re.compile(regex)
def validate(self, value):
if value is None or (type(value) == str and len(value) == 0):
return ""
value = str(value)
if not self.re.match(value):
self.raiseForValue(value)
return value
@@ -0,0 +1,514 @@
import json
from enum import Enum, unique
from os.path import abspath, join
from .boolvalidator import BoolValidator
from .floatvalidator import FloatValidator
from .intvalidator import IntValidator
from .regexvalidator import RegexValidator
from .stringvalidator import StringValidator
from .listvalidator import ListValidator
from .durationasstringvalidator import DurationAsStringValidator
from .bytesizeasstringvalidator import BytesizeAsStringValidator
from ..logger import getLogger
logger = getLogger(__name__)
@unique
class Setting(Enum):
MAX_BACKUPS_IN_HA = "max_backups_in_ha"
MAX_BACKUPS_IN_GOOGLE_DRIVE = "max_backups_in_google_drive"
DAYS_BETWEEN_BACKUPS = "days_between_backups"
IGNORE_OTHER_BACKUPS = "ignore_other_backups"
IGNORE_UPGRADE_BACKUPS = "ignore_upgrade_backups"
DELETE_IGNORED_AFTER_DAYS = "delete_ignored_after_days"
DELETE_BEFORE_NEW_BACKUP = "delete_before_new_backup"
BACKUP_NAME = "backup_name"
BACKUP_TIME_OF_DAY = "backup_time_of_day"
SPECIFY_BACKUP_FOLDER = "specify_backup_folder"
NOTIFY_FOR_STALE_BACKUPS = "notify_for_stale_backups"
ENABLE_BACKUP_STALE_SENSOR = "enable_backup_stale_sensor"
ENABLE_BACKUP_STATE_SENSOR = "enable_backup_state_sensor"
BACKUP_PASSWORD = "backup_password"
CALL_BACKUP_SNAPSHOT = "call_backup_snapshot"
# Basic backup settings
WARN_FOR_LOW_SPACE = "warn_for_low_space"
LOW_SPACE_THRESHOLD = "low_space_threshold"
DELETE_AFTER_UPLOAD = "delete_after_upload"
# generational settings
GENERATIONAL_DAYS = "generational_days"
GENERATIONAL_WEEKS = "generational_weeks"
GENERATIONAL_MONTHS = "generational_months"
GENERATIONAL_YEARS = "generational_years"
GENERATIONAL_DAY_OF_WEEK = "generational_day_of_week"
GENERATIONAL_DAY_OF_MONTH = "generational_day_of_month"
GENERATIONAL_DAY_OF_YEAR = "generational_day_of_year"
GENERATIONAL_DELETE_EARLY = "generational_delete_early"
# Partial backups
EXCLUDE_FOLDERS = "exclude_folders"
EXCLUDE_ADDONS = "exclude_addons"
STOP_ADDONS = "stop_addons"
DISABLE_WATCHDOG_WHEN_STOPPING = "disable_watchdog_when_stopping"
# UI Server Options
USE_SSL = "use_ssl"
CERTFILE = "certfile"
KEYFILE = "keyfile"
INGRESS_PORT = "ingress_port"
PORT = "port"
REQUIRE_LOGIN = "require_login"
EXPOSE_EXTRA_SERVER = "expose_extra_server"
# Add-on options
VERBOSE = "verbose"
SEND_ERROR_REPORTS = "send_error_reports"
CONFIRM_MULTIPLE_DELETES = "confirm_multiple_deletes"
ENABLE_DRIVE_UPLOAD = "enable_drive_upload"
WATCH_BACKUP_DIRECTORY = "watch_backup_directory"
TRACE_REQUESTS = "trace_requests"
# Theme Settings
BACKGROUND_COLOR = "background_color"
ACCENT_COLOR = "accent_color"
# Network and dns stuff
DRIVE_EXPERIMENTAL = "drive_experimental"
DRIVE_IPV4 = "drive_ipv4"
IGNORE_IPV6_ADDRESSES = "ignore_ipv6_addresses"
GOOGLE_DRIVE_TIMEOUT_SECONDS = "google_drive_timeout_seconds"
GOOGLE_DRIVE_PAGE_SIZE = "google_drive_page_size"
ALTERNATE_DNS_SERVERS = "alternate_dns_servers"
DEFAULT_DRIVE_CLIENT_ID = "default_drive_client_id"
DEFAULT_DRIVE_CLIENT_SECRET = "default_drive_client_secret"
DRIVE_PICKER_API_KEY = "drive_picker_api_key"
MAXIMUM_UPLOAD_CHUNK_BYTES = "maximum_upload_chunk_bytes"
# Files and folders
FOLDER_FILE_PATH = "folder_file_path"
CREDENTIALS_FILE_PATH = "credentials_file_path"
RETAINED_FILE_PATH = "retained_file_path"
SECRETS_FILE_PATH = "secrets_file_path"
BACKUP_DIRECTORY_PATH = "backup_directory_path"
INGRESS_TOKEN_FILE_PATH = "ingress_token_file_path"
CONFIG_FILE_PATH = "config_file_path"
ID_FILE_PATH = "id_file_path"
DATA_CACHE_FILE_PATH = "data_cache_file_path"
# endpoints
AUTHORIZATION_HOST = "authorization_host"
TOKEN_SERVER_HOSTS = "token_server_hosts"
SUPERVISOR_URL = "supervisor_url"
DRIVE_URL = "drive_url"
SUPERVISOR_TOKEN = "hassio_header"
DRIVE_HOST_NAME = "drive_host_name"
DRIVE_REFRESH_URL = "drive_refresh_url"
DRIVE_AUTHORIZE_URL = "drive_authorize_url"
DRIVE_DEVICE_CODE_URL = "drive_device_code_url"
DRIVE_TOKEN_URL = "drive_token_url"
SAVE_DRIVE_CREDS_PATH = "save_drive_creds_path"
STOP_ADDON_STATE_PATH = "stop_addon_state_path"
# Timing and timeouts
MAX_SYNC_INTERVAL_SECONDS = "max_sync_interval_seconds"
DEFAULT_SYNC_INTERVAL_VARIATION = "default_sync_interval_variation"
BACKUP_STALE_SECONDS = "backup_stale_seconds"
PENDING_BACKUP_TIMEOUT_SECONDS = "pending_backup_timeout_seconds"
FAILED_BACKUP_TIMEOUT_SECONDS = "failed_backup_timeout_seconds"
NEW_BACKUP_TIMEOUT_SECONDS = "new_backup_timeout_seconds"
DOWNLOAD_TIMEOUT_SECONDS = "download_timeout_seconds"
DEFAULT_CHUNK_SIZE = "default_chunk_size"
DEBUGGER_PORT = "debugger_port"
SERVER_PROJECT_ID = "server_project_id"
LOG_LEVEL = "log_level"
CONSOLE_LOG_LEVEL = "console_log_level"
BACKUP_STARTUP_DELAY_MINUTES = "backup_startup_delay_minutes"
EXCHANGER_TIMEOUT_SECONDS = "exchanger_timeout_seconds"
HA_REPORTING_INTERVAL_SECONDS = "ha_reporting_interval_seconds"
LONG_TERM_STALE_BACKUP_SECONDS = "long_term_stale_backup_seconds"
PING_TIMEOUT = "ping_timeout"
CACHE_WARMUP_MAX_SECONDS = "cache_warmup_max_seconds"
CACHE_WARMUP_ERROR_TIMEOUT_SECONDS = "cache_warmup_error_timeout"
MAX_BACKOFF_SECONDS = "max_backoff_seconds"
# Old, deprecated settings
DEPRECTAED_MAX_BACKUPS_IN_HA = "max_snapshots_in_hassio"
DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE = "max_snapshots_in_google_drive"
DEPRECATED_DAYS_BETWEEN_BACKUPS = "days_between_snapshots"
DEPRECTAED_IGNORE_OTHER_BACKUPS = "ignore_other_snapshots"
DEPRECTAED_IGNORE_UPGRADE_BACKUPS = "ignore_upgrade_snapshots"
DEPRECTAED_BACKUP_NAME = "snapshot_name"
DEPRECTAED_BACKUP_TIME_OF_DAY = "snapshot_time_of_day"
DEPRECATED_BACKUP_PASSWORD = "snapshot_password"
DEPRECTAED_SPECIFY_BACKUP_FOLDER = "specify_snapshot_folder"
DEPRECTAED_DELETE_BEFORE_NEW_BACKUP = "delete_before_new_snapshot"
DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS = "notify_for_stale_snapshots"
DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR = "enable_snapshot_stale_sensor"
DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR = "enable_snapshot_state_sensor"
def default(self):
if "staging" in VERSION and self in _STAGING_DEFAULTS:
return _STAGING_DEFAULTS[self]
return _DEFAULTS[self]
def validator(self):
return _VALIDATORS[self]
def key(self):
return self.value
_DEFAULTS = {
Setting.MAX_BACKUPS_IN_HA: 4,
Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE: 4,
Setting.DAYS_BETWEEN_BACKUPS: 3,
Setting.IGNORE_OTHER_BACKUPS: False,
Setting.IGNORE_UPGRADE_BACKUPS: True,
Setting.DELETE_IGNORED_AFTER_DAYS: 0,
Setting.DELETE_BEFORE_NEW_BACKUP: False,
Setting.BACKUP_NAME: "{type} Backup {year}-{month}-{day} {hr24}:{min}:{sec}",
Setting.BACKUP_TIME_OF_DAY: "",
Setting.SPECIFY_BACKUP_FOLDER: False,
Setting.NOTIFY_FOR_STALE_BACKUPS: True,
Setting.ENABLE_BACKUP_STALE_SENSOR: True,
Setting.ENABLE_BACKUP_STATE_SENSOR: True,
Setting.BACKUP_PASSWORD: "",
Setting.WATCH_BACKUP_DIRECTORY: True,
Setting.TRACE_REQUESTS: False,
# Basic backup settings
Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: 4,
Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: 4,
Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS: 3,
Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS: False,
Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS: False,
Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: "",
Setting.DEPRECTAED_BACKUP_NAME: "{type} Snapshot {year}-{month}-{day} {hr24}:{min}:{sec}",
Setting.DEPRECATED_BACKUP_PASSWORD: "",
Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER: False,
Setting.WARN_FOR_LOW_SPACE: True,
Setting.LOW_SPACE_THRESHOLD: 1024 * 1024 * 1024,
Setting.DELETE_AFTER_UPLOAD: False,
Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP: False,
Setting.CALL_BACKUP_SNAPSHOT: False,
# Generational backup settings
Setting.GENERATIONAL_DAYS: 0,
Setting.GENERATIONAL_WEEKS: 0,
Setting.GENERATIONAL_MONTHS: 0,
Setting.GENERATIONAL_YEARS: 0,
Setting.GENERATIONAL_DAY_OF_WEEK: "mon",
Setting.GENERATIONAL_DAY_OF_MONTH: 1,
Setting.GENERATIONAL_DAY_OF_YEAR: 1,
Setting.GENERATIONAL_DELETE_EARLY: False,
# Partial backup settings
Setting.EXCLUDE_FOLDERS: "",
Setting.EXCLUDE_ADDONS: "",
Setting.STOP_ADDONS: "",
Setting.DISABLE_WATCHDOG_WHEN_STOPPING: False,
# UI Server settings
Setting.USE_SSL: False,
Setting.REQUIRE_LOGIN: False,
Setting.EXPOSE_EXTRA_SERVER: False,
Setting.CERTFILE: "/ssl/fullchain.pem",
Setting.KEYFILE: "/ssl/privkey.pem",
Setting.INGRESS_PORT: 8099,
Setting.PORT: 1627,
# Add-on options
Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS: True,
Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR: True,
Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR: True,
Setting.SEND_ERROR_REPORTS: False,
Setting.VERBOSE: False,
Setting.CONFIRM_MULTIPLE_DELETES: True,
Setting.ENABLE_DRIVE_UPLOAD: True,
# Theme Settings
Setting.BACKGROUND_COLOR: "",
Setting.ACCENT_COLOR: "",
# Network and DNS settings
Setting.ALTERNATE_DNS_SERVERS: "8.8.8.8,8.8.4.4",
Setting.DRIVE_EXPERIMENTAL: False,
Setting.DRIVE_IPV4: "",
Setting.IGNORE_IPV6_ADDRESSES: False,
Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS: 180,
Setting.GOOGLE_DRIVE_PAGE_SIZE: 100,
Setting.MAXIMUM_UPLOAD_CHUNK_BYTES: 10 * 1024 * 1024,
# Remote endpoints
Setting.AUTHORIZATION_HOST: "https://habackup.io",
Setting.TOKEN_SERVER_HOSTS: "https://token1.habackup.io,https://habackup.io",
Setting.SUPERVISOR_URL: "",
Setting.SUPERVISOR_TOKEN: "",
Setting.DRIVE_URL: "https://www.googleapis.com",
Setting.DRIVE_REFRESH_URL: "https://www.googleapis.com/oauth2/v4/token",
Setting.DRIVE_AUTHORIZE_URL: "https://accounts.google.com/o/oauth2/v2/auth",
Setting.DRIVE_DEVICE_CODE_URL: "https://oauth2.googleapis.com/device/code",
Setting.DRIVE_TOKEN_URL: "https://oauth2.googleapis.com/token",
Setting.DRIVE_HOST_NAME: "www.googleapis.com",
Setting.SAVE_DRIVE_CREDS_PATH: "token",
# File locations used to store things
Setting.FOLDER_FILE_PATH: "/data/folder.dat",
Setting.CREDENTIALS_FILE_PATH: "/data/credentials.dat",
Setting.BACKUP_DIRECTORY_PATH: "/backup",
Setting.RETAINED_FILE_PATH: "/data/retained.json",
Setting.SECRETS_FILE_PATH: "/config/secrets.yaml",
Setting.INGRESS_TOKEN_FILE_PATH: "/data/ingress.dat",
Setting.CONFIG_FILE_PATH: "/data/options.json",
Setting.ID_FILE_PATH: "/data/id.json",
Setting.STOP_ADDON_STATE_PATH: '/data/stop_addon_state.json',
Setting.DATA_CACHE_FILE_PATH: '/data/data_cache.json',
# Various timeouts and intervals
Setting.BACKUP_STALE_SECONDS: 60 * 60 * 3,
Setting.PENDING_BACKUP_TIMEOUT_SECONDS: 60 * 60 * 5,
Setting.FAILED_BACKUP_TIMEOUT_SECONDS: 60 * 15,
Setting.NEW_BACKUP_TIMEOUT_SECONDS: 5,
Setting.MAX_SYNC_INTERVAL_SECONDS: 60 * 60 * 3, # 3 hours
Setting.DEFAULT_SYNC_INTERVAL_VARIATION: 0.5, # intermittent checkup syncs happen between 1.5 and 3 hours since the last one, randomly
Setting.DEFAULT_DRIVE_CLIENT_ID: "933944288016-n35gnn2juc76ub7u5326ls0iaq9dgjgu.apps.googleusercontent.com",
Setting.DEFAULT_DRIVE_CLIENT_SECRET: "",
Setting.DRIVE_PICKER_API_KEY: "",
Setting.DEFAULT_CHUNK_SIZE: 1024 * 1024 * 5,
Setting.DOWNLOAD_TIMEOUT_SECONDS: 60,
Setting.DEBUGGER_PORT: None,
Setting.SERVER_PROJECT_ID: "",
Setting.LOG_LEVEL: 'DEBUG',
Setting.CONSOLE_LOG_LEVEL: 'INFO',
Setting.BACKUP_STARTUP_DELAY_MINUTES: 10,
Setting.EXCHANGER_TIMEOUT_SECONDS: 10,
Setting.HA_REPORTING_INTERVAL_SECONDS: 10,
Setting.LONG_TERM_STALE_BACKUP_SECONDS: 60 * 60 * 24,
Setting.PING_TIMEOUT: 5,
Setting.CACHE_WARMUP_MAX_SECONDS: 15 * 60, # 30 minutes
Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS: 24 * 60 * 60, # 1 day
Setting.MAX_BACKOFF_SECONDS: 60 * 60 * 2, # 2 hours
}
_STAGING_DEFAULTS = {
Setting.AUTHORIZATION_HOST: "https://dev.habackup.io",
Setting.TOKEN_SERVER_HOSTS: "https://token1.dev.habackup.io,https://dev.habackup.io",
Setting.DEFAULT_DRIVE_CLIENT_ID: "795575624694-jcdhoh1jr1ngccfsbi2f44arr4jupl79.apps.googleusercontent.com",
}
_CONFIG = {
Setting.MAX_BACKUPS_IN_HA: "int(0,)?",
Setting.MAX_BACKUPS_IN_GOOGLE_DRIVE: "int(0,)?",
Setting.DAYS_BETWEEN_BACKUPS: "float(0,)?",
Setting.IGNORE_OTHER_BACKUPS: "bool?",
Setting.IGNORE_UPGRADE_BACKUPS: "bool?",
Setting.DELETE_IGNORED_AFTER_DAYS: "float(0,)?",
Setting.DELETE_BEFORE_NEW_BACKUP: "bool?",
Setting.BACKUP_NAME: "str?",
Setting.BACKUP_TIME_OF_DAY: "match(^[0-2]\\d:[0-5]\\d$)?",
Setting.SPECIFY_BACKUP_FOLDER: "bool?",
Setting.NOTIFY_FOR_STALE_BACKUPS: "bool?",
Setting.ENABLE_BACKUP_STALE_SENSOR: "bool?",
Setting.ENABLE_BACKUP_STATE_SENSOR: "bool?",
Setting.BACKUP_PASSWORD: "str?",
Setting.WATCH_BACKUP_DIRECTORY: "bool?",
Setting.TRACE_REQUESTS: "bool?",
# Basic backup settings
Setting.DEPRECTAED_MAX_BACKUPS_IN_HA: "int(0,)?",
Setting.DEPRECTAED_MAX_BACKUPS_IN_GOOGLE_DRIVE: "int(0,)?",
Setting.DEPRECATED_DAYS_BETWEEN_BACKUPS: "float(0,)?",
Setting.DEPRECTAED_IGNORE_OTHER_BACKUPS: "bool?",
Setting.DEPRECTAED_IGNORE_UPGRADE_BACKUPS: "bool?",
Setting.DEPRECTAED_BACKUP_TIME_OF_DAY: "match(^[0-2]\\d:[0-5]\\d$)?",
Setting.DEPRECTAED_BACKUP_NAME: "str?",
Setting.DEPRECATED_BACKUP_PASSWORD: "str?",
Setting.DEPRECTAED_SPECIFY_BACKUP_FOLDER: "bool?",
Setting.WARN_FOR_LOW_SPACE: "bool?",
Setting.LOW_SPACE_THRESHOLD: "int(0,)?",
Setting.DELETE_AFTER_UPLOAD: "bool?",
Setting.DEPRECTAED_DELETE_BEFORE_NEW_BACKUP: "bool?",
Setting.CALL_BACKUP_SNAPSHOT: "bool?",
# Generational backup settings
Setting.GENERATIONAL_DAYS: "int(0,)?",
Setting.GENERATIONAL_WEEKS: "int(0,)?",
Setting.GENERATIONAL_MONTHS: "int(0,)?",
Setting.GENERATIONAL_YEARS: "int(0,)?",
Setting.GENERATIONAL_DAY_OF_WEEK: "match(^(mon|tue|wed|thu|fri|sat|sun)$)?",
Setting.GENERATIONAL_DAY_OF_MONTH: "int(1,31)?",
Setting.GENERATIONAL_DAY_OF_YEAR: "int(1,365)?",
Setting.GENERATIONAL_DELETE_EARLY: "bool?",
# Partial backup settings
Setting.EXCLUDE_FOLDERS: "str?",
Setting.EXCLUDE_ADDONS: "str?",
Setting.STOP_ADDONS: "str?",
Setting.DISABLE_WATCHDOG_WHEN_STOPPING: "bool?",
# UI Server settings
Setting.USE_SSL: "bool?",
Setting.REQUIRE_LOGIN: "bool?",
Setting.EXPOSE_EXTRA_SERVER: "bool?",
Setting.CERTFILE: "str?",
Setting.KEYFILE: "str?",
Setting.INGRESS_PORT: "int(0,)?",
Setting.PORT: "int(0,)?",
# Add-on options
Setting.DEPRECTAED_NOTIFY_FOR_STALE_BACKUPS: "bool?",
Setting.DEPRECTAED_ENABLE_BACKUP_STALE_SENSOR: "bool?",
Setting.DEPRECTAED_ENABLE_BACKUP_STATE_SENSOR: "bool?",
Setting.SEND_ERROR_REPORTS: "bool?",
Setting.VERBOSE: "bool?",
Setting.CONFIRM_MULTIPLE_DELETES: "bool?",
Setting.ENABLE_DRIVE_UPLOAD: "bool?",
# Theme Settings
Setting.BACKGROUND_COLOR: "match(^(#[0-9ABCDEFabcdef]{6}|)$)?",
Setting.ACCENT_COLOR: "match(^(#[0-9ABCDEFabcdef]{6}|)$)?",
# Network and DNS settings
Setting.ALTERNATE_DNS_SERVERS: "match(^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(,[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})*$)?",
Setting.DRIVE_EXPERIMENTAL: "bool?",
Setting.DRIVE_IPV4: "match(^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$)?",
Setting.IGNORE_IPV6_ADDRESSES: "bool?",
Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS: "float(1,)?",
Setting.GOOGLE_DRIVE_PAGE_SIZE: "int(1,)?",
Setting.MAXIMUM_UPLOAD_CHUNK_BYTES: f"float({1024 * 256},)?",
# Remote endpoints
Setting.AUTHORIZATION_HOST: "url?",
Setting.TOKEN_SERVER_HOSTS: "str?",
Setting.SUPERVISOR_URL: "url?",
Setting.SUPERVISOR_TOKEN: "str?",
Setting.DRIVE_URL: "url?",
Setting.DRIVE_REFRESH_URL: "url?",
Setting.DRIVE_AUTHORIZE_URL: "url?",
Setting.DRIVE_DEVICE_CODE_URL: "url?",
Setting.DRIVE_TOKEN_URL: "url?",
Setting.DRIVE_HOST_NAME: "str?",
Setting.SAVE_DRIVE_CREDS_PATH: "str?",
# File locations used to store things
Setting.FOLDER_FILE_PATH: "str?",
Setting.CREDENTIALS_FILE_PATH: "str?",
Setting.BACKUP_DIRECTORY_PATH: "str?",
Setting.RETAINED_FILE_PATH: "str?",
Setting.SECRETS_FILE_PATH: "str?",
Setting.INGRESS_TOKEN_FILE_PATH: "str?",
Setting.CONFIG_FILE_PATH: "str?",
Setting.ID_FILE_PATH: "str?",
Setting.STOP_ADDON_STATE_PATH: "str?",
Setting.DATA_CACHE_FILE_PATH: "str?",
# Various timeouts and intervals
Setting.BACKUP_STALE_SECONDS: "float(0,)?",
Setting.PENDING_BACKUP_TIMEOUT_SECONDS: "float(0,)?",
Setting.FAILED_BACKUP_TIMEOUT_SECONDS: "float(0,)?",
Setting.NEW_BACKUP_TIMEOUT_SECONDS: "float(0,)?",
Setting.MAX_SYNC_INTERVAL_SECONDS: "float(300,)?",
Setting.DEFAULT_SYNC_INTERVAL_VARIATION: "float(0,1)?",
Setting.DEFAULT_DRIVE_CLIENT_ID: "str?",
Setting.DEFAULT_DRIVE_CLIENT_SECRET: "str?",
Setting.DRIVE_PICKER_API_KEY: "str?",
Setting.DEFAULT_CHUNK_SIZE: "int(1,)?",
Setting.DOWNLOAD_TIMEOUT_SECONDS: "float(0,)?",
Setting.DEBUGGER_PORT: "int(100,)?",
Setting.SERVER_PROJECT_ID: "str?",
Setting.LOG_LEVEL: "list(DEBUG|TRACE|INFO|WARN|CRITICAL|WARNING)?",
Setting.CONSOLE_LOG_LEVEL: "list(DEBUG|TRACE|INFO|WARN|CRITICAL|WARNING)?",
Setting.BACKUP_STARTUP_DELAY_MINUTES: "float(0,)?",
Setting.EXCHANGER_TIMEOUT_SECONDS: "float(0,)?",
Setting.HA_REPORTING_INTERVAL_SECONDS: "int(1,)?",
Setting.LONG_TERM_STALE_BACKUP_SECONDS: "int(1,)?",
Setting.PING_TIMEOUT: "float(0,)?",
Setting.CACHE_WARMUP_MAX_SECONDS: "float(0,)",
Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS: "float(0,)",
Setting.MAX_BACKOFF_SECONDS: "int(3600,)?",
}
PRIVATE = [
Setting.DEPRECATED_BACKUP_PASSWORD,
Setting.DEPRECTAED_BACKUP_NAME,
Setting.BACKUP_PASSWORD,
Setting.BACKUP_NAME
]
_LOOKUP = {}
_VALIDATORS = {}
def getValidator(name, schema):
if schema.endswith("?"):
schema = schema[:-1]
if schema.startswith("int("):
# its a int
parts = schema[4:-1]
minimum = None
maximum = None
if parts.endswith(","):
minimum = int(parts[0:-1])
elif parts.startswith(","):
maximum = int(parts[1:])
else:
digits = parts.split(",")
minimum = int(digits[0])
maximum = int(digits[1])
return IntValidator(name, minimum, maximum)
elif schema.startswith("float("):
# its a float
parts = schema[6:-1]
minimum = None
maximum = None
if parts.endswith(","):
minimum = float(parts[0:-1])
elif parts.startswith(","):
maximum = float(parts[1:])
else:
digits = parts.split(",")
minimum = float(digits[0])
maximum = float(digits[1])
return FloatValidator(name, minimum, maximum)
elif schema.startswith("bool"):
# its a bool
return BoolValidator(name)
elif schema.startswith("str") or schema.startswith("url"):
# its a url (treat it just like any string)
return StringValidator(name)
elif schema.startswith("match("):
return RegexValidator(name, schema[6:-1])
elif schema.startswith("list("):
return ListValidator(name, schema[5:-1].split("|"))
else:
raise Exception("Invalid schema: " + schema)
# initalize validators
for setting in Setting:
_LOOKUP[setting.value] = setting
with open(abspath(join(__file__, "..", "..", "..", "config.json"))) as f:
# Thsi is a static file included in the container, so don't worry about using JsonFileLoader
addon_config = json.load(f)
for setting in Setting:
_VALIDATORS[setting] = getValidator(setting.value, _CONFIG[setting])
for key in addon_config["schema"]:
_VALIDATORS[_LOOKUP[key]] = getValidator(key, addon_config["schema"][key])
_VALIDATORS[Setting.MAX_SYNC_INTERVAL_SECONDS] = DurationAsStringValidator("max_sync_interval_seconds", minimum=1, maximum=None)
_VALIDATORS[Setting.HA_REPORTING_INTERVAL_SECONDS] = DurationAsStringValidator("ha_reporting_interval_seconds", minimum=1, maximum=None)
_VALIDATORS[Setting.DELETE_IGNORED_AFTER_DAYS] = DurationAsStringValidator("delete_ignored_after_days", minimum=0, maximum=None, base_seconds=60 * 60 * 24, default_as_empty=0)
_VALIDATORS[Setting.MAXIMUM_UPLOAD_CHUNK_BYTES] = BytesizeAsStringValidator("maximum_upload_chunk_bytes", minimum=256 * 1024)
VERSION = addon_config["version"]
def isStaging():
return "staging" in VERSION
@@ -0,0 +1,11 @@
from ..logger import getLogger
logger = getLogger(__name__)
class Startable():
async def start(self):
pass
async def stop(self):
pass
@@ -0,0 +1,14 @@
from .validator import Validator
from ..logger import getLogger
logger = getLogger(__name__)
class StringValidator(Validator):
def __init__(self, name):
super().__init__(name)
def validate(self, value):
if value is None or (type(value) == str and len(value) == 0):
return ""
return str(value)
@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from ..exceptions import InvalidConfigurationValue
from ..logger import getLogger
logger = getLogger(__name__)
class Validator(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def validate(self, value):
return True
def raiseForValue(self, value):
raise InvalidConfigurationValue(self.name, str(value))
def formatForUi(self, value):
return value
@@ -0,0 +1,84 @@
STAGING_KEY = ".staging."
EXPECTED_VERISON_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']
class Version:
def __init__(self, *args):
self._identifiers = args
self.staging = False
@classmethod
def default(cls):
return Version(0)
@classmethod
def parse(cls, version: str):
staging_version = None
if STAGING_KEY in version:
index = version.find(STAGING_KEY)
staging_version = int(version[index + len(STAGING_KEY):])
version = version[0:index]
version = Version._removeUnexpected(version)
parts = []
for part in version.split("."):
if len(part) > 0:
parts.append(int(part))
if staging_version is not None:
parts.append(staging_version)
if len(parts) == 0:
parts.append(0)
ret = Version(*parts)
if staging_version is not None:
ret.staging = True
return ret
@classmethod
def _removeUnexpected(cls, version: str):
ret = ""
for c in version:
if c in EXPECTED_VERISON_CHARS:
ret += c
while ".." in ret:
ret = ret.replace("..", ".")
return ret
def __getitem__(self, key):
return self._identifiers[key]
def length(self):
return len(self._identifiers)
def _compare(self, other):
i = 0
while(i < min(self.length(), other.length())):
if self[i] < other[i]:
return -1
if self[i] > other[i]:
return 1
i += 1
if self.length() < other.length():
return -1
if self.length() > other.length():
return 1
return 0
def __lt__(self, other):
return self._compare(other) < 0
def __le__(self, other):
return self._compare(other) <= 0
def __eq__(self, other):
return self._compare(other) == 0
def __ne__(self, other):
return self._compare(other) != 0
def __gt__(self, other):
return self._compare(other) > 0
def __ge__(self, other):
return self._compare(other) >= 0
def __str__(self):
return ".".join(str(i) for i in self._identifiers)