Merge branch 'dev' into 'master'

v4.0.21

See merge request crafty-controller/crafty-4!568
This commit is contained in:
Iain Powrie 2023-03-04 20:07:51 +00:00
commit d03966a54f
126 changed files with 18078 additions and 79114 deletions

View File

@ -1,5 +1,34 @@
# Changelog # Changelog
## --- [4.0.20] - 2022/01/29 ## --- [4.0.21] - 2023/03/04
### New features
- Add better feedback for uploads with a progress bar ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/546))
- Add ignored exit codes for crash detection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/553))
- Allow users to change the directory where Crafty Stores Servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/539)) <br>
*(Only for non-docker, docker users should change host volume mount)*
- Add host storage display option to the dashboard ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/551))
### Bug fixes
- Fix exception related to page data on server start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/544))
- Fix logical issue with uploading dynamic files ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/555))
- Fix backups failing by correctly using tz objects ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/556))
- Bump Cryptography/pyOpenSSL for CVE-2023-23931 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/554))
- Fix debug logging to only display with the -v (verbose) flag ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/560))
- Optimize world size calculation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/550))
- Only copy bedrock_server executable on update ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/562))
- Fix bug where unloaded servers could not be deleted ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/566))
- Fix bug where "servers" was not appended ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/567))
### Tweaks
- Cleanup authentication helpers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/545))
- Optimize file upload progress WS ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/546))
- Truncate sidebar servers to a max of 10 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/552))
- Upgrade to FA 6. Add Translations ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/549))([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/558))
- Forge installer and Java Detection improvements ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/559))
- Crafty log clean up -config option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/563))
### Lang
- Add additional translations to backups page strings ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/543))
- Add additional missing translations ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/549))
<br><br>
## --- [4.0.20] - 2023/01/29
### New features ### New features
- Add option to run command before backup. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/536)) - Add option to run command before backup. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/536))
- Make Config.json editable from panel. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/532)) - Make Config.json editable from panel. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/532))
@ -19,7 +48,7 @@
- Add Default redirection to Dashboard if the user is connected. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/540)) - Add Default redirection to Dashboard if the user is connected. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/540))
<br><br> <br><br>
## --- [4.0.19] - 2022/01/07 ## --- [4.0.19] - 2023/01/07
### Bug fixes ### Bug fixes
- Fix port tooltip not showing on dash while server online. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/503)) - Fix port tooltip not showing on dash while server online. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/503))
- Fix '+' char in path causing any file operation to fail. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/502)) - Fix '+' char in path causing any file operation to fail. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/502))

View File

@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com) [![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.0.20 # Crafty Controller 4.0.21
> Python based Control Panel for your Minecraft Server > Python based Control Panel for your Minecraft Server
## What is Crafty Controller? ## What is Crafty Controller?
@ -18,6 +18,8 @@ Discord Server - https://discord.gg/9VJPhCE
Git Repository - https://gitlab.com/crafty-controller/crafty-4 Git Repository - https://gitlab.com/crafty-controller/crafty-4
Docker Hub - [arcadiatechnology/crafty-4](https://hub.docker.com/r/arcadiatechnology/crafty-4)
<br> <br>
## Basic Docker Usage 🐳 ## Basic Docker Usage 🐳

View File

@ -195,3 +195,14 @@ class ManagementController:
def del_excluded_backup_dir(self, server_id: int, dir_to_del: str): def del_excluded_backup_dir(self, server_id: int, dir_to_del: str):
self.management_helper.del_excluded_backup_dir(server_id, dir_to_del) self.management_helper.del_excluded_backup_dir(server_id, dir_to_del)
# **********************************************************************************
# Crafty Methods
# **********************************************************************************
@staticmethod
def get_master_server_dir():
return HelpersManagement.get_master_server_dir()
@staticmethod
def set_master_server_dir(server_dir):
HelpersManagement.set_master_server_dir(server_dir)

View File

@ -102,6 +102,7 @@ class ServersController(metaclass=Singleton):
server_obj.server_id server_obj.server_id
) )
server_instance.update_server_instance() server_instance.update_server_instance()
return ret return ret
def get_history_stats(self, server_id, days): def get_history_stats(self, server_id, days):
@ -163,10 +164,9 @@ class ServersController(metaclass=Singleton):
return server["server_obj"] return server["server_obj"]
logger.warning(f"Unable to find server object for server id {server_id}") logger.warning(f"Unable to find server object for server id {server_id}")
raise Exception(f"Unable to find server object for server id {server_id}") raise ValueError(f"Unable to find server object for server id {server_id}")
def init_all_servers(self): def init_all_servers(self):
servers = self.get_all_defined_servers() servers = self.get_all_defined_servers()
self.failed_servers = [] self.failed_servers = []
@ -227,7 +227,6 @@ class ServersController(metaclass=Singleton):
) )
def check_server_loaded(self, server_id_to_check: int): def check_server_loaded(self, server_id_to_check: int):
logger.info(f"Checking to see if we already registered {server_id_to_check}") logger.info(f"Checking to see if we already registered {server_id_to_check}")
for server in self.servers_list: for server in self.servers_list:

View File

@ -19,7 +19,6 @@ class Server:
self.description = data.get("description") self.description = data.get("description")
# print(self.description) # print(self.description)
if isinstance(self.description, dict): if isinstance(self.description, dict):
# cat server # cat server
if "translate" in self.description: if "translate" in self.description:
self.description = self.description["translate"] self.description = self.description["translate"]
@ -124,7 +123,7 @@ def ping(ip, port):
try: try:
k = sock.recv(1) k = sock.recv(1)
if not k: if not k:
raise Exception() raise ValueError()
except: except:
return 0 return 0
k = k[0] k = k[0]

View File

@ -104,7 +104,6 @@ class ServerJars:
logger.error(f"Unable to update serverjars.com cache file: {e}") logger.error(f"Unable to update serverjars.com cache file: {e}")
def refresh_cache(self): def refresh_cache(self):
cache_file = self.helper.serverjar_cache cache_file = self.helper.serverjar_cache
cache_old = self.helper.is_file_older_than_x_days(cache_file) cache_old = self.helper.is_file_older_than_x_days(cache_file)

View File

@ -210,8 +210,7 @@ class Stats:
return disk_data return disk_data
@staticmethod @staticmethod
def get_world_size(server_path): def get_server_dir_size(server_path):
total_size = 0 total_size = 0
total_size = Helpers.get_dir_size(server_path) total_size = Helpers.get_dir_size(server_path)
@ -221,7 +220,6 @@ class Stats:
return level_total_size return level_total_size
def get_server_players(self, server_id): def get_server_players(self, server_id):
server = HelperServers.get_server_data_by_id(server_id) server = HelperServers.get_server_data_by_id(server_id)
logger.info(f"Getting players for server {server}") logger.info(f"Getting players for server {server}")
@ -295,7 +293,6 @@ class Stats:
@staticmethod @staticmethod
def parse_server_raknet_ping(ping_obj: object): def parse_server_raknet_ping(ping_obj: object):
try: try:
server_icon = base64.encodebytes(ping_obj["icon"]) server_icon = base64.encodebytes(ping_obj["icon"])
except Exception as e: except Exception as e:

View File

@ -15,6 +15,7 @@ from app.classes.shared.permission_helper import PermissionHelper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ********************************************************************************** # **********************************************************************************
# User_Crafty Class # User_Crafty Class
# ********************************************************************************** # **********************************************************************************

View File

@ -20,6 +20,7 @@ from app.classes.shared.main_models import DatabaseShortcuts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ********************************************************************************** # **********************************************************************************
# Audit_Log Class # Audit_Log Class
# ********************************************************************************** # **********************************************************************************
@ -46,6 +47,7 @@ class CraftySettings(BaseModel):
cookie_secret = CharField(default="") cookie_secret = CharField(default="")
login_photo = CharField(default="login_1.jpg") login_photo = CharField(default="login_1.jpg")
login_opacity = IntegerField(default=100) login_opacity = IntegerField(default=100)
master_server_dir = CharField(default="")
class Meta: class Meta:
table_name = "crafty_settings" table_name = "crafty_settings"
@ -271,6 +273,19 @@ class HelpersManagement:
CraftySettings.id == 1 CraftySettings.id == 1
).execute() ).execute()
@staticmethod
def get_master_server_dir():
settings = CraftySettings.select(CraftySettings.master_server_dir).where(
CraftySettings.id == 1
)
return settings[0].master_server_dir
@staticmethod
def set_master_server_dir(server_dir):
CraftySettings.update({CraftySettings.master_server_dir: server_dir}).where(
CraftySettings.id == 1
).execute()
# ********************************************************************************** # **********************************************************************************
# Schedules Methods # Schedules Methods
# ********************************************************************************** # **********************************************************************************

View File

@ -15,6 +15,7 @@ from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ********************************************************************************** # **********************************************************************************
# Roles Class # Roles Class
# ********************************************************************************** # **********************************************************************************

View File

@ -16,6 +16,7 @@ from app.classes.shared.permission_helper import PermissionHelper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ********************************************************************************** # **********************************************************************************
# Role Servers Class # Role Servers Class
# ********************************************************************************** # **********************************************************************************

View File

@ -29,6 +29,7 @@ logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger("peewee") peewee_logger = logging.getLogger("peewee")
peewee_logger.setLevel(logging.INFO) peewee_logger.setLevel(logging.INFO)
# ********************************************************************************** # **********************************************************************************
# Servers Stats Class # Servers Stats Class
# ********************************************************************************** # **********************************************************************************

View File

@ -15,6 +15,7 @@ from app.classes.models.base_model import BaseModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ********************************************************************************** # **********************************************************************************
# Servers Model # Servers Model
# ********************************************************************************** # **********************************************************************************
@ -40,6 +41,7 @@ class Servers(BaseModel):
show_status = BooleanField(default=1) show_status = BooleanField(default=1)
created_by = IntegerField(default=-100) created_by = IntegerField(default=-100)
shutdown_timeout = IntegerField(default=60) shutdown_timeout = IntegerField(default=60)
ignored_exits = CharField(default="0")
class Meta: class Meta:
table_name = "servers" table_name = "servers"

View File

@ -21,6 +21,7 @@ from app.classes.models.roles import Roles, HelperRoles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ********************************************************************************** # **********************************************************************************
# Users Class # Users Class
# ********************************************************************************** # **********************************************************************************
@ -58,6 +59,7 @@ PUBLIC_USER_ATTRS: t.Final = [
"lang", # maybe remove? "lang", # maybe remove?
] ]
# ********************************************************************************** # **********************************************************************************
# API Keys Class # API Keys Class
# ********************************************************************************** # **********************************************************************************

View File

@ -13,7 +13,6 @@ logger = logging.getLogger(__name__)
class Authentication: class Authentication:
def __init__(self, helper): def __init__(self, helper):
self.helper = helper self.helper = helper
self.secret = "my secret"
try: try:
self.secret = ManagementController.get_crafty_api_key() self.secret = ManagementController.get_crafty_api_key()
if self.secret == "": if self.secret == "":
@ -77,7 +76,7 @@ class Authentication:
output = self.check(token) output = self.check(token)
if output is None: if output is None:
raise Exception("Invalid token") raise ValueError("Invalid token")
return output return output
def check_bool(self, token) -> bool: def check_bool(self, token) -> bool:

View File

@ -58,7 +58,6 @@ class MainPrompt(cmd.Cmd):
Console.info("Unknown migration command") Console.info("Unknown migration command")
def do_set_passwd(self, line): def do_set_passwd(self, line):
try: try:
username = str(line).lower() username = str(line).lower()
# If no user is found it returns None # If no user is found it returns None

View File

@ -19,6 +19,8 @@ except ModuleNotFoundError as ex:
class Console: class Console:
level = ""
def __init__(self): def __init__(self):
if "colorama" in sys.modules: if "colorama" in sys.modules:
init() init()
@ -61,8 +63,9 @@ class Console:
@staticmethod @staticmethod
def debug(message): def debug(message):
date_time = Console.get_fmt_date_time() if Console.level == "debug":
Console.magenta(f"[+] Crafty: {date_time} - DEBUG:\t{message}") date_time = Console.get_fmt_date_time()
Console.magenta(f"[+] Crafty: {date_time} - DEBUG:\t{message}")
@staticmethod @staticmethod
def info(message): def info(message):

View File

@ -283,27 +283,31 @@ class FileHelpers:
return True return True
@staticmethod @staticmethod
def unzip_file(zip_path): def unzip_file(zip_path, server_update=False):
new_dir_list = zip_path.split("/") ignored_names = ["server.properties", "permissions.json", "allowlist.json"]
new_dir = "" # Get directory without zipfile name
for i in range(len(new_dir_list) - 1): new_dir = pathlib.Path(zip_path).parents[0]
if i == 0: # make sure we're able to access the zip file
new_dir += new_dir_list[i]
else:
new_dir += "/" + new_dir_list[i]
if Helpers.check_file_perms(zip_path) and os.path.isfile(zip_path): if Helpers.check_file_perms(zip_path) and os.path.isfile(zip_path):
# make sure the directory we're unzipping this to exists
Helpers.ensure_dir_exists(new_dir) Helpers.ensure_dir_exists(new_dir)
# we'll make a temporary directory to unzip this to.
temp_dir = tempfile.mkdtemp() temp_dir = tempfile.mkdtemp()
try: try:
with zipfile.ZipFile(zip_path, "r") as zip_ref: with zipfile.ZipFile(zip_path, "r") as zip_ref:
# we'll extract this to the temp dir using zipfile module
zip_ref.extractall(temp_dir) zip_ref.extractall(temp_dir)
full_root_path = temp_dir # we'll iterate through the top level directory moving everything
for item in os.listdir(full_root_path): # out of the temp directory and into it's final home.
if os.path.isdir(os.path.join(full_root_path, item)): for item in os.listdir(temp_dir):
# if the file is one of our ignored names we'll skip it
if item in ignored_names and server_update:
continue
# we handle files and dirs differently or we'll crash out.
if os.path.isdir(os.path.join(temp_dir, item)):
try: try:
FileHelpers.move_dir_exist( FileHelpers.move_dir_exist(
os.path.join(full_root_path, item), os.path.join(temp_dir, item),
os.path.join(new_dir, item), os.path.join(new_dir, item),
) )
except Exception as ex: except Exception as ex:
@ -311,7 +315,7 @@ class FileHelpers:
else: else:
try: try:
FileHelpers.move_file( FileHelpers.move_file(
os.path.join(full_root_path, item), os.path.join(temp_dir, item),
os.path.join(new_dir, item), os.path.join(new_dir, item),
) )
except Exception as ex: except Exception as ex:

View File

@ -15,6 +15,7 @@ import html
import zipfile import zipfile
import pathlib import pathlib
import ctypes import ctypes
import shutil
import subprocess import subprocess
import itertools import itertools
from datetime import datetime from datetime import datetime
@ -62,6 +63,7 @@ class Helpers:
self.servers_dir = os.path.join(self.root_dir, "servers") self.servers_dir = os.path.join(self.root_dir, "servers")
self.backup_path = os.path.join(self.root_dir, "backups") self.backup_path = os.path.join(self.root_dir, "backups")
self.migration_dir = os.path.join(self.root_dir, "app", "migrations") self.migration_dir = os.path.join(self.root_dir, "app", "migrations")
self.dir_migration = False
self.session_file = os.path.join(self.root_dir, "app", "config", "session.lock") self.session_file = os.path.join(self.root_dir, "app", "config", "session.lock")
self.settings_file = os.path.join(self.root_dir, "app", "config", "config.json") self.settings_file = os.path.join(self.root_dir, "app", "config", "config.json")
@ -94,7 +96,7 @@ class Helpers:
try: try:
# Get tags from Gitlab, select the latest and parse the semver # Get tags from Gitlab, select the latest and parse the semver
response = get( response = get(
"https://gitlab.com/api/v4/projects/20430749/repository/tags" "https://gitlab.com/api/v4/projects/20430749/repository/tags", timeout=1
) )
if response.status_code == 200: if response.status_code == 200:
remote_version = pkg_version.parse(json.loads(response.text)[0]["name"]) remote_version = pkg_version.parse(json.loads(response.text)[0]["name"])
@ -131,7 +133,7 @@ class Helpers:
try: try:
# Get minecraft server download page # Get minecraft server download page
# (hopefully the don't change the structure) # (hopefully the don't change the structure)
download_page = get(url, headers=headers) download_page = get(url, headers=headers, timeout=1)
# Search for our string targets # Search for our string targets
win_download_url = re.search(target_win, download_page.text).group(0) win_download_url = re.search(target_win, download_page.text).group(0)
@ -145,6 +147,22 @@ class Helpers:
logger.error(f"Unable to resolve remote bedrock download url! \n{e}") logger.error(f"Unable to resolve remote bedrock download url! \n{e}")
return False return False
def detect_java(self):
if len(self.find_java_installs()) > 0:
return True
# We'll use this as a fallback for systems
# That do not properly setup reg keys or
# Update alternatives
if self.is_os_windows():
if shutil.which("java.exe"):
return True
else:
if shutil.which("java"):
return True
return False
@staticmethod @staticmethod
def find_java_installs(): def find_java_installs():
# If we're windows return oracle java versions, # If we're windows return oracle java versions,
@ -281,7 +299,7 @@ class Helpers:
@staticmethod @staticmethod
def check_port(server_port): def check_port(server_port):
try: try:
ip = get("https://api.ipify.org").content.decode("utf8") ip = get("https://api.ipify.org", timeout=1).content.decode("utf8")
except: except:
ip = "google.com" ip = "google.com"
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -395,6 +413,10 @@ class Helpers:
@staticmethod @staticmethod
def get_master_config(): def get_master_config():
# Let's get the mounts and only show the first one by default
mounts = Helpers.get_all_mounts()
if len(mounts) != 0:
mounts = mounts[0]
# Make changes for users' local config.json files here. As of 4.0.20 # Make changes for users' local config.json files here. As of 4.0.20
# Config.json was removed from the repo to make it easier for users # Config.json was removed from the repo to make it easier for users
# To make non-breaking changes to the file. # To make non-breaking changes to the file.
@ -405,7 +427,7 @@ class Helpers:
"cookie_expire": 30, "cookie_expire": 30,
"show_errors": True, "show_errors": True,
"history_max_age": 7, "history_max_age": 7,
"stats_update_frequency": 30, "stats_update_frequency_seconds": 30,
"delete_default_json": False, "delete_default_json": False,
"show_contribute_link": True, "show_contribute_link": True,
"virtual_terminal_lines": 70, "virtual_terminal_lines": 70,
@ -417,6 +439,9 @@ class Helpers:
"allow_nsfw_profile_pictures": False, "allow_nsfw_profile_pictures": False,
"enable_user_self_delete": False, "enable_user_self_delete": False,
"reset_secrets_on_next_boot": False, "reset_secrets_on_next_boot": False,
"monitored_mounts": mounts,
"dir_size_poll_freq_minutes": 5,
"crafty_logs_delete_after_days": 0,
} }
def get_all_settings(self): def get_all_settings(self):
@ -436,11 +461,27 @@ class Helpers:
return data return data
@staticmethod @staticmethod
def is_subdir(server_path, root_dir): def get_all_mounts():
mounts = []
for item in psutil.disk_partitions(all=False):
mounts.append(item.mountpoint)
return mounts
def is_subdir(self, server_path, root_dir):
server_path = os.path.realpath(server_path) server_path = os.path.realpath(server_path)
root_dir = os.path.realpath(root_dir) root_dir = os.path.realpath(root_dir)
relative = os.path.relpath(server_path, root_dir) if self.is_os_windows():
try:
relative = os.path.relpath(server_path, root_dir)
except:
# Windows will crash out if two paths are on different
# Drives We can happily return false if this is the case.
# Since two different drives will not be relative to eachother.
return False
else:
relative = os.path.relpath(server_path, root_dir)
if relative.startswith(os.pardir): if relative.startswith(os.pardir):
return False return False
@ -596,7 +637,6 @@ class Helpers:
# open our file # open our file
with open(file_name, "r", encoding="utf-8") as f: with open(file_name, "r", encoding="utf-8") as f:
# seek # seek
f.seek(0, 2) f.seek(0, 2)
@ -752,7 +792,7 @@ class Helpers:
use_ssl=True, use_ssl=True,
) # + "?d=404" ) # + "?d=404"
try: try:
if requests.head(url).status_code != 404: if requests.head(url, timeout=1).status_code != 404:
profile_url = url profile_url = url
except Exception as e: except Exception as e:
logger.debug(f"Could not pull resource from Gravatar with error {e}") logger.debug(f"Could not pull resource from Gravatar with error {e}")
@ -761,7 +801,6 @@ class Helpers:
@staticmethod @staticmethod
def get_file_contents(path: str, lines=100): def get_file_contents(path: str, lines=100):
contents = "" contents = ""
if os.path.exists(path) and os.path.isfile(path): if os.path.exists(path) and os.path.isfile(path):
@ -782,12 +821,10 @@ class Helpers:
return False return False
def create_session_file(self, ignore=False): def create_session_file(self, ignore=False):
if ignore and os.path.exists(self.session_file): if ignore and os.path.exists(self.session_file):
os.remove(self.session_file) os.remove(self.session_file)
if os.path.exists(self.session_file): if os.path.exists(self.session_file):
file_data = self.get_file_contents(self.session_file) file_data = self.get_file_contents(self.session_file)
try: try:
data = json.loads(file_data) data = json.loads(file_data)
@ -887,15 +924,16 @@ class Helpers:
try: try:
os.makedirs(path) os.makedirs(path)
logger.debug(f"Created Directory : {path}") logger.debug(f"Created Directory : {path}")
return True
# directory already exists - non-blocking error # directory already exists - non-blocking error
except FileExistsError: except FileExistsError:
pass return True
except PermissionError as e: except PermissionError as e:
logger.critical(f"Check generated exception due to permssion error: {e}") logger.critical(f"Check generated exception due to permssion error: {e}")
return False
def create_self_signed_cert(self, cert_dir=None): def create_self_signed_cert(self, cert_dir=None):
if cert_dir is None: if cert_dir is None:
cert_dir = os.path.join(self.config_dir, "web", "certs") cert_dir = os.path.join(self.config_dir, "web", "certs")
@ -977,6 +1015,15 @@ class Helpers:
def is_os_windows(): def is_os_windows():
return os.name == "nt" return os.name == "nt"
@staticmethod
def is_env_docker():
path = "/proc/self/cgroup"
return (
os.path.exists("/.dockerenv")
or os.path.isfile(path)
and any("docker" in line for line in open(path, encoding="utf-8"))
)
@staticmethod @staticmethod
def wtol_path(w_path): def wtol_path(w_path):
l_path = w_path.replace("\\", "/") l_path = w_path.replace("\\", "/")
@ -1013,7 +1060,7 @@ class Helpers:
for item in file_list: for item in file_list:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item) dir_list.append(item)
elif str(item) != "crafty.sqlite": elif str(item) != self.ignored_names:
unsorted_files.append(item) unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted( file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold unsorted_files, key=str.casefold
@ -1026,9 +1073,9 @@ class Helpers:
if filename not in self.ignored_names: if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item" output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}"> data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" \n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder"> class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" <span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)"> data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i> <i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i> <i style="color: var(--info);" class="far fa-folder-open"></i>
@ -1047,14 +1094,13 @@ class Helpers:
return output return output
def generate_dir(self, folder, output=""): def generate_dir(self, folder, output=""):
dir_list = [] dir_list = []
unsorted_files = [] unsorted_files = []
file_list = os.listdir(folder) file_list = os.listdir(folder)
for item in file_list: for item in file_list:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item) dir_list.append(item)
elif str(item) != "crafty.sqlite": elif str(item) != self.ignored_names:
unsorted_files.append(item) unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted( file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold unsorted_files, key=str.casefold
@ -1068,9 +1114,9 @@ class Helpers:
if filename not in self.ignored_names: if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item" output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}"> data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" \n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder"> class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" <span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)"> data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i> <i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i> <i style="color: var(--info);" class="far fa-folder-open"></i>

View File

@ -226,7 +226,6 @@ class ImportHelpers:
download_thread.start() download_thread.start()
def download_threaded_bedrock_server(self, path, new_id): def download_threaded_bedrock_server(self, path, new_id):
# downloads zip from remote url # downloads zip from remote url
try: try:
bedrock_url = Helpers.get_latest_bedrock_url() bedrock_url = Helpers.get_latest_bedrock_url()

View File

@ -10,7 +10,6 @@ class Install:
) )
def do_install(self): def do_install(self):
# are we in a venv? # are we in a venv?
if not self.is_venv(): if not self.is_venv():
print("Crafty Requires a venv to install") print("Crafty Requires a venv to install")

View File

@ -6,6 +6,7 @@ import platform
import shutil import shutil
import time import time
import logging import logging
import threading
from peewee import DoesNotExist from peewee import DoesNotExist
# TZLocal is set as a hidden import on win pipeline # TZLocal is set as a hidden import on win pipeline
@ -347,7 +348,7 @@ class Controller:
elif root_create_data["create_type"] == "import_zip": elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory # TODO: Copy files from the zip file to the new server directory
server_file = create_data["jarfile"] server_file = create_data["jarfile"]
raise Exception("Not yet implemented") raise NotImplementedError("Not yet implemented")
_create_server_properties_if_needed( _create_server_properties_if_needed(
create_data["server_properties_port"], create_data["server_properties_port"],
) )
@ -379,7 +380,7 @@ class Controller:
logger.error(f"Server import failed with error: {ex}") logger.error(f"Server import failed with error: {ex}")
elif root_create_data["create_type"] == "import_zip": elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory # TODO: Copy files from the zip file to the new server directory
raise Exception("Not yet implemented") raise NotImplementedError("Not yet implemented")
_create_server_properties_if_needed(0, True) _create_server_properties_if_needed(0, True)
@ -401,7 +402,7 @@ class Controller:
logger.error(f"Server import failed with error: {ex}") logger.error(f"Server import failed with error: {ex}")
elif root_create_data["create_type"] == "import_zip": elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory # TODO: Copy files from the zip file to the new server directory
raise Exception("Not yet implemented") raise NotImplementedError("Not yet implemented")
_create_server_properties_if_needed(0, True) _create_server_properties_if_needed(0, True)
@ -941,7 +942,6 @@ class Controller:
def remove_server(self, server_id, files): def remove_server(self, server_id, files):
counter = 0 counter = 0
for server in self.servers.servers_list: for server in self.servers.servers_list:
# if this is the droid... im mean server we are looking for... # if this is the droid... im mean server we are looking for...
if str(server["server_id"]) == str(server_id): if str(server["server_id"]) == str(server_id):
server_data = self.servers.get_server_data(server_id) server_data = self.servers.get_server_data(server_id)
@ -1003,3 +1003,125 @@ class Controller:
@staticmethod @staticmethod
def clear_support_status(): def clear_support_status():
HelperUsers.clear_support_status() HelperUsers.clear_support_status()
def set_master_server_dir(self, server_dir):
# This method should only be used on a first run basis if the server dir is ""
self.helper.servers_dir = server_dir
HelpersManagement.set_master_server_dir(server_dir)
def update_master_server_dir(self, server_dir, user_id):
self.helper.dir_migration = True
move_thread = threading.Thread(
name="dir_move",
target=self.t_update_master_server_dir,
daemon=True,
args=(
server_dir,
user_id,
),
)
move_thread.start()
def t_update_master_server_dir(self, new_server_path, user_id):
new_server_path = self.helper.wtol_path(new_server_path)
new_server_path = os.path.join(new_server_path, "servers")
self.helper.websocket_helper.broadcast_page(
"/panel/panel_config", "move_status", "Checking dir"
)
current_master = self.helper.wtol_path(
HelpersManagement.get_master_server_dir()
)
if current_master == new_server_path:
logger.info(
"Admin tried to change server dir to current server dir. Canceling..."
)
self.helper.websocket_helper.broadcast_page(
"/panel/panel_config",
"move_status",
"done",
)
return
if self.helper.is_subdir(new_server_path, current_master):
logger.info(
"Admin tried to change server dir to be inside a sub directory of the"
" current server dir. This will result in a copy loop."
)
self.helper.websocket_helper.broadcast_page(
"/panel/panel_config",
"move_status",
"done",
)
return
self.helper.websocket_helper.broadcast_page(
"/panel/panel_config", "move_status", "Checking permissions"
)
if not self.helper.ensure_dir_exists(new_server_path):
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": "Crafty failed to move server dir. "
"It seems Crafty lacks permission to write to "
"the new directory."
},
)
return
# set the cached serve dir
self.helper.servers_dir = new_server_path
# set DB server dir
HelpersManagement.set_master_server_dir(new_server_path)
servers = self.servers.get_all_defined_servers()
# move the servers
for server in servers:
server_path = server.get("path")
new_local_server_path = os.path.join(
new_server_path, server.get("server_uuid")
)
if os.path.isdir(server_path):
self.helper.websocket_helper.broadcast_page(
"/panel/panel_config",
"move_status",
f"Moving {server.get('server_name')}",
)
try:
self.file_helper.move_dir(
server_path,
new_local_server_path,
)
except FileExistsError as e:
logger.error(f"Failed to move server with error: {e}")
server_obj = self.servers.get_server_obj(server.get("server_id"))
# reset executable path
if current_master in server["executable"]:
server_obj.executable = str(server["executable"]).replace(
current_master, new_local_server_path
)
# reset run command path
if current_master in server["execution_command"]:
server_obj.execution_command = str(server["execution_command"]).replace(
current_master, new_local_server_path
)
# reset log path
if current_master in server["log_path"]:
server_obj.log_path = str(server["log_path"]).replace(
current_master, new_local_server_path
)
server_obj.path = new_local_server_path
failed = False
for s in self.servers.failed_servers:
if int(s["server_id"]) == int(server.get("server_id")):
failed = True
if not failed:
self.servers.update_server(server_obj)
else:
self.servers.update_unloaded_server(server_obj)
self.servers.init_all_servers()
self.helper.dir_migration = False
self.helper.websocket_helper.broadcast_page(
"/panel/panel_config",
"move_status",
"done",
)

View File

@ -12,6 +12,8 @@ import html
import urllib.request import urllib.request
import glob import glob
from zoneinfo import ZoneInfo
# TZLocal is set as a hidden import on win pipeline # TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError from tzlocal.utils import ZoneInfoNotFoundError
@ -136,9 +138,12 @@ class ServerInstance:
logger.error( logger.error(
"Could not capture time zone from system. Falling back to Europe/London" "Could not capture time zone from system. Falling back to Europe/London"
) )
self.tz = "Europe/London" self.tz = ZoneInfo("Europe/London")
self.server_scheduler = BackgroundScheduler(timezone=str(self.tz)) self.server_scheduler = BackgroundScheduler(timezone=str(self.tz))
self.dir_scheduler = BackgroundScheduler(timezone=str(self.tz))
self.server_scheduler.start() self.server_scheduler.start()
self.dir_scheduler.start()
self.start_dir_calc_task()
self.backup_thread = threading.Thread( self.backup_thread = threading.Thread(
target=self.a_backup_server, daemon=True, name=f"backup_{self.name}" target=self.a_backup_server, daemon=True, name=f"backup_{self.name}"
) )
@ -156,6 +161,8 @@ class ServerInstance:
self.jar_update_url = server_data.executable_update_url self.jar_update_url = server_data.executable_update_url
self.name = server_data.server_name self.name = server_data.server_name
self.server_object = server_data self.server_object = server_data
self.stats_helper.select_database()
self.reload_server_settings()
def reload_server_settings(self): def reload_server_settings(self):
server_data = HelperServers.get_server_data_by_id(self.server_id) server_data = HelperServers.get_server_data_by_id(self.server_id)
@ -301,6 +308,22 @@ class ServerInstance:
else: else:
user_lang = HelperUsers.get_user_lang_by_id(user_id) user_lang = HelperUsers.get_user_lang_by_id(user_id)
# Checks if user is currently attempting to move global server
# dir
if self.helper.dir_migration:
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error",
"migration",
user_lang,
)
},
)
return False
if self.stats_helper.get_import_status() and not forge_install: if self.stats_helper.get_import_status() and not forge_install:
if user_id: if user_id:
self.helper.websocket_helper.broadcast_user( self.helper.websocket_helper.broadcast_user(
@ -443,7 +466,7 @@ class ServerInstance:
) )
except Exception as ex: except Exception as ex:
# Checks for java on initial fail # Checks for java on initial fail
if os.system("java -version") == 32512: if not self.helper.detect_java():
if user_id: if user_id:
self.helper.websocket_helper.broadcast_user( self.helper.websocket_helper.broadcast_user(
user_id, user_id,
@ -588,7 +611,6 @@ class ServerInstance:
# We need to grab the exact forge version number. # We need to grab the exact forge version number.
# We know we can find it here in the run.sh/bat script. # We know we can find it here in the run.sh/bat script.
try: try:
# Getting the forge version from the executable command # Getting the forge version from the executable command
version = re.findall( version = re.findall(
r"forge-([0-9\.]+)((?:)|(?:-([0-9\.]+)-[a-zA-Z]+)).jar", r"forge-([0-9\.]+)((?:)|(?:-([0-9\.]+)-[a-zA-Z]+)).jar",
@ -672,7 +694,7 @@ class ServerInstance:
execution_command = ( execution_command = (
f"java @{server_command[0]}" f"java @{server_command[0]}"
f" @{executable_path}{server_command[3]} nogui" f" @{executable_path}{server_command[3]} nogui"
" {server_command[4]}" f" {server_command[4]}"
) )
server_obj.execution_command = execution_command server_obj.execution_command = execution_command
Console.debug("SUCCESS! Forge install completed") Console.debug("SUCCESS! Forge install completed")
@ -730,26 +752,26 @@ class ServerInstance:
self.server_thread.join() self.server_thread.join()
def stop_server(self): def stop_server(self):
if self.settings["stop_command"]:
self.send_command(self.settings["stop_command"])
if self.settings["crash_detection"]:
# remove crash detection watcher
logger.info(f"Removing crash watcher for server {self.name}")
try:
self.server_scheduler.remove_job("c_" + str(self.server_id))
except:
logger.error(
f"Removing crash watcher for server {self.name} failed. "
f"Assuming it was never started."
)
else:
# windows will need to be handled separately for Ctrl+C
self.process.terminate()
running = self.check_running() running = self.check_running()
if not running: if not running:
logger.info(f"Can't stop server {self.name} if it's not running") logger.info(f"Can't stop server {self.name} if it's not running")
Console.info(f"Can't stop server {self.name} if it's not running") Console.info(f"Can't stop server {self.name} if it's not running")
return return
if self.settings["crash_detection"]:
# remove crash detection watcher
logger.info(f"Removing crash watcher for server {self.name}")
try:
self.server_scheduler.remove_job("c_" + str(self.server_id))
except:
logger.error(
f"Removing crash watcher for server {self.name} failed. "
f"Assuming it was never started."
)
if self.settings["stop_command"]:
self.send_command(self.settings["stop_command"])
else:
# windows will need to be handled separately for Ctrl+C
self.process.terminate()
i = 0 i = 0
# caching the name and pid number # caching the name and pid number
@ -848,7 +870,6 @@ class ServerInstance:
return True return True
def crash_detected(self, name): def crash_detected(self, name):
# clear the old scheduled watcher task # clear the old scheduled watcher task
self.server_scheduler.remove_job(f"c_{self.server_id}") self.server_scheduler.remove_job(f"c_{self.server_id}")
# remove the stats polling job since server is stopped # remove the stats polling job since server is stopped
@ -910,7 +931,6 @@ class ServerInstance:
return self.process.pid if self.process is not None else None return self.process.pid if self.process is not None else None
def detect_crash(self): def detect_crash(self):
logger.info(f"Detecting possible crash for server: {self.name} ") logger.info(f"Detecting possible crash for server: {self.name} ")
running = self.check_running() running = self.check_running()
@ -919,7 +939,7 @@ class ServerInstance:
if running: if running:
return return
# check the exit code -- This could be a fix for /stop # check the exit code -- This could be a fix for /stop
if self.process.returncode == 0: if str(self.process.returncode) in self.settings["ignored_exits"].split(","):
logger.warning( logger.warning(
f"Process {self.process.pid} exited with code " f"Process {self.process.pid} exited with code "
f"{self.process.returncode}. This is considered a clean exit" f"{self.process.returncode}. This is considered a clean exit"
@ -933,7 +953,6 @@ class ServerInstance:
self.stats_helper.sever_crashed() self.stats_helper.sever_crashed()
# if we haven't tried to restart more 3 or more times # if we haven't tried to restart more 3 or more times
if self.restart_count <= 3: if self.restart_count <= 3:
# start the server if needed # start the server if needed
server_restarted = self.crash_detected(self.name) server_restarted = self.crash_detected(self.name)
@ -1301,7 +1320,7 @@ class ServerInstance:
unzip_path = os.path.join(self.settings["path"], "bedrock_server.zip") unzip_path = os.path.join(self.settings["path"], "bedrock_server.zip")
unzip_path = self.helper.wtol_path(unzip_path) unzip_path = self.helper.wtol_path(unzip_path)
# unzips archive that was downloaded. # unzips archive that was downloaded.
FileHelpers.unzip_file(unzip_path) FileHelpers.unzip_file(unzip_path, server_update=True)
# adjusts permissions for execution if os is not windows # adjusts permissions for execution if os is not windows
if not self.helper.is_os_windows(): if not self.helper.is_os_windows():
os.chmod( os.chmod(
@ -1315,6 +1334,7 @@ class ServerInstance:
logger.critical( logger.critical(
f"Failed to download bedrock executable for update \n{e}" f"Failed to download bedrock executable for update \n{e}"
) )
downloaded = False
if downloaded: if downloaded:
logger.info("Executable updated successfully. Starting Server") logger.info("Executable updated successfully. Starting Server")
@ -1374,6 +1394,20 @@ class ServerInstance:
for user in server_users: for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "remove_spinner", {}) self.helper.websocket_helper.broadcast_user(user, "remove_spinner", {})
def start_dir_calc_task(self):
server_dt = HelperServers.get_server_data_by_id(self.server_id)
self.server_size = self.stats.get_server_dir_size(server_dt["path"])
self.dir_scheduler.add_job(
self.calc_dir_size,
"interval",
minutes=self.helper.get_setting("dir_size_poll_freq_minutes"),
id=str(self.server_id) + "_dir_poll",
)
def calc_dir_size(self):
server_dt = HelperServers.get_server_data_by_id(self.server_id)
self.server_size = self.stats.get_server_dir_size(server_dt["path"])
# ********************************************************************************** # **********************************************************************************
# Minecraft Servers Statistics # Minecraft Servers Statistics
# ********************************************************************************** # **********************************************************************************
@ -1459,7 +1493,6 @@ class ServerInstance:
Console.critical("Can't broadcast server status to websocket") Console.critical("Can't broadcast server status to websocket")
def get_servers_stats(self): def get_servers_stats(self):
server_stats = {} server_stats = {}
logger.info("Getting Stats for Server " + self.name + " ...") logger.info("Getting Stats for Server " + self.name + " ...")
@ -1472,9 +1505,6 @@ class ServerInstance:
# get our server object, settings and data dictionaries # get our server object, settings and data dictionaries
self.reload_server_settings() self.reload_server_settings()
# world data
server_path = server["path"]
# process stats # process stats
p_stats = Stats._try_get_process_stats(self.process, self.check_running()) p_stats = Stats._try_get_process_stats(self.process, self.check_running())
@ -1515,7 +1545,7 @@ class ServerInstance:
"mem": p_stats.get("memory_usage", 0), "mem": p_stats.get("memory_usage", 0),
"mem_percent": p_stats.get("mem_percentage", 0), "mem_percent": p_stats.get("mem_percentage", 0),
"world_name": server_name, "world_name": server_name,
"world_size": Stats.get_world_size(server_path), "world_size": self.server_size,
"server_port": server_port, "server_port": server_port,
"int_ping_results": int_data, "int_ping_results": int_data,
"online": ping_data.get("online", False), "online": ping_data.get("online", False),
@ -1533,7 +1563,7 @@ class ServerInstance:
"mem": p_stats.get("memory_usage", 0), "mem": p_stats.get("memory_usage", 0),
"mem_percent": p_stats.get("mem_percentage", 0), "mem_percent": p_stats.get("mem_percentage", 0),
"world_name": server_name, "world_name": server_name,
"world_size": Stats.get_world_size(server_path), "world_size": self.server_size,
"server_port": server_port, "server_port": server_port,
"int_ping_results": int_data, "int_ping_results": int_data,
"online": False, "online": False,
@ -1546,7 +1576,6 @@ class ServerInstance:
return server_stats return server_stats
def get_server_players(self): def get_server_players(self):
server = HelperServers.get_server_data_by_id(self.server_id) server = HelperServers.get_server_data_by_id(self.server_id)
logger.info(f"Getting players for server {server}") logger.info(f"Getting players for server {server}")
@ -1567,7 +1596,6 @@ class ServerInstance:
return [] return []
def get_raw_server_stats(self, server_id): def get_raw_server_stats(self, server_id):
try: try:
server = HelperServers.get_server_obj(server_id) server = HelperServers.get_server_obj(server_id)
except: except:
@ -1603,7 +1631,6 @@ class ServerInstance:
# world data # world data
server_name = server_dt["server_name"] server_name = server_dt["server_name"]
server_path = server_dt["path"]
# process stats # process stats
p_stats = Stats._try_get_process_stats(self.process, self.check_running()) p_stats = Stats._try_get_process_stats(self.process, self.check_running())
@ -1636,7 +1663,7 @@ class ServerInstance:
"mem": p_stats.get("memory_usage", 0), "mem": p_stats.get("memory_usage", 0),
"mem_percent": p_stats.get("mem_percentage", 0), "mem_percent": p_stats.get("mem_percentage", 0),
"world_name": server_name, "world_name": server_name,
"world_size": Stats.get_world_size(server_path), "world_size": self.server_size,
"server_port": server_port, "server_port": server_port,
"int_ping_results": int_data, "int_ping_results": int_data,
"online": ping_data.get("online", False), "online": ping_data.get("online", False),
@ -1665,7 +1692,7 @@ class ServerInstance:
"mem": p_stats.get("memory_usage", 0), "mem": p_stats.get("memory_usage", 0),
"mem_percent": p_stats.get("mem_percentage", 0), "mem_percent": p_stats.get("mem_percentage", 0),
"world_name": server_name, "world_name": server_name,
"world_size": Stats.get_world_size(server_path), "world_size": self.server_size,
"server_port": server_port, "server_port": server_port,
"int_ping_results": int_data, "int_ping_results": int_data,
"online": ping_data["online"], "online": ping_data["online"],
@ -1684,7 +1711,7 @@ class ServerInstance:
"mem": p_stats.get("memory_usage", 0), "mem": p_stats.get("memory_usage", 0),
"mem_percent": p_stats.get("mem_percentage", 0), "mem_percent": p_stats.get("mem_percentage", 0),
"world_name": server_name, "world_name": server_name,
"world_size": Stats.get_world_size(server_path), "world_size": self.server_size,
"server_port": server_port, "server_port": server_port,
"int_ping_results": int_data, "int_ping_results": int_data,
"online": False, "online": False,
@ -1703,7 +1730,7 @@ class ServerInstance:
"mem": p_stats.get("memory_usage", 0), "mem": p_stats.get("memory_usage", 0),
"mem_percent": p_stats.get("mem_percentage", 0), "mem_percent": p_stats.get("mem_percentage", 0),
"world_name": server_name, "world_name": server_name,
"world_size": Stats.get_world_size(server_path), "world_size": self.server_size,
"server_port": server_port, "server_port": server_port,
"int_ping_results": int_data, "int_ping_results": int_data,
"online": False, "online": False,
@ -1716,7 +1743,6 @@ class ServerInstance:
return server_stats return server_stats
def record_server_stats(self): def record_server_stats(self):
server_stats = self.get_servers_stats() server_stats = self.get_servers_stats()
self.stats_helper.insert_server_stats(server_stats) self.stats_helper.insert_server_stats(server_stats)

View File

@ -4,6 +4,7 @@ import logging
import threading import threading
import asyncio import asyncio
import datetime import datetime
import json
from tzlocal import get_localzone from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError from tzlocal.utils import ZoneInfoNotFoundError
@ -635,7 +636,9 @@ class TasksManager:
logger.error(f"Task failed with error: {event.exception}") logger.error(f"Task failed with error: {event.exception}")
def start_stats_recording(self): def start_stats_recording(self):
stats_update_frequency = self.helper.get_setting("stats_update_frequency") stats_update_frequency = self.helper.get_setting(
"stats_update_frequency_seconds"
)
logger.info( logger.info(
f"Stats collection frequency set to {stats_update_frequency} seconds" f"Stats collection frequency set to {stats_update_frequency} seconds"
) )
@ -672,7 +675,6 @@ class TasksManager:
host_stats = HelpersManagement.get_latest_hosts_stats() host_stats = HelpersManagement.get_latest_hosts_stats()
while True: while True:
if host_stats.get( if host_stats.get(
"cpu_usage" "cpu_usage"
) != HelpersManagement.get_latest_hosts_stats().get( ) != HelpersManagement.get_latest_hosts_stats().get(
@ -687,18 +689,37 @@ class TasksManager:
host_stats = HelpersManagement.get_latest_hosts_stats() host_stats = HelpersManagement.get_latest_hosts_stats()
if len(self.helper.websocket_helper.clients) > 0: if len(self.helper.websocket_helper.clients) > 0:
# There are clients # There are clients
self.helper.websocket_helper.broadcast_page( try:
"/panel/dashboard", self.helper.websocket_helper.broadcast_page(
"update_host_stats", "/panel/dashboard",
{ "update_host_stats",
"cpu_usage": host_stats.get("cpu_usage"), {
"cpu_cores": host_stats.get("cpu_cores"), "cpu_usage": host_stats.get("cpu_usage"),
"cpu_cur_freq": host_stats.get("cpu_cur_freq"), "cpu_cores": host_stats.get("cpu_cores"),
"cpu_max_freq": host_stats.get("cpu_max_freq"), "cpu_cur_freq": host_stats.get("cpu_cur_freq"),
"mem_percent": host_stats.get("mem_percent"), "cpu_max_freq": host_stats.get("cpu_max_freq"),
"mem_usage": host_stats.get("mem_usage"), "mem_percent": host_stats.get("mem_percent"),
}, "mem_usage": host_stats.get("mem_usage"),
) "disk_usage": json.loads(
host_stats.get("disk_json").replace("'", '"')
),
"mounts": self.helper.get_setting("monitored_mounts"),
},
)
except:
self.helper.websocket_helper.broadcast_page(
"/panel/dashboard",
"update_host_stats",
{
"cpu_usage": host_stats.get("cpu_usage"),
"cpu_cores": host_stats.get("cpu_cores"),
"cpu_cur_freq": host_stats.get("cpu_cur_freq"),
"cpu_max_freq": host_stats.get("cpu_max_freq"),
"mem_percent": host_stats.get("mem_percent"),
"mem_usage": host_stats.get("mem_usage"),
"disk_usage": {},
},
)
time.sleep(1) time.sleep(1)
def check_for_updates(self): def check_for_updates(self):
@ -730,10 +751,42 @@ class TasksManager:
logger.debug("Could not clear out file from import directory") logger.debug("Could not clear out file from import directory")
def log_watcher(self): def log_watcher(self):
self.controller.servers.check_for_old_logs() self.check_for_old_logs()
self.scheduler.add_job( self.scheduler.add_job(
self.controller.servers.check_for_old_logs, self.check_for_old_logs,
"interval", "interval",
hours=6, hours=6,
id="log-mgmt", id="log-mgmt",
) )
def check_for_old_logs(self):
# check for server logs first
self.controller.servers.check_for_old_logs()
# check for crafty logs now
logs_path = os.path.join(self.controller.project_root, "logs")
logs_delete_after = int(
self.helper.get_setting("crafty_logs_delete_after_days")
)
latest_log_files = [
"session.log",
"schedule.log",
"tornado-access.log",
"session.log",
"commander.log",
]
# we won't delete if delete logs after is set to 0
if logs_delete_after != 0:
log_files = list(
filter(
lambda val: val not in latest_log_files,
os.listdir(logs_path),
)
)
for log_file in log_files:
log_file_path = os.path.join(logs_path, log_file)
if Helpers.check_file_exists(
log_file_path
) and Helpers.is_file_older_than_x_days(
log_file_path, logs_delete_after
):
os.remove(log_file_path)

View File

@ -575,6 +575,33 @@ class AjaxHandler(BaseHandler):
self.controller.server_jars.manual_refresh_cache() self.controller.server_jars.manual_refresh_cache()
return return
elif page == "update_server_dir":
if self.helper.dir_migration:
return
for server in self.controller.servers.get_all_servers_stats():
if server["stats"]["running"]:
self.helper.websocket_helper.broadcast_user(
exec_user["user_id"],
"send_start_error",
{
"error": "You must stop all servers before "
"starting a storage migration."
},
)
return
if not superuser:
self.redirect("/panel/error?error=Not a super user")
return
if self.helper.is_env_docker():
self.redirect(
"/panel/error?error=This feature is not"
" supported on docker environments"
)
return
new_dir = urllib.parse.unquote(self.get_argument("server_dir"))
self.controller.update_master_server_dir(new_dir, exec_user["user_id"])
return
@tornado.web.authenticated @tornado.web.authenticated
def delete(self, page): def delete(self, page):
api_key, _, exec_user = self.current_user api_key, _, exec_user = self.current_user

View File

@ -6,7 +6,6 @@ logger = logging.getLogger(__name__)
class DefaultHandler(BaseHandler): class DefaultHandler(BaseHandler):
# Override prepare() instead of get() to cover all possible HTTP methods. # Override prepare() instead of get() to cover all possible HTTP methods.
def prepare(self, page=None): # pylint: disable=arguments-differ def prepare(self, page=None): # pylint: disable=arguments-differ
if page is not None: if page is not None:

View File

@ -290,9 +290,11 @@ class PanelHandler(BaseHandler):
page_data: t.Dict[str, t.Any] = { page_data: t.Dict[str, t.Any] = {
# todo: make this actually pull and compare version data # todo: make this actually pull and compare version data
"update_available": self.helper.update_available, "update_available": self.helper.update_available,
"docker": self.helper.is_env_docker(),
"background": self.controller.cached_login, "background": self.controller.cached_login,
"login_opacity": self.controller.management.get_login_opacity(), "login_opacity": self.controller.management.get_login_opacity(),
"serverTZ": tz, "serverTZ": tz,
"monitored": self.helper.get_setting("monitored_mounts"),
"version_data": self.helper.get_version_string(), "version_data": self.helper.get_version_string(),
"failed_servers": self.controller.servers.failed_servers, "failed_servers": self.controller.servers.failed_servers,
"user_data": exec_user, "user_data": exec_user,
@ -332,7 +334,12 @@ class PanelHandler(BaseHandler):
else None, else None,
"superuser": superuser, "superuser": superuser,
} }
try:
page_data["hosts_data"]["disk_json"] = json.loads(
page_data["hosts_data"]["disk_json"].replace("'", '"')
)
except:
page_data["hosts_data"]["disk_json"] = {}
if page == "unauthorized": if page == "unauthorized":
template = "panel/denied.html" template = "panel/denied.html"
@ -532,6 +539,7 @@ class PanelHandler(BaseHandler):
"auto_start": server_temp_obj["auto_start"], "auto_start": server_temp_obj["auto_start"],
"crash_detection": server_temp_obj["crash_detection"], "crash_detection": server_temp_obj["crash_detection"],
"show_status": server_temp_obj["show_status"], "show_status": server_temp_obj["show_status"],
"ignored_exits": server_temp_obj["ignored_exits"],
}, },
"running": False, "running": False,
"crashed": False, "crashed": False,
@ -841,6 +849,9 @@ class PanelHandler(BaseHandler):
page_data["auth-servers"] = auth_servers page_data["auth-servers"] = auth_servers
page_data["role-servers"] = auth_role_servers page_data["role-servers"] = auth_role_servers
page_data["user-roles"] = user_roles page_data["user-roles"] = user_roles
page_data["servers_dir"], _tail = os.path.split(
self.controller.management.get_master_server_dir()
)
page_data["users"] = self.controller.users.user_query(exec_user["user_id"]) page_data["users"] = self.controller.users.user_query(exec_user["user_id"])
page_data["roles"] = self.controller.users.user_role_query( page_data["roles"] = self.controller.users.user_role_query(
@ -880,6 +891,7 @@ class PanelHandler(BaseHandler):
page_data["config-json"] = data page_data["config-json"] = data
page_data["availables_languages"] = [] page_data["availables_languages"] = []
page_data["all_languages"] = [] page_data["all_languages"] = []
page_data["all_partitions"] = self.helper.get_all_mounts()
for file in sorted( for file in sorted(
os.listdir( os.listdir(
@ -1585,6 +1597,8 @@ class PanelHandler(BaseHandler):
crash_detection = int(float(self.get_argument("crash_detection", "0"))) crash_detection = int(float(self.get_argument("crash_detection", "0")))
logs_delete_after = int(float(self.get_argument("logs_delete_after", "0"))) logs_delete_after = int(float(self.get_argument("logs_delete_after", "0")))
java_selection = self.get_argument("java_selection", None) java_selection = self.get_argument("java_selection", None)
# make sure there is no whitespace
ignored_exits = self.get_argument("ignored_exits", "").replace(" ", "")
# subpage = self.get_argument('subpage', None) # subpage = self.get_argument('subpage', None)
server_id = self.check_server_id() server_id = self.check_server_id()
@ -1669,6 +1683,7 @@ class PanelHandler(BaseHandler):
server_obj.auto_start = auto_start server_obj.auto_start = auto_start
server_obj.crash_detection = crash_detection server_obj.crash_detection = crash_detection
server_obj.logs_delete_after = logs_delete_after server_obj.logs_delete_after = logs_delete_after
server_obj.ignored_exits = ignored_exits
failed = False failed = False
for servers in self.controller.servers.failed_servers: for servers in self.controller.servers.failed_servers:
if servers["server_id"] == int(server_id): if servers["server_id"] == int(server_id):

View File

@ -10,7 +10,6 @@ logger = logging.getLogger(__name__)
class PublicHandler(BaseHandler): class PublicHandler(BaseHandler):
def set_current_user(self, user_id: str = None): def set_current_user(self, user_id: str = None):
expire_days = self.helper.get_setting("cookie_expire") expire_days = self.helper.get_setting("cookie_expire")
# if helper comes back with false # if helper comes back with false
@ -29,7 +28,6 @@ class PublicHandler(BaseHandler):
# self.clear_cookie("user_data") # self.clear_cookie("user_data")
def get(self, page=None): def get(self, page=None):
error = bleach.clean(self.get_argument("error", "Invalid Login!")) error = bleach.clean(self.get_argument("error", "Invalid Login!"))
error_msg = bleach.clean(self.get_argument("error_msg", "")) error_msg = bleach.clean(self.get_argument("error_msg", ""))
@ -81,7 +79,6 @@ class PublicHandler(BaseHandler):
) )
def post(self, page=None): def post(self, page=None):
error = bleach.clean(self.get_argument("error", "Invalid Login!")) error = bleach.clean(self.get_argument("error", "Invalid Login!"))
error_msg = bleach.clean(self.get_argument("error_msg", "")) error_msg = bleach.clean(self.get_argument("error_msg", ""))
@ -96,7 +93,6 @@ class PublicHandler(BaseHandler):
page_data["query"] = self.request.query page_data["query"] = self.request.query
if page == "login": if page == "login":
next_page = "/login" next_page = "/login"
if self.request.query: if self.request.query:
next_page = "/login?" + self.request.query next_page = "/login?" + self.request.query

View File

@ -26,7 +26,6 @@ login_schema = {
class ApiAuthLoginHandler(BaseApiHandler): class ApiAuthLoginHandler(BaseApiHandler):
def post(self): def post(self):
try: try:
data = json.loads(self.request.body) data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:

View File

@ -631,7 +631,6 @@ class ApiServersIndexHandler(BaseApiHandler):
self.finish_json(200, {"status": "ok", "data": auth_data[0]}) self.finish_json(200, {"status": "ok", "data": auth_data[0]})
def post(self): def post(self):
auth_data = self.authenticate_user() auth_data = self.authenticate_user()
if not auth_data: if not auth_data:
return return

View File

@ -183,6 +183,7 @@ class ServerHandler(BaseHandler):
"version_data": "version_data_here", # TODO "version_data": "version_data_here", # TODO
"user_data": exec_user, "user_data": exec_user,
"show_contribute": self.helper.get_setting("show_contribute_link", True), "show_contribute": self.helper.get_setting("show_contribute_link", True),
"background": self.controller.cached_login,
"lang": self.controller.users.get_user_lang_by_id(exec_user["user_id"]), "lang": self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
"lang_page": Helpers.get_lang_page( "lang_page": Helpers.get_lang_page(
self.controller.users.get_user_lang_by_id(exec_user["user_id"]) self.controller.users.get_user_lang_by_id(exec_user["user_id"])
@ -403,6 +404,14 @@ class ServerHandler(BaseHandler):
jar_type, server_type, server_version = server_parts jar_type, server_type, server_version = server_parts
# TODO: add server type check here and call the correct server # TODO: add server type check here and call the correct server
# add functions if not a jar # add functions if not a jar
if server_type == "forge" and not self.helper.detect_java():
translation = self.helper.translation.translate(
"error",
"installerJava",
self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
).format(server_name)
self.redirect(f"/panel/error?error={translation}")
return
new_server_id = self.controller.create_jar_server( new_server_id = self.controller.create_jar_server(
jar_type, jar_type,
server_type, server_type,
@ -551,7 +560,6 @@ class ServerHandler(BaseHandler):
self.get_remote_ip(), self.get_remote_ip(),
) )
else: else:
new_server_id = self.controller.create_bedrock_server( new_server_id = self.controller.create_bedrock_server(
server_name, server_name,
exec_user["user_id"], exec_user["user_id"],

View File

@ -7,12 +7,12 @@ logger = logging.getLogger(__name__)
class StatusHandler(BaseHandler): class StatusHandler(BaseHandler):
def get(self): def get(self):
page_data = {"background": self.controller.cached_login} page_data = {
page_data["lang"] = self.helper.get_setting("language") "background": self.controller.cached_login,
page_data["lang_page"] = self.helper.get_lang_page( "lang": self.helper.get_setting("language"),
self.helper.get_setting("language") "lang_page": self.helper.get_lang_page(self.helper.get_setting("language")),
) "servers": self.controller.servers.get_all_servers_stats(),
page_data["servers"] = self.controller.servers.get_all_servers_stats() }
running = 0 running = 0
for srv in page_data["servers"]: for srv in page_data["servers"]:
if srv["stats"]["running"]: if srv["stats"]["running"]:

View File

@ -59,7 +59,6 @@ class Webserver:
@staticmethod @staticmethod
def log_function(handler): def log_function(handler):
info = { info = {
"Status_Code": handler.get_status(), "Status_Code": handler.get_status(),
"Method": handler.request.method, "Method": handler.request.method,
@ -103,7 +102,6 @@ class Webserver:
logger.debug("Applied asyncio patch") logger.debug("Applied asyncio patch")
def run_tornado(self): def run_tornado(self):
# let's verify we have an SSL cert # let's verify we have an SSL cert
self.helper.create_self_signed_cert() self.helper.create_self_signed_cert()

View File

@ -18,7 +18,6 @@ logger = logging.getLogger(__name__)
@tornado.web.stream_request_body @tornado.web.stream_request_body
class UploadHandler(BaseHandler): class UploadHandler(BaseHandler):
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
def initialize( def initialize(
self, self,
@ -52,18 +51,19 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if superuser: if superuser:
@ -141,48 +141,49 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if not superuser: if not superuser:
self.helper.websocket_helper.broadcast_user( return self.finish_json(
user_id, 401,
"send_start_error",
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "UNAUTHORIZED ACCESS",
"info": self.helper.translation.translate(
"error", "error",
"superError", "superError",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
if not self.request.headers.get("X-Content-Type", None).startswith( if not self.request.headers.get("X-Content-Type", None).startswith(
"image/" "image/"
): ):
self.helper.websocket_helper.broadcast_user( return self.finish_json(
user_id, 415,
"send_start_error",
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TYPE ERROR",
"info": self.helper.translation.translate(
"error", "error",
"fileError", "fileError",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
if user_id is None: if user_id is None:
logger.warning("User ID not found in upload handler call") logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call") Console.warning("User ID not found in upload handler call")
@ -219,18 +220,19 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if superuser: if superuser:

View File

@ -79,7 +79,6 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
# pylint: disable=arguments-renamed # pylint: disable=arguments-renamed
@staticmethod @staticmethod
def on_message(raw_message): def on_message(raw_message):
logger.debug(f"Got message from WebSocket connection {raw_message}") logger.debug(f"Got message from WebSocket connection {raw_message}")
message = json.loads(raw_message) message = json.loads(raw_message)
logger.debug(f"Event Type: {message['event']}, Data: {message['data']}") logger.debug(f"Event Type: {message['event']}, Data: {message['data']}")

View File

@ -1,5 +1,5 @@
{ {
"major": 4, "major": 4,
"minor": 0, "minor": 0,
"sub": 20 "sub": 21
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,15 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face {
font-family: 'Font Awesome 5 Brands';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-brands-400.eot");
src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
.fab {
font-family: 'Font Awesome 5 Brands';
font-weight: 400; }

View File

@ -1,5 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,15 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face {
font-family: 'Font Awesome 5 Pro';
font-style: normal;
font-weight: 300;
font-display: block;
src: url("../webfonts/fa-light-300.eot");
src: url("../webfonts/fa-light-300.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-light-300.woff2") format("woff2"), url("../webfonts/fa-light-300.woff") format("woff"), url("../webfonts/fa-light-300.ttf") format("truetype"), url("../webfonts/fa-light-300.svg#fontawesome") format("svg"); }
.fal {
font-family: 'Font Awesome 5 Pro';
font-weight: 300; }

View File

@ -1,5 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face{font-family:"Font Awesome 5 Pro";font-style:normal;font-weight:300;font-display:block;src:url(../webfonts/fa-light-300.eot);src:url(../webfonts/fa-light-300.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-light-300.woff2) format("woff2"),url(../webfonts/fa-light-300.woff) format("woff"),url(../webfonts/fa-light-300.ttf) format("truetype"),url(../webfonts/fa-light-300.svg#fontawesome) format("svg")}.fal{font-family:"Font Awesome 5 Pro";font-weight:300}

View File

@ -1,15 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face {
font-family: 'Font Awesome 5 Pro';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.eot");
src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
.far {
font-family: 'Font Awesome 5 Pro';
font-weight: 400; }

View File

@ -1,5 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face{font-family:"Font Awesome 5 Pro";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Pro";font-weight:400}

View File

@ -1,16 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face {
font-family: 'Font Awesome 5 Pro';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.eot");
src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
.fa,
.fas {
font-family: 'Font Awesome 5 Pro';
font-weight: 900; }

View File

@ -1,5 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
@font-face{font-family:"Font Awesome 5 Pro";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Pro";font-weight:900}

View File

@ -1,492 +0,0 @@
/*!
* Font Awesome Pro 5.14.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Commercial License)
*/
svg:not(:root).svg-inline--fa {
overflow: visible;
}
.svg-inline--fa {
display: inline-block;
font-size: inherit;
height: 1em;
overflow: visible;
vertical-align: -.125em;
}
.svg-inline--fa.fa-lg {
vertical-align: -.225em;
}
.svg-inline--fa.fa-w-1 {
width: 0.0625em;
}
.svg-inline--fa.fa-w-2 {
width: 0.125em;
}
.svg-inline--fa.fa-w-3 {
width: 0.1875em;
}
.svg-inline--fa.fa-w-4 {
width: 0.25em;
}
.svg-inline--fa.fa-w-5 {
width: 0.3125em;
}
.svg-inline--fa.fa-w-6 {
width: 0.375em;
}
.svg-inline--fa.fa-w-7 {
width: 0.4375em;
}
.svg-inline--fa.fa-w-8 {
width: 0.5em;
}
.svg-inline--fa.fa-w-9 {
width: 0.5625em;
}
.svg-inline--fa.fa-w-10 {
width: 0.625em;
}
.svg-inline--fa.fa-w-11 {
width: 0.6875em;
}
.svg-inline--fa.fa-w-12 {
width: 0.75em;
}
.svg-inline--fa.fa-w-13 {
width: 0.8125em;
}
.svg-inline--fa.fa-w-14 {
width: 0.875em;
}
.svg-inline--fa.fa-w-15 {
width: 0.9375em;
}
.svg-inline--fa.fa-w-16 {
width: 1em;
}
.svg-inline--fa.fa-w-17 {
width: 1.0625em;
}
.svg-inline--fa.fa-w-18 {
width: 1.125em;
}
.svg-inline--fa.fa-w-19 {
width: 1.1875em;
}
.svg-inline--fa.fa-w-20 {
width: 1.25em;
}
.svg-inline--fa.fa-pull-left {
margin-right: .3em;
width: auto;
}
.svg-inline--fa.fa-pull-right {
margin-left: .3em;
width: auto;
}
.svg-inline--fa.fa-border {
height: 1.5em;
}
.svg-inline--fa.fa-li {
width: 2em;
}
.svg-inline--fa.fa-fw {
width: 1.25em;
}
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
}
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em;
}
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center;
}
.fa-layers-text,
.fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center;
}
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center;
}
.fa-layers-counter {
background-color: #ff253a;
border-radius: 1em;
-webkit-box-sizing: border-box;
box-sizing: border-box;
var(--base-text);
height: 1.5em;
line-height: 1;
max-width: 5em;
min-width: 1.5em;
overflow: hidden;
padding: .25em;
right: 0;
text-overflow: ellipsis;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top right;
transform-origin: top right;
}
.fa-layers-bottom-right {
bottom: 0;
right: 0;
top: auto;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: bottom right;
transform-origin: bottom right;
}
.fa-layers-bottom-left {
bottom: 0;
left: 0;
right: auto;
top: auto;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: bottom left;
transform-origin: bottom left;
}
.fa-layers-top-right {
right: 0;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top right;
transform-origin: top right;
}
.fa-layers-top-left {
left: 0;
right: auto;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top left;
transform-origin: top left;
}
.fa-lg {
font-size: 1.33333em;
line-height: 0.75em;
vertical-align: -.0667em;
}
.fa-xs {
font-size: .75em;
}
.fa-sm {
font-size: .875em;
}
.fa-1x {
font-size: 1em;
}
.fa-2x {
font-size: 2em;
}
.fa-3x {
font-size: 3em;
}
.fa-4x {
font-size: 4em;
}
.fa-5x {
font-size: 5em;
}
.fa-6x {
font-size: 6em;
}
.fa-7x {
font-size: 7em;
}
.fa-8x {
font-size: 8em;
}
.fa-9x {
font-size: 9em;
}
.fa-10x {
font-size: 10em;
}
.fa-fw {
text-align: center;
width: 1.25em;
}
.fa-ul {
list-style-type: none;
margin-left: 2.5em;
padding-left: 0;
}
.fa-ul>li {
position: relative;
}
.fa-li {
left: -2em;
position: absolute;
text-align: center;
width: 2em;
line-height: inherit;
}
.fa-border {
border: solid 0.08em #eee;
border-radius: .1em;
padding: .2em .25em .15em;
}
.fa-pull-left {
float: left;
}
.fa-pull-right {
float: right;
}
.fa.fa-pull-left,
.fas.fa-pull-left,
.far.fa-pull-left,
.fal.fa-pull-left,
.fab.fa-pull-left {
margin-right: .3em;
}
.fa.fa-pull-right,
.fas.fa-pull-right,
.far.fa-pull-right,
.fal.fa-pull-right,
.fab.fa-pull-right {
margin-left: .3em;
}
.fa-spin {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear;
}
.fa-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.fa-rotate-90 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.fa-rotate-180 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.fa-rotate-270 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
}
.fa-flip-horizontal {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1);
}
.fa-flip-vertical {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(1, -1);
transform: scale(1, -1);
}
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1);
}
:root .fa-rotate-90,
:root .fa-rotate-180,
:root .fa-rotate-270,
:root .fa-flip-horizontal,
:root .fa-flip-vertical,
:root .fa-flip-both {
-webkit-filter: none;
filter: none;
}
.fa-stack {
display: inline-block;
height: 2em;
position: relative;
width: 2.5em;
}
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
}
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em;
}
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em;
}
.fa-inverse {
var(--base-text);
}
.sr-only {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
width: auto;
}
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: 1;
opacity: var(--fa-primary-opacity, 1);
}
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: 0.4;
opacity: var(--fa-secondary-opacity, 0.4);
}
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: 0.4;
opacity: var(--fa-secondary-opacity, 0.4);
}
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: 1;
opacity: var(--fa-primary-opacity, 1);
}
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black;
}
.fad.fa-inverse {
var(--base-text);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 713 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 2.5 MiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 2.3 MiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.7 MiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
.far,
.fa-regular {
font-weight: 400; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
.fas,
.fa-solid {
font-weight: 900; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}

View File

@ -0,0 +1,635 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Solid';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Regular';
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Light';
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Thin';
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
overflow: visible;
box-sizing: content-box; }
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-2xs {
vertical-align: 0.1em; }
.svg-inline--fa.fa-xs {
vertical-align: 0em; }
.svg-inline--fa.fa-sm {
vertical-align: -0.07143em; }
.svg-inline--fa.fa-lg {
vertical-align: -0.2em; }
.svg-inline--fa.fa-xl {
vertical-align: -0.25em; }
.svg-inline--fa.fa-2xl {
vertical-align: -0.3125em; }
.svg-inline--fa.fa-pull-left {
margin-right: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-li {
width: var(--fa-li-width, 2em);
top: 0.25em; }
.svg-inline--fa.fa-fw {
width: var(--fa-fw-width, 1.25em); }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers-text, .fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-counter {
background-color: var(--fa-counter-background-color, #ff253a);
border-radius: var(--fa-counter-border-radius, 1em);
box-sizing: border-box;
color: var(--fa-inverse, #fff);
line-height: var(--fa-counter-line-height, 1);
max-width: var(--fa-counter-max-width, 5em);
min-width: var(--fa-counter-min-width, 1.5em);
overflow: hidden;
padding: var(--fa-counter-padding, 0.25em 0.5em);
right: var(--fa-right, 0);
text-overflow: ellipsis;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-counter-scale, 0.25));
transform: scale(var(--fa-counter-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: var(--fa-bottom, 0);
right: var(--fa-right, 0);
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom right;
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: var(--fa-bottom, 0);
left: var(--fa-left, 0);
right: auto;
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom left;
transform-origin: bottom left; }
.fa-layers-top-right {
top: var(--fa-top, 0);
right: var(--fa-right, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-top-left {
left: var(--fa-left, 0);
right: auto;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top left;
transform-origin: top left; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-2xs {
font-size: 0.625em;
line-height: 0.1em;
vertical-align: 0.225em; }
.fa-xs {
font-size: 0.75em;
line-height: 0.08333em;
vertical-align: 0.125em; }
.fa-sm {
font-size: 0.875em;
line-height: 0.07143em;
vertical-align: 0.05357em; }
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em; }
.fa-xl {
font-size: 1.5em;
line-height: 0.04167em;
vertical-align: -0.125em; }
.fa-2xl {
font-size: 2em;
line-height: 0.03125em;
vertical-align: -0.1875em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: var(--fa-li-margin, 2.5em);
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: calc(var(--fa-li-width, 2em) * -1);
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
line-height: inherit; }
.fa-border {
border-color: var(--fa-border-color, #eee);
border-radius: var(--fa-border-radius, 0.1em);
border-style: var(--fa-border-style, solid);
border-width: var(--fa-border-width, 0.08em);
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
.fa-pull-left {
float: left;
margin-right: var(--fa-pull-margin, 0.3em); }
.fa-pull-right {
float: right;
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
-webkit-animation-name: fa-beat;
animation-name: fa-beat;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
-webkit-animation-name: fa-bounce;
animation-name: fa-bounce;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
-webkit-animation-name: fa-fade;
animation-name: fa-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
-webkit-animation-name: fa-beat-fade;
animation-name: fa-beat-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
-webkit-animation-name: fa-flip;
animation-name: fa-flip;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
-webkit-animation-name: fa-shake;
animation-name: fa-shake;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 2s);
animation-duration: var(--fa-animation-duration, 2s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
--fa-animation-direction: reverse; }
.fa-pulse,
.fa-spin-pulse {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
.fa-beat,
.fa-bounce,
.fa-fade,
.fa-beat-fade,
.fa-flip,
.fa-pulse,
.fa-shake,
.fa-spin,
.fa-spin-pulse {
-webkit-animation-delay: -1ms;
animation-delay: -1ms;
-webkit-animation-duration: 1ms;
animation-duration: 1ms;
-webkit-animation-iteration-count: 1;
animation-iteration-count: 1;
transition-delay: 0s;
transition-duration: 0s; } }
@-webkit-keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@-webkit-keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@-webkit-keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@-webkit-keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@-webkit-keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@-webkit-keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
.fa-rotate-90 {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.fa-rotate-180 {
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
.fa-rotate-270 {
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
.fa-flip-horizontal {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
.fa-flip-vertical {
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
.fa-rotate-by {
-webkit-transform: rotate(var(--fa-rotate-angle, none));
transform: rotate(var(--fa-rotate-angle, none)); }
.fa-stack {
display: inline-block;
vertical-align: middle;
height: 2em;
position: relative;
width: 2.5em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
z-index: var(--fa-stack-z-index, auto); }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
.fa-inverse {
color: var(--fa-inverse, #fff); }
.sr-only,
.fa-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.sr-only-focusable:not(:focus),
.fa-sr-only-focusable:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
.fad.fa-inverse,
.fa-duotone.fa-inverse {
color: var(--fa-inverse, #fff); }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
@font-face {
font-family: 'Font Awesome 5 Brands';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 900;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}

Some files were not shown because too many files have changed in this diff Show More