2021-09-05 17:10:14 +00:00
|
|
|
"""
|
|
|
|
Functionality:
|
|
|
|
- read and write config
|
|
|
|
- load config variables into redis
|
|
|
|
"""
|
|
|
|
|
|
|
|
import json
|
|
|
|
import os
|
2022-01-08 13:04:51 +00:00
|
|
|
import re
|
2023-01-11 15:00:44 +00:00
|
|
|
from random import randint
|
2023-06-22 17:15:07 +00:00
|
|
|
from time import sleep
|
2021-09-05 17:10:14 +00:00
|
|
|
|
2022-12-21 07:24:24 +00:00
|
|
|
import requests
|
2021-12-02 11:37:54 +00:00
|
|
|
from celery.schedules import crontab
|
2022-12-21 07:24:24 +00:00
|
|
|
from django.conf import settings
|
2022-01-22 15:13:37 +00:00
|
|
|
from home.src.ta.ta_redis import RedisArchivist
|
2021-09-05 17:10:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AppConfig:
|
2021-09-21 09:25:22 +00:00
|
|
|
"""handle user settings and application variables"""
|
2021-09-05 17:10:14 +00:00
|
|
|
|
2021-10-29 16:43:19 +00:00
|
|
|
def __init__(self, user_id=False):
|
|
|
|
self.user_id = user_id
|
2021-09-05 17:10:14 +00:00
|
|
|
self.config = self.get_config()
|
2021-10-29 16:43:19 +00:00
|
|
|
self.colors = self.get_colors()
|
2021-09-05 17:10:14 +00:00
|
|
|
|
|
|
|
def get_config(self):
|
2021-09-21 09:25:22 +00:00
|
|
|
"""get config from default file or redis if changed"""
|
2021-09-05 17:10:14 +00:00
|
|
|
config = self.get_config_redis()
|
|
|
|
if not config:
|
2021-09-20 10:45:01 +00:00
|
|
|
config = self.get_config_file()
|
|
|
|
|
2021-10-29 16:43:19 +00:00
|
|
|
if self.user_id:
|
|
|
|
key = f"{self.user_id}:page_size"
|
|
|
|
page_size = RedisArchivist().get_message(key)["status"]
|
|
|
|
if page_size:
|
|
|
|
config["archive"]["page_size"] = page_size
|
|
|
|
|
2021-09-21 09:25:22 +00:00
|
|
|
config["application"].update(self.get_config_env())
|
2021-09-05 17:10:14 +00:00
|
|
|
return config
|
|
|
|
|
2021-09-20 10:45:01 +00:00
|
|
|
def get_config_file(self):
|
2021-09-21 09:25:22 +00:00
|
|
|
"""read the defaults from config.json"""
|
|
|
|
with open("home/config.json", "r", encoding="utf-8") as f:
|
2022-01-27 08:32:58 +00:00
|
|
|
config_file = json.load(f)
|
2021-09-20 10:45:01 +00:00
|
|
|
|
2021-09-21 09:25:22 +00:00
|
|
|
config_file["application"].update(self.get_config_env())
|
2021-09-20 10:45:01 +00:00
|
|
|
|
|
|
|
return config_file
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_config_env():
|
2021-09-21 09:25:22 +00:00
|
|
|
"""read environment application variables"""
|
2021-10-28 08:49:58 +00:00
|
|
|
es_pass = os.environ.get("ELASTIC_PASSWORD")
|
|
|
|
es_user = os.environ.get("ELASTIC_USER", default="elastic")
|
|
|
|
|
2021-09-20 10:45:01 +00:00
|
|
|
application = {
|
2021-09-21 09:25:22 +00:00
|
|
|
"REDIS_HOST": os.environ.get("REDIS_HOST"),
|
|
|
|
"es_url": os.environ.get("ES_URL"),
|
2021-10-28 08:49:58 +00:00
|
|
|
"es_auth": (es_user, es_pass),
|
2022-12-19 07:43:10 +00:00
|
|
|
"HOST_UID": int(os.environ.get("HOST_UID", False)),
|
|
|
|
"HOST_GID": int(os.environ.get("HOST_GID", False)),
|
|
|
|
"enable_cast": bool(os.environ.get("ENABLE_CAST")),
|
2021-09-20 10:45:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return application
|
|
|
|
|
2021-09-05 17:10:14 +00:00
|
|
|
@staticmethod
|
|
|
|
def get_config_redis():
|
2021-09-21 09:25:22 +00:00
|
|
|
"""read config json set from redis to overwrite defaults"""
|
2023-06-22 17:15:07 +00:00
|
|
|
for i in range(10):
|
|
|
|
try:
|
|
|
|
config = RedisArchivist().get_message("config")
|
|
|
|
if not list(config.values())[0]:
|
|
|
|
return False
|
2021-09-05 17:10:14 +00:00
|
|
|
|
2023-06-22 17:15:07 +00:00
|
|
|
return config
|
|
|
|
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
|
|
print(f"... Redis connection failed, retry [{i}/10]")
|
|
|
|
sleep(3)
|
|
|
|
|
|
|
|
raise ConnectionError("failed to connect to redis")
|
2021-09-05 17:10:14 +00:00
|
|
|
|
|
|
|
def update_config(self, form_post):
|
2021-09-21 09:25:22 +00:00
|
|
|
"""update config values from settings form"""
|
2022-04-30 11:35:04 +00:00
|
|
|
updated = []
|
2021-09-05 17:10:14 +00:00
|
|
|
for key, value in form_post.items():
|
2022-04-07 15:30:20 +00:00
|
|
|
if not value and not isinstance(value, int):
|
|
|
|
continue
|
|
|
|
|
|
|
|
if value in ["0", 0]:
|
|
|
|
to_write = False
|
|
|
|
elif value == "1":
|
|
|
|
to_write = True
|
|
|
|
else:
|
|
|
|
to_write = value
|
2021-09-05 17:10:14 +00:00
|
|
|
|
2022-04-07 15:30:20 +00:00
|
|
|
config_dict, config_value = key.split("_", maxsplit=1)
|
|
|
|
self.config[config_dict][config_value] = to_write
|
2022-04-30 11:35:04 +00:00
|
|
|
updated.append((config_value, to_write))
|
2021-09-05 17:10:14 +00:00
|
|
|
|
2023-08-23 11:35:20 +00:00
|
|
|
RedisArchivist().set_message("config", self.config, save=True)
|
2022-04-30 11:35:04 +00:00
|
|
|
return updated
|
2021-09-20 10:45:01 +00:00
|
|
|
|
2021-10-29 15:37:31 +00:00
|
|
|
@staticmethod
|
|
|
|
def set_user_config(form_post, user_id):
|
|
|
|
"""set values in redis for user settings"""
|
|
|
|
for key, value in form_post.items():
|
2022-04-07 15:30:20 +00:00
|
|
|
if not value:
|
|
|
|
continue
|
|
|
|
|
|
|
|
message = {"status": value}
|
|
|
|
redis_key = f"{user_id}:{key}"
|
2023-08-23 11:35:20 +00:00
|
|
|
RedisArchivist().set_message(redis_key, message, save=True)
|
2021-10-29 15:37:31 +00:00
|
|
|
|
2021-10-29 16:43:19 +00:00
|
|
|
def get_colors(self):
|
|
|
|
"""overwrite config if user has set custom values"""
|
|
|
|
colors = False
|
|
|
|
if self.user_id:
|
|
|
|
col_dict = RedisArchivist().get_message(f"{self.user_id}:colors")
|
|
|
|
colors = col_dict["status"]
|
|
|
|
|
|
|
|
if not colors:
|
|
|
|
colors = self.config["application"]["colors"]
|
|
|
|
|
|
|
|
self.config["application"]["colors"] = colors
|
|
|
|
return colors
|
|
|
|
|
2023-01-11 15:00:44 +00:00
|
|
|
@staticmethod
|
|
|
|
def _build_rand_daily():
|
|
|
|
"""build random daily schedule per installation"""
|
|
|
|
return {
|
|
|
|
"minute": randint(0, 59),
|
|
|
|
"hour": randint(0, 23),
|
|
|
|
"day_of_week": "*",
|
|
|
|
}
|
|
|
|
|
2021-09-20 10:45:01 +00:00
|
|
|
def load_new_defaults(self):
|
2021-09-21 09:25:22 +00:00
|
|
|
"""check config.json for missing defaults"""
|
2021-09-20 10:45:01 +00:00
|
|
|
default_config = self.get_config_file()
|
|
|
|
redis_config = self.get_config_redis()
|
|
|
|
|
|
|
|
# check for customizations
|
|
|
|
if not redis_config:
|
2022-07-21 16:01:01 +00:00
|
|
|
config = self.get_config()
|
2023-01-16 02:13:58 +00:00
|
|
|
config["scheduler"]["version_check"] = self._build_rand_daily()
|
2022-07-21 16:01:01 +00:00
|
|
|
RedisArchivist().set_message("config", config)
|
2023-02-02 05:06:24 +00:00
|
|
|
return False
|
2021-09-20 10:45:01 +00:00
|
|
|
|
|
|
|
needs_update = False
|
|
|
|
|
|
|
|
for key, value in default_config.items():
|
|
|
|
# missing whole main key
|
|
|
|
if key not in redis_config:
|
|
|
|
redis_config.update({key: value})
|
|
|
|
needs_update = True
|
|
|
|
continue
|
|
|
|
|
|
|
|
# missing nested values
|
|
|
|
for sub_key, sub_value in value.items():
|
|
|
|
if sub_key not in redis_config[key].keys():
|
2023-01-11 15:00:44 +00:00
|
|
|
if sub_value == "rand-d":
|
|
|
|
sub_value = self._build_rand_daily()
|
|
|
|
|
2021-09-20 10:45:01 +00:00
|
|
|
redis_config[key].update({sub_key: sub_value})
|
|
|
|
needs_update = True
|
|
|
|
|
|
|
|
if needs_update:
|
2022-06-16 03:37:46 +00:00
|
|
|
RedisArchivist().set_message("config", redis_config)
|
2021-12-02 11:37:54 +00:00
|
|
|
|
2023-02-02 05:06:24 +00:00
|
|
|
return needs_update
|
|
|
|
|
2021-12-02 11:37:54 +00:00
|
|
|
|
|
|
|
class ScheduleBuilder:
|
|
|
|
"""build schedule dicts for beat"""
|
|
|
|
|
2021-12-03 04:27:26 +00:00
|
|
|
SCHEDULES = {
|
|
|
|
"update_subscribed": "0 8 *",
|
|
|
|
"download_pending": "0 16 *",
|
|
|
|
"check_reindex": "0 12 *",
|
|
|
|
"thumbnail_check": "0 17 *",
|
|
|
|
"run_backup": "0 18 0",
|
2022-12-22 04:17:06 +00:00
|
|
|
"version_check": "0 11 *",
|
2021-12-03 04:27:26 +00:00
|
|
|
}
|
2021-12-13 13:28:41 +00:00
|
|
|
CONFIG = ["check_reindex_days", "run_backup_rotate"]
|
2023-07-29 16:41:54 +00:00
|
|
|
NOTIFY = [
|
|
|
|
"update_subscribed_notify",
|
|
|
|
"download_pending_notify",
|
|
|
|
"check_reindex_notify",
|
|
|
|
]
|
2022-06-16 03:37:46 +00:00
|
|
|
MSG = "message:setting"
|
2021-12-02 11:37:54 +00:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.config = AppConfig().config
|
|
|
|
|
|
|
|
def update_schedule_conf(self, form_post):
|
|
|
|
"""process form post"""
|
|
|
|
print("processing form, restart container for changes to take effect")
|
|
|
|
redis_config = self.config
|
|
|
|
for key, value in form_post.items():
|
2022-04-07 15:30:20 +00:00
|
|
|
if key in self.SCHEDULES and value:
|
2021-12-17 08:09:21 +00:00
|
|
|
try:
|
2022-04-07 15:30:20 +00:00
|
|
|
to_write = self.value_builder(key, value)
|
2021-12-17 08:09:21 +00:00
|
|
|
except ValueError:
|
2022-04-07 15:30:20 +00:00
|
|
|
print(f"failed: {key} {value}")
|
2021-12-17 08:09:21 +00:00
|
|
|
mess_dict = {
|
2022-06-16 03:37:46 +00:00
|
|
|
"status": self.MSG,
|
2021-12-17 08:09:21 +00:00
|
|
|
"level": "error",
|
|
|
|
"title": "Scheduler update failed.",
|
|
|
|
"message": "Invalid schedule input",
|
|
|
|
}
|
2022-06-16 03:37:46 +00:00
|
|
|
RedisArchivist().set_message(
|
|
|
|
self.MSG, mess_dict, expire=True
|
|
|
|
)
|
2021-12-17 08:09:21 +00:00
|
|
|
return
|
|
|
|
|
2021-12-02 11:37:54 +00:00
|
|
|
redis_config["scheduler"][key] = to_write
|
2022-04-07 15:30:20 +00:00
|
|
|
if key in self.CONFIG and value:
|
|
|
|
redis_config["scheduler"][key] = int(value)
|
2023-07-29 16:41:54 +00:00
|
|
|
if key in self.NOTIFY and value:
|
|
|
|
if value == "0":
|
|
|
|
to_write = False
|
|
|
|
else:
|
|
|
|
to_write = value
|
|
|
|
redis_config["scheduler"][key] = to_write
|
|
|
|
|
2023-08-23 11:35:20 +00:00
|
|
|
RedisArchivist().set_message("config", redis_config, save=True)
|
2021-12-17 08:09:21 +00:00
|
|
|
mess_dict = {
|
2022-06-16 03:37:46 +00:00
|
|
|
"status": self.MSG,
|
2021-12-17 08:09:21 +00:00
|
|
|
"level": "info",
|
|
|
|
"title": "Scheduler changed.",
|
|
|
|
"message": "Please restart container for changes to take effect",
|
|
|
|
}
|
2022-06-16 03:37:46 +00:00
|
|
|
RedisArchivist().set_message(self.MSG, mess_dict, expire=True)
|
2021-12-02 11:37:54 +00:00
|
|
|
|
2022-04-07 15:30:20 +00:00
|
|
|
def value_builder(self, key, value):
|
2021-12-03 04:27:26 +00:00
|
|
|
"""validate single cron form entry and return cron dict"""
|
2022-04-07 15:30:20 +00:00
|
|
|
print(f"change schedule for {key} to {value}")
|
|
|
|
if value == "0":
|
2021-12-03 04:27:26 +00:00
|
|
|
# deactivate this schedule
|
|
|
|
return False
|
2022-04-07 15:30:20 +00:00
|
|
|
if re.search(r"[\d]{1,2}\/[\d]{1,2}", value):
|
2022-01-08 13:04:51 +00:00
|
|
|
# number/number cron format will fail in celery
|
|
|
|
print("number/number schedule formatting not supported")
|
|
|
|
raise ValueError
|
2021-12-03 04:27:26 +00:00
|
|
|
|
|
|
|
keys = ["minute", "hour", "day_of_week"]
|
2022-04-07 15:30:20 +00:00
|
|
|
if value == "auto":
|
2021-12-03 04:27:26 +00:00
|
|
|
# set to sensible default
|
|
|
|
values = self.SCHEDULES[key].split()
|
|
|
|
else:
|
2022-04-07 15:30:20 +00:00
|
|
|
values = value.split()
|
2021-12-03 04:27:26 +00:00
|
|
|
|
|
|
|
if len(keys) != len(values):
|
2022-04-07 15:30:20 +00:00
|
|
|
print(f"failed to parse {value} for {key}")
|
2021-12-17 08:09:21 +00:00
|
|
|
raise ValueError("invalid input")
|
2021-12-03 04:27:26 +00:00
|
|
|
|
|
|
|
to_write = dict(zip(keys, values))
|
2022-04-10 08:58:11 +00:00
|
|
|
self._validate_cron(to_write)
|
2021-12-03 04:27:26 +00:00
|
|
|
|
|
|
|
return to_write
|
|
|
|
|
2022-04-10 08:58:11 +00:00
|
|
|
@staticmethod
|
|
|
|
def _validate_cron(to_write):
|
|
|
|
"""validate all fields, raise value error for impossible schedule"""
|
|
|
|
all_hours = list(re.split(r"\D+", to_write["hour"]))
|
|
|
|
for hour in all_hours:
|
|
|
|
if hour.isdigit() and int(hour) > 23:
|
|
|
|
print("hour can not be greater than 23")
|
|
|
|
raise ValueError("invalid input")
|
|
|
|
|
|
|
|
all_days = list(re.split(r"\D+", to_write["day_of_week"]))
|
|
|
|
for day in all_days:
|
|
|
|
if day.isdigit() and int(day) > 6:
|
|
|
|
print("day can not be greater than 6")
|
|
|
|
raise ValueError("invalid input")
|
|
|
|
|
|
|
|
if not to_write["minute"].isdigit():
|
|
|
|
print("too frequent: only number in minutes are supported")
|
|
|
|
raise ValueError("invalid input")
|
|
|
|
|
|
|
|
if int(to_write["minute"]) > 59:
|
|
|
|
print("minutes can not be greater than 59")
|
|
|
|
raise ValueError("invalid input")
|
|
|
|
|
2021-12-02 11:37:54 +00:00
|
|
|
def build_schedule(self):
|
|
|
|
"""build schedule dict as expected by app.conf.beat_schedule"""
|
2023-02-02 06:06:00 +00:00
|
|
|
AppConfig().load_new_defaults()
|
2023-02-18 02:20:09 +00:00
|
|
|
self.config = AppConfig().config
|
2021-12-02 11:37:54 +00:00
|
|
|
schedule_dict = {}
|
|
|
|
|
|
|
|
for schedule_item in self.SCHEDULES:
|
|
|
|
item_conf = self.config["scheduler"][schedule_item]
|
|
|
|
if not item_conf:
|
|
|
|
continue
|
|
|
|
|
2023-01-11 15:00:44 +00:00
|
|
|
schedule_dict.update(
|
|
|
|
{
|
|
|
|
f"schedule_{schedule_item}": {
|
|
|
|
"task": schedule_item,
|
|
|
|
"schedule": crontab(
|
|
|
|
minute=item_conf["minute"],
|
|
|
|
hour=item_conf["hour"],
|
|
|
|
day_of_week=item_conf["day_of_week"],
|
|
|
|
),
|
|
|
|
}
|
2021-12-02 11:37:54 +00:00
|
|
|
}
|
2023-01-11 15:00:44 +00:00
|
|
|
)
|
2021-12-02 11:37:54 +00:00
|
|
|
|
|
|
|
return schedule_dict
|
2022-12-21 07:24:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ReleaseVersion:
|
|
|
|
"""compare local version with remote version"""
|
|
|
|
|
|
|
|
REMOTE_URL = "https://www.tubearchivist.com/api/release/latest/"
|
|
|
|
NEW_KEY = "versioncheck:new"
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.local_version = self._parse_version(settings.TA_VERSION)
|
|
|
|
self.is_unstable = settings.TA_VERSION.endswith("-unstable")
|
|
|
|
self.remote_version = False
|
|
|
|
self.is_breaking = False
|
|
|
|
self.response = False
|
|
|
|
|
|
|
|
def check(self):
|
|
|
|
"""check version"""
|
|
|
|
print(f"[{self.local_version}]: look for updates")
|
|
|
|
self.get_remote_version()
|
|
|
|
new_version, is_breaking = self._has_update()
|
|
|
|
if new_version:
|
|
|
|
message = {
|
|
|
|
"status": True,
|
|
|
|
"version": new_version,
|
|
|
|
"is_breaking": is_breaking,
|
|
|
|
}
|
|
|
|
RedisArchivist().set_message(self.NEW_KEY, message)
|
|
|
|
print(f"[{self.local_version}]: found new version {new_version}")
|
|
|
|
|
2023-06-28 03:50:28 +00:00
|
|
|
def get_local_version(self):
|
|
|
|
"""read version from local"""
|
|
|
|
return self.local_version
|
|
|
|
|
2022-12-21 07:24:24 +00:00
|
|
|
def get_remote_version(self):
|
|
|
|
"""read version from remote"""
|
|
|
|
self.response = requests.get(self.REMOTE_URL, timeout=20).json()
|
|
|
|
remote_version_str = self.response["release_version"]
|
|
|
|
self.remote_version = self._parse_version(remote_version_str)
|
|
|
|
self.is_breaking = self.response["breaking_changes"]
|
|
|
|
|
|
|
|
def _has_update(self):
|
|
|
|
"""check if there is an update"""
|
|
|
|
for idx, number in enumerate(self.local_version):
|
|
|
|
is_newer = self.remote_version[idx] > number
|
|
|
|
if is_newer:
|
|
|
|
return self.response["release_version"], self.is_breaking
|
|
|
|
|
|
|
|
if self.is_unstable and self.local_version == self.remote_version:
|
|
|
|
return self.response["release_version"], self.is_breaking
|
|
|
|
|
|
|
|
return False, False
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _parse_version(version):
|
|
|
|
"""return version parts"""
|
|
|
|
clean = version.rstrip("-unstable").lstrip("v")
|
|
|
|
return tuple((int(i) for i in clean.split(".")))
|
|
|
|
|
|
|
|
def is_updated(self):
|
|
|
|
"""check if update happened in the mean time"""
|
2022-12-21 11:26:32 +00:00
|
|
|
message = self.get_update()
|
|
|
|
if not message:
|
2023-02-02 05:06:24 +00:00
|
|
|
return False
|
2022-12-21 07:24:24 +00:00
|
|
|
|
|
|
|
if self._parse_version(message.get("version")) == self.local_version:
|
|
|
|
RedisArchivist().del_message(self.NEW_KEY)
|
2023-02-02 05:06:24 +00:00
|
|
|
return settings.TA_VERSION
|
|
|
|
|
|
|
|
return False
|
2022-12-21 11:26:32 +00:00
|
|
|
|
|
|
|
def get_update(self):
|
|
|
|
"""return new version dict if available"""
|
|
|
|
message = RedisArchivist().get_message(self.NEW_KEY)
|
|
|
|
if not message.get("status"):
|
|
|
|
return False
|
|
|
|
|
|
|
|
return message
|