New Addon
Google BK
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user