Merge branch 'dev' into tweak/file-enhancements

This commit is contained in:
Zedifus 2022-06-21 20:41:09 +01:00
commit 610b94dd59
36 changed files with 1267 additions and 630 deletions

View File

@ -21,7 +21,7 @@ win-dev-build:
- pyinstaller -F main.py
--distpath .
--icon app\frontend\static\assets\images\Crafty_4-0_Logo_square.ico
--name "crafty_commander"
--name "crafty"
--paths .venv\Lib\site-packages
--hidden-import cryptography
--hidden-import cffi
@ -37,7 +37,7 @@ win-dev-build:
name: "crafty-${CI_RUNNER_TAGS}-${CI_COMMIT_BRANCH}_${CI_COMMIT_SHORT_SHA}"
paths:
- app\
- .\crafty_commander.exe
- .\crafty.exe
exclude:
- app\classes\**\*
@ -63,7 +63,7 @@ win-prod-build:
- pyinstaller -F main.py
--distpath .
--icon app\frontend\static\assets\images\Crafty_4-0_Logo_square.ico
--name "crafty_commander"
--name "crafty"
--paths .venv\Lib\site-packages
--hidden-import cryptography
--hidden-import cffi
@ -81,7 +81,7 @@ win-prod-build:
name: "crafty-${CI_RUNNER_TAGS}-${CI_COMMIT_BRANCH}_${CI_COMMIT_SHORT_SHA}"
paths:
- app\
- .\crafty_commander.exe
- .\crafty.exe
expire_in: never
exclude:
- app\classes\**\*

View File

@ -232,6 +232,8 @@ function-naming-style=snake_case
good-names=e,
ex,
f,
fd,
fn,
i,
id,
ip,

View File

@ -1,43 +1,51 @@
# Changelog
## [4.0.3] - TBD
## --- [4.0.4] - 2022/06/21
### New features
TBD
- Add shutdown on backup feature ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/373))
- Add detection and dropdown of java versions ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/375))
### Bug fixes
TBD
- Backup/Config.json rework for API key hardening ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/369))
- Fix stack on ping result being falsy ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/371))
- Fix sec bug with server creation roles ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/376))
### Tweaks
- Use 4 space indentation for the session file.
- Use with-blocks when opening files to prevent them from being left open
- Spelling mistake fixed in German lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/370))
- Backup failure warning (Tab text goes red) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/373))
- Rework server list on dashboard display for use on small screens ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/372))
- File handling enhancements ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/362))
<br><br>
## [4.0.2] - 2022/06/16
## --- [4.0.3] - 2022/06/18
### New features
- Integrate Wiki iframe into panel instead of link ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/367))
### Bug fixes
- Amend Java system variable fix to be more specfic since they only affect Oracle. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/364))
- API Token authentication hardening ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/364))
### Tweaks
- Add better error logging for statistic collection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/359))
<br><br>
## --- [4.0.2-hotfix1] - 2022/06/17
### Crit Bug fixes
- Fix blank server_detail page for general users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/358))
<br><br>
## --- [4.0.2] - 2022/06/16
### New features
None
### Bug fixes
- Fix winreg import pass on non-NT systems ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/344))
- Make the WebSocket automatically reconnect. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/345))
- Fix an error when there are no servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/346))
- Use relative paths for the jarfile and logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/347))
- Flatten all instances of username creation or editing, usernames should be lower case.
- - ([Merge Request 1](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/342))
- - ([Merge Request 2](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/351))
- Add version inheretence & config check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/353))
- Fix support log temp file deletion issue/hang ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/354))
<br><br>
## [4.0.1] - 2022/06/15
## --- [4.0.1] - 2022/06/15
### New features
None
### Bug fixes
- Remove session.lock warning ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/338))
- Correct Dutch Spacing Issue ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/340))
- Remove no-else-* pylint exemptions and tidy code. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/342))
- Make unRAID more readable, and flatten path to lower, to fit standard practice. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/337))
- Fix Java Pathing issues on windows ([Commit](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/343/diffs?commit_id=cda2120579083d447db5dbeb5489822880f4cae7))

68
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,68 @@
# Crafty 4 - A contributors guide.
*Don't Panic!*<br><br>
First off, thank you for choosing Crafty Controller! <br>
We hope you've been enjoying the beta so far and are absolutely thrilled that you are looking to contribute!
The following guide will show you how to easily and safely contribute to our current workflow. There are a few components that need to be taken into account and processes that need followed before we can merge your code into our repository.
<br><br>
## Getting started
There are two incredibly helpful ways of contributing to the project: `Issues` and `Changes`.
### Issues
#### Create a new issue
If you spot a problem with crafty, [search if an issue already exists](https://gitlab.com/crafty-controller/crafty-4/-/issues). If a related issue doesn't exist, you can [open a new issue](https://gitlab.com/crafty-controller/crafty-4/-/issues/new) using a relevant issue template:
- Bug - For any bugs you may find in Crafty.
- Feature Request - For any features you'd like to see in Crafty.
- Change Request - For any changes you'd like to see to existing Crafty functions.
#### Solve an issue
If you're feeling inclined and want to help us with our workload you can have a look through our [existing issues](https://gitlab.com/crafty-controller/crafty-4/-/issues) to find one that interests you. (You can narrow down the search using `labels` as filters.)
### Make changes
1. [Install Git](https://docs.gitlab.com/ee/topics/git/how_to_install_git/).
2. Fork the repository.
3. Create a branch from `dev` with a suitable name that matches our folder branch flow<br> `bugfix/` `tweak/` `lang/` `feature/`<br>
For Example:<br>
`tweak/websocket-auto-reconnect`<br>
`bugfix/blank-page-as-non-superuser`<br>
`lang/german-spelling-correction`<br>
`feature/support-log-downloader`
4. Make your changes!
5. Make sure your code is formatted correctly ([We use Black](https://black.readthedocs.io/en/stable/getting_started.html)).
> 🧑‍🎓 If you are using **VSCODE** you can follow this [handy dandy tiny guide](https://marcobelo.medium.com/setting-up-python-black-on-visual-studio-code-5318eba4cd00) on how to setup formatting on save.<br>
This will allow you to write your code without having to think about ⬛**black**, and then when you press `ctrl+s` black will immediately format your code!
### Commit your update
Commit your changes once you are happy with them. See [Chris Beam's guide](https://chris.beams.io/posts/git-commit/) on how to write a suitable commit message. This will be enforced. If your commit messages don't meet suitable standards, your merge request will not be merged.
- Please make sure and test the area that you have been working in, this makes our reviewers' lives easier!
### Create a merge request
Once you are all done making your changes make a MR (merge request) into our `dev` branch.
- Fill in the merge request template. This template helps reviewers understand your changes as well as the purpose of your merge request. Make sure to include details!
- If you are solving an issue don't forget to link the MR to that issue.
- Make sure to [allow upstream commits](https://docs.gitlab.com/ee/user/project/merge_requests/allow_collaboration.html#allow-commits-from-upstream-members) so we can prepare the branch for merge if it's not quite right. <br> A member of the maintainer team will review your proposal. We may also ask questions or request additional information at this stage. On some occasions we may reject your proposal, please don't be disheartened if we do so. Even if your code does not make it into the repo we appreciate your time and effort spent on creating the MR.
- Please make sure your merge request complies with the pylint's Code Climate report on your MR and fix any issues that are raised.
- We may ask for changes to be made before a MR can be merged, either using [suggested changes](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html), inline comments or merge request threads. You can apply suggested changes directly through GitLab's UI. You can also make any other changes in your fork, then commit them to your branch before the merge request is processed.
- As you update your MR with changes we request, mark each thread as [resolved](https://docs.gitlab.com/ee/user/discussions/#resolve-a-thread).
- If you run into any merge issues checkout this [git tutorial](https://about.gitlab.com/blog/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) to help you resolve them. (If you get stuck your reviewer can help you.)
### Your MR is merged!
Congratulations 🎉 You've successfully made a contribution to Crafty!

View File

@ -2,11 +2,11 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Supported Python Versions](https://shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20-blue)](https://www.python.org)
[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.2--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.4--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
[![Code Quality(temp-hardcoded)](https://img.shields.io/badge/code%20quality-10-brightgreen)](https://gitlab.com/crafty-controller/crafty-4)
[![Build Status](https://gitlab.com/crafty-controller/crafty-4/badges/master/pipeline.svg)](https://gitlab.com/crafty-controller/crafty-4/-/commits/master)
# Crafty Controller 4.0.2-beta
# Crafty Controller 4.0.4-beta
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
@ -39,7 +39,7 @@ With `Crafty Controller 4.0` we have focused on building our DevOps Principles,
> __**⚠ 🔻WARNING: [WSL/WSL2 | WINDOWS 11 | DOCKER DESKTOP]🔻**__ <br>
BE ADVISED! Upstream is currently broken for Minecraft running on **Docker under WSL/WSL2, Windows 11 / DOCKER DESKTOP!** <br>
On '**Stop**' or '**Restart**' of the MC Server, there is a 90% chance the World's Chunks will be shredded irreparably! <br>
Please only run Docker on Linux, If you are using Windows we have a portable installs found here: [Latest-Stable](https://gitlab.com/crafty-controller/crafty-4/-/jobs/artifacts/master/download?job=win-prod-build), [Latest-Development](https://gitlab.com/crafty-controller/crafty-4/-/jobs/artifacts/dev/download?job=win-dev-build)
Please only run Docker on Linux, If you are using Windows we have a portable installs found here: [Latest-Stable](https://gitlab.com/crafty-controller/crafty-4/-/releases), [Latest-Development](https://gitlab.com/crafty-controller/crafty-4/-/jobs/artifacts/dev/download?job=win-dev-build)
----

View File

@ -17,6 +17,14 @@ class ManagementController:
def get_latest_hosts_stats():
return HelpersManagement.get_latest_hosts_stats()
@staticmethod
def set_crafty_api_key(key):
HelpersManagement.set_secret_api_key(key)
@staticmethod
def get_crafty_api_key():
return HelpersManagement.get_secret_api_key()
# **********************************************************************************
# Commands Methods
# **********************************************************************************
@ -128,9 +136,10 @@ class ManagementController:
max_backups: int = None,
excluded_dirs: list = None,
compress: bool = False,
shutdown: bool = False,
):
return self.management_helper.set_backup_config(
server_id, backup_path, max_backups, excluded_dirs, compress
server_id, backup_path, max_backups, excluded_dirs, compress, shutdown
)
@staticmethod

View File

@ -5,6 +5,7 @@ import json
import typing as t
from app.classes.controllers.roles_controller import RolesController
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.singleton import Singleton
from app.classes.shared.server import ServerInstance
@ -28,8 +29,9 @@ logger = logging.getLogger(__name__)
class ServersController(metaclass=Singleton):
servers_list: ServerInstance
def __init__(self, helper, servers_helper, management_helper):
def __init__(self, helper, servers_helper, management_helper, file_helper):
self.helper: Helpers = helper
self.file_helper: FileHelpers = file_helper
self.servers_helper: HelperServers = servers_helper
self.management_helper = management_helper
self.servers_list = []
@ -189,6 +191,7 @@ class ServersController(metaclass=Singleton):
self.helper,
self.management_helper,
self.stats,
self.file_helper,
),
"server_settings": settings.props,
}

View File

@ -63,7 +63,9 @@ class Stats:
psutil.boot_time(), datetime.timezone.utc
)
except Exception as e:
logger.debug(f"error while getting boot time due to {e}")
logger.debug(
"getting boot time failed due to the following error:", exc_info=e
)
# unix epoch with no timezone data
return datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
@ -72,7 +74,9 @@ class Stats:
try:
return psutil.cpu_percent(interval=0.5) / psutil.cpu_count()
except Exception as e:
logger.debug(f"error while getting cpu percentage due to {e}")
logger.debug(
"getting the cpu usage failed due to the following error:", exc_info=e
)
return -1
def __init__(self, helper, controller):
@ -100,7 +104,9 @@ class Stats:
"disk_data": Stats._try_all_disk_usage(),
}
except Exception as e:
logger.debug(f"error while getting host stats due to {e}")
logger.debug(
"getting host stats failed due to the following error:", exc_info=e
)
node_stats: NodeStatsDict = {
"boot_time": str(
datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
@ -124,21 +130,27 @@ class Stats:
}
@staticmethod
def _try_get_process_stats(process):
def _try_get_process_stats(process, running):
if running:
try:
return Stats._get_process_stats(process)
except Exception as e:
logger.debug(f"error while getting process stats due to {e}")
logger.debug(
f"getting process stats for pid {process.pid} "
"failed due to the following error:",
exc_info=e,
)
return {"cpu_usage": -1, "memory_usage": -1, "mem_percentage": -1}
else:
return {"cpu_usage": 0, "memory_usage": 0, "mem_percentage": 0}
@staticmethod
def _get_process_stats(process):
if process is None:
return {"cpu_usage": 0, "memory_usage": 0, "mem_percentage": 0}
return {"cpu_usage": -1, "memory_usage": -1, "mem_percentage": -1}
process_pid = process.pid
try:
p = psutil.Process(process_pid)
dummy = p.cpu_percent()
_dummy = p.cpu_percent()
# call it first so we can be more accurate per the docs
# https://giamptest.readthedocs.io/en/latest/#psutil.Process.cpu_percent
@ -149,25 +161,19 @@ class Stats:
with p.oneshot():
process_stats = {
"cpu_usage": real_cpu,
"memory_usage": Helpers.human_readable_file_size(
p.memory_info()[0]
),
"memory_usage": Helpers.human_readable_file_size(p.memory_info()[0]),
"mem_percentage": round(p.memory_percent(), 0),
}
return process_stats
except Exception as e:
logger.error(
f"Unable to get process details for pid: {process_pid} Error: {e}"
)
return {"cpu_usage": 0, "memory_usage": 0, "mem_percentage": 0}
@staticmethod
def _try_all_disk_usage():
try:
return Stats._all_disk_usage()
except Exception as e:
logger.debug(f"error while getting disk data due to {e}")
logger.debug(
"getting disk stats failed due to the following error:", exc_info=e
)
return []
# Source: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py
@ -212,7 +218,7 @@ class Stats:
return level_total_size
def get_server_players(self, server_id): # pylint: disable=no-self-use
def get_server_players(self, server_id):
server = HelperServers.get_server_data_by_id(server_id)
@ -246,15 +252,20 @@ class Stats:
online_stats = json.loads(ping_obj.players)
except Exception as e:
logger.info(f"Unable to read json from ping_obj: {e}")
logger.info(
"Unable to read json from ping_obj due to the following error:",
exc_info=e,
)
try:
server_icon = base64.encodebytes(ping_obj.icon)
server_icon = server_icon.decode("utf-8")
except Exception as e:
server_icon = False
logger.info(f"Unable to read the server icon : {e}")
logger.info(
"Unable to read the server icon due to the following error:", exc_info=e
)
if ping_obj:
ping_data = {
"online": online_stats.get("online", 0),
"max": online_stats.get("max", 0),
@ -263,6 +274,15 @@ class Stats:
"server_version": ping_obj.version,
"server_icon": server_icon,
}
else:
ping_data = {
"online": online_stats.get("online", 0),
"max": online_stats.get("max", 0),
"players": online_stats.get("players", 0),
"server_description": "",
"server_version": "",
"server_icon": server_icon,
}
return ping_data
@ -273,7 +293,9 @@ class Stats:
server_icon = base64.encodebytes(ping_obj["icon"])
except Exception as e:
server_icon = False
logger.info(f"Unable to read the server icon : {e}")
logger.info(
"Unable to read the server icon due to the following error:", exc_info=e
)
ping_data = {
"online": ping_obj["server_player_count"],
"max": ping_obj["server_player_max"],

View File

@ -38,6 +38,16 @@ class AuditLog(BaseModel):
table_name = "audit_log"
# **********************************************************************************
# Crafty Settings Class
# **********************************************************************************
class CraftySettings(BaseModel):
secret_api_key = CharField(default="")
class Meta:
table_name = "crafty_settings"
# **********************************************************************************
# Host_Stats Class
# **********************************************************************************
@ -118,6 +128,7 @@ class Backups(BaseModel):
max_backups = IntegerField()
server_id = ForeignKeyField(Servers, backref="backups_server")
compress = BooleanField(default=False)
shutdown = BooleanField(default=False)
class Meta:
table_name = "backups"
@ -231,6 +242,17 @@ class HelpersManagement:
else:
return
@staticmethod
def set_secret_api_key(key):
CraftySettings.insert(secret_api_key=key).execute()
@staticmethod
def get_secret_api_key():
settings = CraftySettings.select(CraftySettings.secret_api_key).where(
CraftySettings.id == 1
)
return settings[0].secret_api_key
# **********************************************************************************
# Schedules Methods
# **********************************************************************************
@ -330,6 +352,7 @@ class HelpersManagement:
"max_backups": row.max_backups,
"server_id": row.server_id_id,
"compress": row.compress,
"shutdown": row.shutdown,
}
except IndexError:
conf = {
@ -338,6 +361,7 @@ class HelpersManagement:
"max_backups": 0,
"server_id": server_id,
"compress": False,
"shutdown": False,
}
return conf
@ -348,6 +372,7 @@ class HelpersManagement:
max_backups: int = None,
excluded_dirs: list = None,
compress: bool = False,
shutdown: bool = False,
):
logger.debug(f"Updating server {server_id} backup config with {locals()}")
if Backups.select().where(Backups.server_id == server_id).exists():
@ -359,6 +384,7 @@ class HelpersManagement:
"max_backups": 0,
"server_id": server_id,
"compress": False,
"shutdown": False,
}
new_row = True
if max_backups is not None:
@ -367,6 +393,7 @@ class HelpersManagement:
dirs_to_exclude = ",".join(excluded_dirs)
conf["excluded_dirs"] = dirs_to_exclude
conf["compress"] = compress
conf["shutdown"] = shutdown
if not new_row:
with self.database.atomic():
if backup_path is not None:

View File

@ -5,6 +5,7 @@ import jwt
from jwt import PyJWTError
from app.classes.models.users import HelperUsers, ApiKeys
from app.classes.controllers.management_controller import ManagementController
logger = logging.getLogger(__name__)
@ -13,11 +14,14 @@ class Authentication:
def __init__(self, helper):
self.helper = helper
self.secret = "my secret"
self.secret = self.helper.get_setting("apikey_secret", None)
if self.secret is None or self.secret == "random":
try:
self.secret = ManagementController.get_crafty_api_key()
if self.secret == "":
self.secret = self.helper.random_string_generator(64)
self.helper.set_setting("apikey_secret", self.secret)
ManagementController.set_crafty_api_key(str(self.secret))
except:
self.secret = self.helper.random_string_generator(64)
ManagementController.set_crafty_api_key(str(self.secret))
def generate(self, user_id, extra=None):
if extra is None:

View File

@ -2,14 +2,22 @@ import os
import shutil
import logging
import pathlib
import tempfile
import zipfile
from zipfile import ZipFile, ZIP_DEFLATED
from app.classes.shared.helpers import Helpers
from app.classes.shared.console import Console
logger = logging.getLogger(__name__)
class FileHelpers:
allowed_quotes = ['"', "'", "`"]
def __init__(self, helper):
self.helper: Helpers = helper
@staticmethod
def del_dirs(path):
path = pathlib.Path(path)
@ -82,7 +90,6 @@ class FileHelpers:
f"Error backing up: {os.path.join(root, file)}!"
f" - Error was: {e}"
)
return True
@staticmethod
@ -113,3 +120,173 @@ class FileHelpers:
)
return True
def make_compressed_backup(
self, path_to_destination, path_to_zip, excluded_dirs, server_id
):
# create a ZipFile object
path_to_destination += ".zip"
ex_replace = [p.replace("\\", "/") for p in excluded_dirs]
total_bytes = 0
dir_bytes = Helpers.get_dir_size(path_to_zip)
results = {
"percent": 0,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
with ZipFile(path_to_destination, "w", ZIP_DEFLATED) as zip_file:
for root, dirs, files in os.walk(path_to_zip, topdown=True):
for l_dir in dirs:
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
dirs.remove(l_dir)
ziproot = path_to_zip
for file in files:
if (
str(os.path.join(root, file)).replace("\\", "/")
not in ex_replace
and file != "crafty.sqlite"
):
try:
logger.info(f"backing up: {os.path.join(root, file)}")
if os.name == "nt":
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, ""), file),
)
else:
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, "/"), file),
)
except Exception as e:
logger.warning(
f"Error backing up: {os.path.join(root, file)}!"
f" - Error was: {e}"
)
total_bytes += os.path.getsize(os.path.join(root, file))
percent = round((total_bytes / dir_bytes) * 100, 2)
results = {
"percent": percent,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
return True
def make_backup(self, path_to_destination, path_to_zip, excluded_dirs, server_id):
# create a ZipFile object
path_to_destination += ".zip"
ex_replace = [p.replace("\\", "/") for p in excluded_dirs]
total_bytes = 0
dir_bytes = Helpers.get_dir_size(path_to_zip)
results = {
"percent": 0,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
with ZipFile(path_to_destination, "w") as zip_file:
for root, dirs, files in os.walk(path_to_zip, topdown=True):
for l_dir in dirs:
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
dirs.remove(l_dir)
ziproot = path_to_zip
for file in files:
if (
str(os.path.join(root, file)).replace("\\", "/")
not in ex_replace
and file != "crafty.sqlite"
):
try:
logger.info(f"backing up: {os.path.join(root, file)}")
if os.name == "nt":
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, ""), file),
)
else:
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, "/"), file),
)
except Exception as e:
logger.warning(
f"Error backing up: {os.path.join(root, file)}!"
f" - Error was: {e}"
)
total_bytes += os.path.getsize(os.path.join(root, file))
percent = round((total_bytes / dir_bytes) * 100, 2)
results = {
"percent": percent,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
return True
@staticmethod
def unzip_file(zip_path):
new_dir_list = zip_path.split("/")
new_dir = ""
for i in range(len(new_dir_list) - 1):
if i == 0:
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):
Helpers.ensure_dir_exists(new_dir)
temp_dir = tempfile.mkdtemp()
try:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
for i in enumerate(zip_ref.filelist):
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[
i
].filename.endswith("/"):
break
full_root_path = temp_dir
for item in os.listdir(full_root_path):
if os.path.isdir(os.path.join(full_root_path, item)):
try:
FileHelpers.move_dir(
os.path.join(full_root_path, item),
os.path.join(new_dir, item),
)
except Exception as ex:
logger.error(f"ERROR IN ZIP IMPORT: {ex}")
else:
try:
FileHelpers.move_file(
os.path.join(full_root_path, item),
os.path.join(new_dir, item),
)
except Exception as ex:
logger.error(f"ERROR IN ZIP IMPORT: {ex}")
except Exception as ex:
Console.error(ex)
else:
return "false"
return

View File

@ -15,6 +15,8 @@ import html
import zipfile
import pathlib
import ctypes
import subprocess
import itertools
from datetime import datetime
from socket import gethostname
from contextlib import redirect_stderr, suppress
@ -22,7 +24,6 @@ from contextlib import redirect_stderr, suppress
from app.classes.shared.null_writer import NullWriter
from app.classes.shared.console import Console
from app.classes.shared.installer import installer
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.translation import Translation
from app.classes.web.websocket_helper import WebSocketHelper
@ -81,6 +82,60 @@ class Helpers:
print(f"Import Error: Unable to load {ex.name} module")
installer.do_install()
@staticmethod
def find_java_installs():
# If we're windows return oracle java versions,
# otherwise java vers need to be manual.
if os.name == "nt":
# Adapted from LeeKamentsky >>>
# https://github.com/LeeKamentsky/python-javabridge/blob/master/javabridge/locate.py
jdk_key_paths = (
"SOFTWARE\\JavaSoft\\JDK",
"SOFTWARE\\JavaSoft\\Java Development Kit",
)
java_paths = []
for jdk_key_path in jdk_key_paths:
try:
with suppress(OSError), winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, jdk_key_path
) as kjdk:
for i in itertools.count():
version = winreg.EnumKey(kjdk, i)
kjdk_current = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
jdk_key_path,
)
kjdk_current = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
jdk_key_path + "\\" + version,
)
kjdk_current_values = dict( # pylint: disable=consider-using-dict-comprehension
[
winreg.EnumValue(kjdk_current, i)[:2]
for i in range(winreg.QueryInfoKey(kjdk_current)[1])
]
)
java_paths.append(kjdk_current_values["JavaHome"])
except OSError as e:
if e.errno == 2:
continue
raise
return java_paths
# If we get here we're linux so we will use 'update-alternatives'
# (If distro does not have update-alternatives then manual input.)
try:
paths = subprocess.check_output(
["/usr/bin/update-alternatives", "--list", "java"], encoding="utf8"
)
if re.match("^(/[^/ ]*)+/?$", paths):
return paths.split("\n")
except Exception as e:
print("Java Detect Error: ", e)
logger.error(f"Java Detect Error: {e}")
@staticmethod
def float_to_string(gbs: float):
s = str(float(gbs) * 1000).rstrip("0").rstrip(".")
@ -443,53 +498,6 @@ class Helpers:
return ctypes.windll.shell32.IsUserAnAdmin() == 1
return os.geteuid() == 0
@staticmethod
def unzip_file(zip_path):
new_dir_list = zip_path.split("/")
new_dir = ""
for i in range(len(new_dir_list) - 1):
if i == 0:
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):
Helpers.ensure_dir_exists(new_dir)
temp_dir = tempfile.mkdtemp()
try:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
for i in enumerate(zip_ref.filelist):
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[
i
].filename.endswith("/"):
break
full_root_path = temp_dir
for item in os.listdir(full_root_path):
if os.path.isdir(os.path.join(full_root_path, item)):
try:
FileHelpers.move_dir(
os.path.join(full_root_path, item),
os.path.join(new_dir, item),
)
except Exception as ex:
logger.error(f"ERROR IN ZIP IMPORT: {ex}")
else:
try:
FileHelpers.move_file(
os.path.join(full_root_path, item),
os.path.join(new_dir, item),
)
except Exception as ex:
logger.error(f"ERROR IN ZIP IMPORT: {ex}")
except Exception as ex:
Console.error(ex)
else:
return "false"
return
def ensure_logging_setup(self):
log_file = os.path.join(os.path.curdir, "logs", "commander.log")
session_log_file = os.path.join(os.path.curdir, "logs", "session.log")
@ -837,7 +845,7 @@ class Helpers:
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
elif str(item) != "crafty.sqlite":
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
@ -868,13 +876,14 @@ class Helpers:
@staticmethod
def generate_dir(folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
elif str(item) != "crafty.sqlite":
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
@ -991,14 +1000,6 @@ class Helpers:
[parent_path, child_path]
)
@staticmethod
def copy_files(source, dest):
if os.path.isfile(source):
FileHelpers.copy_file(source, dest)
logger.info("Copying jar %s to %s", source, dest)
else:
logger.info("Source jar does not exist.")
@staticmethod
def download_file(executable_url, jar_path):
try:

View File

@ -34,8 +34,9 @@ logger = logging.getLogger(__name__)
class Controller:
def __init__(self, database, helper):
def __init__(self, database, helper, file_helper):
self.helper: Helpers = helper
self.file_helper: FileHelpers = file_helper
self.server_jars: ServerJars = ServerJars(helper)
self.users_helper: HelperUsers = HelperUsers(database, self.helper)
self.roles_helper: HelperRoles = HelperRoles(database)
@ -53,7 +54,7 @@ class Controller:
)
self.server_perms: ServerPermsController = ServerPermsController()
self.servers: ServersController = ServersController(
self.helper, self.servers_helper, self.management_helper
self.helper, self.servers_helper, self.management_helper, self.file_helper
)
self.users: UsersController = UsersController(
self.helper, self.users_helper, self.authentication

View File

@ -17,8 +17,6 @@ class DatabaseBuilder:
logger.info("Fresh Install Detected - Creating Default Settings")
Console.info("Fresh Install Detected - Creating Default Settings")
default_data = self.helper.find_default_password()
# Reset this value if the DB has been dumped
self.helper.set_setting("apikey_secret", "random")
username = default_data.get("username", "admin")
password = default_data.get("password", "crafty")

View File

@ -9,7 +9,6 @@ import threading
import logging.config
import subprocess
import html
import tempfile
# TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone
@ -102,12 +101,14 @@ class ServerOutBuf:
class ServerInstance:
server_object: Servers
helper: Helpers
file_helper: FileHelpers
management_helper: HelpersManagement
stats: Stats
stats_helper: HelperServerStats
def __init__(self, server_id, helper, management_helper, stats):
def __init__(self, server_id, helper, management_helper, stats, file_helper):
self.helper = helper
self.file_helper = file_helper
self.management_helper = management_helper
# holders for our process
self.process = None
@ -126,6 +127,7 @@ class ServerInstance:
self.stats = stats
self.server_object = HelperServers.get_server_obj(self.server_id)
self.stats_helper = HelperServerStats(self.server_id)
self.last_backup_failed = False
try:
tz = get_localzone()
except ZoneInfoNotFoundError:
@ -239,6 +241,11 @@ class ServerInstance:
"Detected nebulous java in start command. "
"Replacing with full java path."
)
# Checks for Oracle Java. Only Oracle Java's helper will cause a re-exec.
if "/Oracle/Java/" in str(shutil.which("java")):
logger.info(
"Oracle Java detected. Changing start command to avoid re-exec."
)
which_java_raw = self.helper.which_java()
java_path = which_java_raw + "\\bin\\java"
if str(which_java_raw) != str(self.helper.get_servers_root_dir) or str(
@ -840,6 +847,7 @@ class ServerInstance:
"backup_reload",
{"percent": 0, "total_files": 0},
)
was_server_running = None
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
server_users = PermissionsServers.get_server_user_list(self.server_id)
for user in server_users:
@ -852,6 +860,15 @@ class ServerInstance:
)
time.sleep(3)
conf = HelpersManagement.get_backup_config(self.server_id)
if conf["shutdown"]:
logger.info(
"Found shutdown preference. Delaying"
+ "backup start. Shutting down server."
)
if self.check_running():
self.stop_server()
was_server_running = True
self.helper.ensure_dir_exists(self.settings["backup_path"])
try:
backup_filename = (
@ -863,62 +880,27 @@ class ServerInstance:
f" (ID#{self.server_id}, path={self.server_path}) "
f"at '{backup_filename}'"
)
temp_dir = tempfile.mkdtemp()
self.server_scheduler.add_job(
self.backup_status,
"interval",
seconds=1,
id="backup_" + str(self.server_id),
args=[temp_dir + "/", backup_filename + ".zip"],
)
# pylint: disable=unexpected-keyword-arg
try:
FileHelpers.copy_dir(self.server_path, temp_dir, dirs_exist_ok=True)
except shutil.Error as e:
logger.error(f"Failed to fully complete backup due to shutil error {e}")
excluded_dirs = HelpersManagement.get_excluded_backup_dirs(self.server_id)
server_dir = Helpers.get_os_understandable_path(self.settings["path"])
for my_dir in excluded_dirs:
# Take the full path of the excluded dir and replace the
# server path with the temp path, this is so that we're
# only deleting excluded dirs from the temp path
# and not the server path
excluded_dir = Helpers.get_os_understandable_path(my_dir).replace(
server_dir, Helpers.get_os_understandable_path(temp_dir)
)
# Next, check to see if it is a directory
if os.path.isdir(excluded_dir):
# If it is a directory,
# recursively delete the entire directory from the backup
try:
FileHelpers.del_dirs(excluded_dir)
except FileNotFoundError:
Console.error(
f"Excluded dir {excluded_dir} not found. Moving on..."
)
else:
# If not, just remove the file
try:
os.remove(excluded_dir)
except:
Console.error(
f"Excluded dir {excluded_dir} not found. Moving on..."
)
if conf["compress"]:
logger.debug(
"Found compress backup to be true. Calling compressed archive"
)
FileHelpers.make_compressed_archive(
Helpers.get_os_understandable_path(backup_filename), temp_dir
self.file_helper.make_compressed_backup(
Helpers.get_os_understandable_path(backup_filename),
server_dir,
excluded_dirs,
self.server_id,
)
else:
logger.debug(
"Found compress backup to be false. Calling NON-compressed archive"
)
FileHelpers.make_archive(
Helpers.get_os_understandable_path(backup_filename), temp_dir
self.file_helper.make_backup(
Helpers.get_os_understandable_path(backup_filename),
server_dir,
excluded_dirs,
self.server_id,
)
while (
@ -933,7 +915,6 @@ class ServerInstance:
self.is_backingup = False
logger.info(f"Backup of server: {self.name} completed")
self.server_scheduler.remove_job("backup_" + str(self.server_id))
results = {"percent": 100, "total_files": 0, "current_file": 0}
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params(
@ -953,12 +934,17 @@ class ServerInstance:
HelperUsers.get_user_lang_by_id(user),
).format(self.name),
)
if was_server_running:
logger.info(
"Backup complete. User had shutdown preference. Starting server."
)
self.start_server(HelperUsers.get_user_id_by_name("system"))
time.sleep(3)
self.last_backup_failed = False
except:
logger.exception(
f"Failed to create backup of server {self.name} (ID {self.server_id})"
)
self.server_scheduler.remove_job("backup_" + str(self.server_id))
results = {"percent": 100, "total_files": 0, "current_file": 0}
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params(
@ -968,8 +954,12 @@ class ServerInstance:
results,
)
self.is_backingup = False
finally:
FileHelpers.del_dirs(temp_dir)
if was_server_running:
logger.info(
"Backup complete. User had shutdown preference. Starting server."
)
self.start_server(HelperUsers.get_user_id_by_name("system"))
self.last_backup_failed = True
def backup_status(self, source_path, dest_path):
results = Helpers.calc_percent(source_path, dest_path)
@ -982,6 +972,9 @@ class ServerInstance:
results,
)
def last_backup_status(self):
return self.last_backup_failed
def send_backup_status(self):
try:
return self.backup_stats
@ -1087,7 +1080,7 @@ class ServerInstance:
)
# copies to backup dir
Helpers.copy_files(current_executable, backup_executable)
FileHelpers.copy_file(current_executable, backup_executable)
# boolean returns true for false for success
downloaded = Helpers.download_file(
@ -1249,7 +1242,7 @@ class ServerInstance:
server_path = server["path"]
# process stats
p_stats = Stats._try_get_process_stats(self.process)
p_stats = Stats._try_get_process_stats(self.process, self.check_running())
# TODO: search server properties file for possible override of 127.0.0.1
internal_ip = server["server_ip"]
@ -1382,7 +1375,7 @@ class ServerInstance:
server_path = server_dt["path"]
# process stats
p_stats = Stats._try_get_process_stats(self.process)
p_stats = Stats._try_get_process_stats(self.process, self.check_running())
# TODO: search server properties file for possible override of 127.0.0.1
# internal_ip = server['server_ip']

View File

@ -220,7 +220,7 @@ class FileHandler(BaseHandler):
path = Helpers.get_os_understandable_path(self.get_argument("path", None))
if Helpers.is_os_windows():
path = Helpers.wtol_path(path)
Helpers.unzip_file(path)
FileHelpers.unzip_file(path)
self.redirect(f"/panel/server_detail?id={server_id}&subpage=files")
return

View File

@ -6,6 +6,7 @@ import typing as t
import json
import logging
import threading
import shlex
import bleach
import libgravatar
import requests
@ -183,6 +184,7 @@ class PanelHandler(BaseHandler):
logger.debug(f'User {exec_user["user_id"]} does not have permission')
self.redirect("/panel/error?error=Invalid Server ID")
return None
return server_id
# Server fetching, spawned asynchronously
# TODO: Make the related front-end elements update with AJAX
@ -496,6 +498,10 @@ class PanelHandler(BaseHandler):
if server_id is None:
return
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
page_data["backup_failed"] = server_obj.last_backup_status()
server_obj = None
valid_subpages = [
"term",
"logs",
@ -626,6 +632,18 @@ class PanelHandler(BaseHandler):
"/panel/error?error=Unauthorized access Server Config"
)
return
page_data["java_versions"] = Helpers.find_java_installs()
server_obj: Servers = self.controller.servers.get_server_obj(server_id)
page_java = []
page_data["java_versions"].append("java")
for version in page_data["java_versions"]:
if os.name == "nt":
page_java.append(version)
else:
if len(version) > 0:
page_java.append(version)
page_data["java_versions"] = page_java
if subpage == "files":
if (
@ -1057,6 +1075,11 @@ class PanelHandler(BaseHandler):
if user_id is None:
self.redirect("/panel/error?error=Invalid User ID")
return
if int(user_id) != exec_user["user_id"] and not exec_user["superuser"]:
self.redirect(
"/panel/error?error=You are not authorized to view this page."
)
return
template = "panel/panel_edit_user_apikeys.html"
@ -1221,6 +1244,9 @@ class PanelHandler(BaseHandler):
self.download_file(name, file)
self.redirect(f"/panel/server_detail?id={server_id}&subpage=files")
elif page == "wiki":
template = "panel/wiki.html"
elif page == "download_support_package":
temp_zip_storage = exec_user["support_logs"]
@ -1333,6 +1359,8 @@ class PanelHandler(BaseHandler):
if Helpers.is_os_windows():
log_path.replace(" ", "^ ")
log_path = Helpers.wtol_path(log_path)
if not self.helper.validate_traversal(server_obj.path, log_path):
log_path = ""
executable = self.get_argument("executable", None)
execution_command = self.get_argument("execution_command", None)
server_ip = self.get_argument("server_ip", None)
@ -1346,11 +1374,46 @@ class PanelHandler(BaseHandler):
auto_start = int(float(self.get_argument("auto_start", "0")))
crash_detection = int(float(self.get_argument("crash_detection", "0")))
logs_delete_after = int(float(self.get_argument("logs_delete_after", "0")))
java_selection = self.get_argument("java_selection", None)
# subpage = self.get_argument('subpage', None)
server_id = self.check_server_id()
if server_id is None:
return
if java_selection:
try:
execution_list = shlex.split(execution_command)
except ValueError:
self.redirect(
"/panel/error?error=Invalid execution command. Java path"
" must be surrounded by quotes."
" (Are you missing a closing quote?)"
)
if not any(
java_selection in path for path in Helpers.find_java_installs()
):
self.redirect(
"/panel/error?error=Attack attempted."
+ " A copy of this report is being sent to server owner."
)
self.controller.management.add_to_audit_log_raw(
exec_user["username"],
exec_user["user_id"],
server_id,
f"Attempted to send bad java path for {server_id}."
+ " Possible attack. Act accordingly.",
self.get_remote_ip(),
)
if java_selection != "java":
if self.helper.is_os_windows():
execution_list[0] = '"' + java_selection + '/bin/java"'
else:
execution_list[0] = '"' + java_selection + '"'
else:
execution_list[0] = "java"
execution_command = ""
for item in execution_list:
execution_command += item + " "
server_obj: Servers = self.controller.servers.get_server_obj(server_id)
stale_executable = server_obj.executable
@ -1380,7 +1443,7 @@ class PanelHandler(BaseHandler):
server_obj.path = server_obj.path
server_obj.log_path = server_obj.log_path
server_obj.executable = server_obj.executable
server_obj.execution_command = server_obj.execution_command
server_obj.execution_command = execution_command
server_obj.server_ip = server_obj.server_ip
server_obj.server_port = server_obj.server_port
server_obj.executable_update_url = server_obj.executable_update_url
@ -1424,6 +1487,7 @@ class PanelHandler(BaseHandler):
server_obj = self.controller.servers.get_server_obj(server_id)
compress = self.get_argument("compress", False)
shutdown = self.get_argument("shutdown", False)
check_changed = self.get_argument("changed")
if str(check_changed) == str(1):
checked = self.get_body_arguments("root_path")
@ -1446,6 +1510,7 @@ class PanelHandler(BaseHandler):
max_backups=max_backups,
excluded_dirs=checked,
compress=bool(compress),
shutdown=bool(shutdown),
)
self.controller.management.add_to_audit_log(
@ -1892,6 +1957,13 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/error?error=Invalid User ID")
return
if str(user_id) != str(exec_user["user_id"]) and not exec_user["superuser"]:
self.redirect(
"/panel/error?error=You do not have access to change"
+ "this user's api key."
)
return
crafty_permissions_mask = self.get_perms()
server_permissions_mask = self.get_perms_server()
@ -1925,6 +1997,15 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/error?error=Invalid Key ID")
return
if (
str(key.user_id) != str(exec_user["user_id"])
and not exec_user["superuser"]
):
self.redirect(
"/panel/error?error=You are not authorized to access this key."
)
return
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Generated a new API token for the key {key.name} "
@ -2141,6 +2222,15 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/error?error=Invalid Key ID")
return
key_obj = self.controller.users.get_user_api_key(key_id)
if key_obj.user_id != exec_user["user_id"] and not exec_user["superuser"]:
self.redirect(
"/panel/error?error=You do not have access to change"
+ "this user's api key."
)
return
self.controller.users.delete_user_api_key(key_id)
self.controller.management.add_to_audit_log(
@ -2150,7 +2240,8 @@ class PanelHandler(BaseHandler):
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect("/panel/panel_config")
self.finish()
self.redirect(f"/panel/edit_user_apikeys?id={key_obj.user_id}")
else:
self.set_status(404)
self.render(

View File

@ -17,6 +17,15 @@ logger = logging.getLogger(__name__)
class ServerHandler(BaseHandler):
def get_user_roles(self):
user_roles = {}
for user_id in self.controller.users.get_all_user_ids():
user_roles_list = self.controller.users.get_user_roles_names(user_id)
# user_servers =
# self.controller.servers.get_authorized_servers(user.user_id)
user_roles[user_id] = user_roles_list
return user_roles
@tornado.web.authenticated
def get(self, page):
(
@ -271,11 +280,19 @@ class ServerHandler(BaseHandler):
)
if page == "step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.controller.roles.get_all_roles()
user_roles = self.get_user_roles()
server = bleach.clean(self.get_argument("server", ""))
server_name = bleach.clean(self.get_argument("server_name", ""))
min_mem = bleach.clean(self.get_argument("min_memory", ""))
@ -396,6 +413,14 @@ class ServerHandler(BaseHandler):
self.redirect("/panel/dashboard")
if page == "bedrock_step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:

View File

@ -1,6 +1,6 @@
{
"major": 4,
"minor": 0,
"sub": 2,
"sub": 4,
"meta": "beta"
}

View File

@ -9,7 +9,7 @@
}
.sidebar > .nav > .nav-item:not(.nav-profile) > .nav-link:before {
.sidebar>.nav>.nav-item:not(.nav-profile)>.nav-link:before {
content: none;
position: absolute;
left: 30px;
@ -21,7 +21,7 @@
display: block;
}
.sidebar > .nav > .nav-item:not(.nav-profile) > .nav-link:before {
.sidebar>.nav>.nav-item:not(.nav-profile)>.nav-link:before {
content: none;
position: absolute;
left: 30px;
@ -33,43 +33,48 @@
display: block;
}
.sidebar > .nav .nav-item .nav-link, .collapsed{
.sidebar>.nav .nav-item .nav-link,
.collapsed {
padding: 15px 30px;
}
.mc-log-time{
color:#19d895;
.mc-log-time {
color: #19d895;
}
.mc-log-info{
color:#8862e0;
.mc-log-info {
color: #8862e0;
}
.mc-log-warn{
color:#ffaf00;
.mc-log-warn {
color: #ffaf00;
}
.mc-log-error{
color:#af463f;
.mc-log-error {
color: #af463f;
}
.mc-log-fatal{
color:#da0f00;
.mc-log-fatal {
color: #da0f00;
}
.mc-log-keyword{
color:#2196f3;
.mc-log-keyword {
color: #2196f3;
}
.scrollable-element {
scrollbar-color: red yellow;
}
.term-nav-item {
padding: 1%;
}
/* Fix body scrollbar color */
body { background-color: var(--dark) !important; /* Firefox */ }
body {
background-color: var(--dark) !important;
/* Firefox */
}
/* Webkit */
/* Didn't really work out
@ -81,11 +86,20 @@ body { background-color: var(--dark) !important; /* Firefox */ }
::-webkit-scrollbar-track { background-color: #202538; }
::-webkit-scrollbar-corner { background-color: #202538; }*/
.actions_serverlist > a > i {
.actions_serverlist>a>i {
cursor: pointer;
}
.actions_serveritem {
cursor: pointer;
}
.corner {
position: absolute;
margin-top: 0;
margin-left: 0;
}
.accordion .card {
margin-bottom: 0px;
}

View File

@ -1,21 +1,21 @@
<!-- partial -->
<div class="container-fluid page-body-wrapper">
<div class="container-fluid page-body-wrapper">
<!-- partial:partials/_sidebar.html -->
<style>
@media screen and (max-width: 991px) {
.sidebar-offcanvas {
-webkit-transition: all 0.25s cubic-bezier(.22,.61,.36,1);
transition: all 0.25s cubic-bezier(.22,.61,.36,1);
box-shadow: 0px 8px 17px 2px rgba(0,0,0,0.14) , 0px 3px 14px 2px rgba(0,0,0,0.12) , 0px 5px 5px -3px rgba(0,0,0,0.2);
-webkit-transition: all 0.25s cubic-bezier(.22, .61, .36, 1);
transition: all 0.25s cubic-bezier(.22, .61, .36, 1);
box-shadow: 0px 8px 17px 2px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12), 0px 5px 5px -3px rgba(0, 0, 0, 0.2);
}
}
</style>
<script>
function debounce(func, wait, immediate) {
var timeout;
return function() {
return function () {
var context = this, args = arguments;
var later = function() {
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
@ -33,7 +33,7 @@
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
return vw >= 992;
}
$(document).ready(function() {
$(document).ready(function () {
sidebarResizeHandler(null);
$(window).on(
'resize',
@ -66,7 +66,8 @@
<nav class="sidebar sidebar-offcanvas" id="sidebar">
<ul class="nav">
<li class="nav-item nav-category" style="margin-top:10px;">{{ translate('sidebar', 'navigation', data['lang']) }}</li>
<li class="nav-item nav-category" style="margin-top:10px;">{{ translate('sidebar', 'navigation', data['lang']) }}
</li>
<li class="nav-item">
<a class="nav-link" href="/panel/dashboard">
@ -76,7 +77,8 @@
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="collapse" href="#page-layouts" aria-expanded="false" aria-controls="page-layouts">
<a class="nav-link" data-toggle="collapse" href="#page-layouts" aria-expanded="false"
aria-controls="page-layouts">
<i class="fas fa-server"></i> &nbsp;
<span class="menu-title">{{ translate('sidebar', 'servers', data['lang']) }}</span>
<i class="menu-arrow"></i>
@ -85,12 +87,14 @@
<ul class="nav flex-column sub-menu">
{% if data['crafty_permissions']['Server_Creation'] in data['user_crafty_permissions'] %}
<li class="nav-item">
<a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> &nbsp; {{ translate('sidebar', 'newServer', data['lang']) }}</a>
<a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> &nbsp; {{ translate('sidebar',
'newServer', data['lang']) }}</a>
</li>
{% end %}
{% for s in data['menu_servers'] %}
<li class="nav-item">
<a class="nav-link" href="/panel/server_detail?id={{s['server_id']}}"><i class="fas fa-server"></i> &nbsp; {{s['server_name']}}</a>
<a class="nav-link" href="/panel/server_detail?id={{s['server_id']}}"><i class="fas fa-server"></i> &nbsp;
{{s['server_name']}}</a>
</li>
{% end %}
@ -105,6 +109,13 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/panel/wiki">
<i class="fa fa-info-circle"></i> &nbsp;
<span class="menu-title">Wiki</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://discord.gg/9VJPhCE" target="_blank">
<i class="fab fa-discord"></i> &nbsp;

View File

@ -25,15 +25,15 @@
<!-- Page Title Header Ends-->
{% if data['first_log'] %}
<script>
$(document).ready(function() {
$(document).ready(function () {
bootbox.alert({
backdrop: true,
title: 'Your Feedback Is Appreciated...',
message: '<p>We will only request this information from the admin user once... 😊</p>'
+'<p><strong>All data collected is completely anonymous</strong> and is only used to improve Crafty 4.0 and allow us to more accurately report the number of Crafty 4 users.</p><iframe width="640px" height="480px"'
+'src="https://forms.office.com/Pages/ResponsePage.aspx?id=LwLajNkpXU2CKc95G1oO3MN0Hu3oEUNLr-EtLx31TS5UNUNVQlFNVUVYMEc'
+'1V1BKS0FQUUlERUtWQy4u&embed=true" frameborder="0" marginwidth="0" marginheight="0" style="border: none; max-width:100%;'
+' max-height:100vh" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen> </iframe>',
+ '<p><strong>All data collected is completely anonymous</strong> and is only used to improve Crafty 4.0 and allow us to more accurately report the number of Crafty 4 users.</p><iframe width="640px" height="480px"'
+ 'src="https://forms.office.com/Pages/ResponsePage.aspx?id=LwLajNkpXU2CKc95G1oO3MN0Hu3oEUNLr-EtLx31TS5UNUNVQlFNVUVYMEc'
+ '1V1BKS0FQUUlERUtWQy4u&embed=true" frameborder="0" marginwidth="0" marginheight="0" style="border: none; max-width:100%;'
+ ' max-height:100vh" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen> </iframe>',
buttons: {
ok: {
label: 'Skip Survey/Done',
@ -125,7 +125,6 @@
</div>
<div class="card-body">
<div class="table-responsive">
{% if len(data['servers']) == 0%}
<div style="text-align: center; color: grey;">
<h1>{{ translate('dashboard', 'welcome', data['lang']) }}</h1>
@ -137,7 +136,8 @@
{% end %}
{% if len(data['servers']) > 0 %}
<!-- View for Large screen -->
<table id="servers_table" class="table table-hover d-none d-sm-table">
<div class="table-responsive d-none d-sm-block">
<table id="servers_table" class="table table-hover">
<thead>
<tr class="rounded" id="first" draggable="false">
<th draggable="false">{{ translate('dashboard', 'server', data['lang']) }}</th>
@ -285,71 +285,30 @@
{% end %}
</tbody>
</table>
<!-- View for Small screen -->
<table id="servers_table" class="table table-hover d-table d-sm-none">
<thead>
<tr class="rounded" id="first" draggable="false">
<th scope="col" draggable="false">{{ translate('dashboard', 'server', data['lang']) }}</th>
<th scope="col" draggable="false">{{ translate('dashboard', 'actions', data['lang']) }}</th>
<th scope="col" draggable="false">{{ translate('dashboard', 'status', data['lang']) }}</th>
<th scope="col" draggable="false"></th>
</tr>
</thead>
<tbody>
</div>
{% end %}
{% if len(data['servers']) > 0 %}
<!-- Try with Accordion -->
<div class="d-sm-none d-block">
<div class="accordion" id="accordionServers">
{% for server in data['servers'] %}
<tr id="{{server['server_data']['server_id']}}" draggable="false">
<td scope="row"><i class="fas fa-server"></i>
<a draggable="false" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
{{ server['server_data']['server_name'] }}
<div class="card">
<div class="card-header" id="heading-{{server['server_data']['server_id']}}">
<h2 class="mb-0 container overflow-hidden">
<div class="row">
<div class="col-10 col-lg-3 mx-0 px-0">
<a class="btn btn-link d-flex justify-content-start" type="button" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
<i class="fas fa-server"></i> {{ server['server_data']['server_name'] }}
</a>
</td>
<td draggable="false" id="controls{{server['server_data']['server_id']}}" class="actions_serverlist">
{% if server['user_command_permission'] %}
{% if server['stats']['running'] %}
<a data-id="{{server['server_data']['server_id']}}" class="stop_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<i class="fas fa-stop"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="restart_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<i class="fas fa-sync"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% elif server['stats']['updating']%}
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class="">{{ translate('serverTerm', 'updating',
data['lang']) }}</i></a>
{% elif server['stats']['waiting_start']%}
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class="" title="{{
translate('dashboard', 'delay-explained' , data['lang'])}}">{{ translate('dashboard', 'starting',
data['lang']) }}</i></a>
{% elif server['stats']['downloading']%}
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i>
{{ translate('serverTerm', 'downloading', data['lang']) }}</a>
{% else %}
<a data-id="{{server['server_data']['server_id']}}" class="play_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'start' , data['lang']) }}">
<i class="fas fa-play"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<i class="fas fa-clone"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% end %}
{% end %}
</td>
<td draggable="false" id="m_server_running_status_{{server['server_data']['server_id']}}">
</div>
<div class="col-2 col-lg-3 mx-0 px-0">
<a class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse" data-target="#collapse-{{server['server_data']['server_id']}}"
aria-expanded="false" aria-controls="collapse-{{server['server_data']['server_id']}}">
<i class="fas fa-chart-bar"></i>
</a>
</div>
<div class="col-4 col-lg-3 mx-0 px-0">
<a id="m_server_running_status_{{server['server_data']['server_id']}}" class="btn btn-link d-flex justify-content-start" type="button">
{% if server['stats']['running'] %}
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online',
data['lang']) }}</span>
@ -361,16 +320,86 @@
<span class="text-warning"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline',
data['lang']) }}</span>
{% end %}
</td>
<td>
<span data-toggle="collapse" data-target="#details_{{server['server_data']['server_id']}}"
aria-expanded="false" aria-controls="details_{{server['server_data']['server_id']}}"><i
class="fas fa-chevron-down"></i></span>
</td>
</tr>
<tr id="details_{{server['server_data']['server_id']}}" class="collapse" draggable="false">
<td colspan="4">
<div class="collapse" id="details_{{server['server_data']['server_id']}}">
</a>
</div>
<div class="col-8 col-lg-3 mx-0 px-0">
<div id="controls{{server['server_data']['server_id']}}" class="container overflow-hidden">
{% if server['user_command_permission'] %}
{% if server['stats']['running'] %}
<div class="row">
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link stop_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<i class="fas fa-stop"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link restart_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<i class="fas fa-sync"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link kill_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a>
</div>
</div>
{% elif server['stats']['updating']%}
<!-- WHAT HAPPENED HERE -->
<div class="row">
<div class="col-12 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link">{{ translate('serverTerm', 'updating',
data['lang']) }}</i></a>
</div>
</div>
{% elif server['stats']['waiting_start']%}
<!-- WHAT HAPPENED HERE -->
<div class="row">
<div class="col-12 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link" title="{{
translate('dashboard', 'delay-explained' , data['lang'])}}">{{ translate('dashboard', 'starting',
data['lang']) }}</i></a>
</div>
</div>
{% elif server['stats']['downloading']%}
<div class="row">
<div class="col-12 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link"><i class="fa fa-spinner fa-spin"></i>
{{ translate('serverTerm', 'downloading', data['lang']) }}</a>
</div>
</div>
{% else %}
<div class="row">
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn play_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'start' , data['lang']) }}">
<i class="fas fa-play"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn clone_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<i class="fas fa-clone"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn kill_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i></a>
</div>
</div>
{% end %}
{% end %}
</div>
</div>
</div>
</h2>
</div>
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse" aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
<div class="card-body">
<div class="row">
<div class="col-6">
<h6>{{ translate('dashboard', 'cpuUsage', data['lang']) }}</h6>
@ -447,14 +476,13 @@
</div>
</div>
</div>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
{% end %}
</div>
</div>
{% end %}
</div>
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Crafty Commander</title>
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">

View File

@ -19,11 +19,18 @@
</li>
{% end %}
{% if data['permissions']['Backup'] in data['user_permissions'] %}
{% if data['backup_failed'] %}
<li class="nav-item term-nav-item">
<a style="color: red !important;" class="nav-link {% if data['active_link'] == 'backup' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=backup" role="tab" aria-selected="false">
<i class="fas fa-save"></i>{{ translate('serverDetails', 'backup', data['lang']) }}&nbsp; <i class="fas fa-exclamation-triangle"> </i></a>
</li>
{% else %}
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'backup' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=backup" role="tab" aria-selected="false">
<i class="fas fa-save"></i>{{ translate('serverDetails', 'backup', data['lang']) }}</a>
</li>
{% end %}
{% end %}
{% if data['permissions']['Files'] in data['user_permissions'] %}
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'files' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=files" role="tab" aria-selected="false">

View File

@ -14,7 +14,8 @@
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
@ -51,55 +52,85 @@
{% if data['backing_up'] %}
<div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar" role="progressbar" style="width:{{data['backup_stats']['percent']}}%;" aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{ data['backup_stats']['percent'] }}%</div>
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div>
<p>Backing up <span id="total_files">{{data['backup_stats']['total_files']}}</span> Files</p>
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>
{% end %}
<br>
{% if not data['backing_up'] %}
<div id="backup_button" class="form-group">
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow', data['lang']) }}</button>
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
data['lang']) }}</button>
</div>
{% end %}
<div class="form-group">
{% if data['super_user'] %}
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="backup_path" id="backup_path" value="{{ data['server_stats']['server_id']['backup_path'] }}" placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="backup_path" id="backup_path"
value="{{ data['server_stats']['server_id']['backup_path'] }}"
placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
{% end %}
</div>
<div class="form-group">
<label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="max_backups" id="max_backups" value="{{ data['backup_config']['max_backups'] }}" placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}">
<label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="max_backups" id="max_backups"
value="{{ data['backup_config']['max_backups'] }}"
placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}">
</div>
<div class="form-group">
<label for="compress" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['compress'] %}
<input type="checkbox" class="form-check-input" id="compress" name="compress"
checked="" value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="compress" name="compress"
<input type="checkbox" class="form-check-input" id="compress" name="compress" checked=""
value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="compress" name="compress" value="True">{{
translate('serverBackups', 'compress', data['lang']) }}
{% end %}
</div>
<div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{ translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button" data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{ translate('serverBackups', 'clickExclude', data['lang']) }}</button>
<label for="shutdown" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['shutdown'] %}
<input type="checkbox" class="form-check-input" id="shutdown" name="shutdown" checked=""
value="True">{{ translate('serverBackups', 'shutdown', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="shutdown" name="shutdown" value="True">{{
translate('serverBackups', 'shutdown', data['lang']) }}
{% end %}
</div>
<input type="number" class="form-control" name="changed" id="changed" value="0" style="visibility: hidden;"></input>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true">
<div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{
translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button"
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div>
<input type="number" class="form-control" name="changed" id="changed" value="0"
style="visibility: hidden;"></input>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups', 'excludedChoose', data['lang']) }}</h5>
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups',
'excludedChoose', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path="" style="overflow: scroll; max-height:75%;">
<div class="tree-ctx-item" id="main-tree-div" data-path=""
style="overflow: scroll; max-height:75%;">
<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i>
@ -110,15 +141,19 @@
</div>
</div>
<div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ translate('serverBackups', 'cancel', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{ translate('serverWizard', 'save', data['lang']) }}</button>
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{
translate('serverBackups', 'cancel', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang']) }}</button>
<button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang']) }}</button>
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang'])
}}</button>
<button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang'])
}}</button>
</form>
</div>
@ -138,13 +173,15 @@
{% for backup in data['backup_list'] %}
<tr>
<td>
<a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-primary">
<a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}"
class="btn btn-primary">
<i class="fas fa-download" aria-hidden="true"></i>
{{ translate('serverBackups', 'download', data['lang']) }}
</a>
<br>
<br>
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}" class="btn btn-danger del_button">
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}"
class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }}
</button>
@ -168,7 +205,8 @@
<br>
<br>
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups', data['lang']) }} <small class="text-muted ml-1"></small> </h4>
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups',
data['lang']) }} <small class="text-muted ml-1"></small> </h4>
</div>
<br>
<ul>

View File

@ -36,7 +36,7 @@
<div class="row">
<div class="col-md-6 col-sm-12">
<form class="forms-sample" method="post" action="/panel/server_detail">
<form class="forms-sample" method="post" id="config_form" action="/panel/server_detail">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="config">
@ -50,8 +50,8 @@
placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" required>
</div>
<div class="form-group">
{% if data['super_user'] %}
<div class="form-group">
<label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small>
</label>
@ -78,7 +78,24 @@
value="{{ data['server_stats']['server_id']['executable'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required>
</div>
{% end %}
{% if data['server_stats']['server_type'] == "minecraft-java" %}
<div class="form-group">
<label for="java_selection">{{ translate('serverConfig', 'javaVersion', data['lang']) }}
<small class="text-muted ml-1">{{ translate('serverConfig', 'javaVersionDesc', data['lang']) }}</small>
</label>
<select class="form-select form-control form-control-lg select-css" id="java_selection"
name="java_selection" form="config_form">
<option value="">{{ translate('serverConfig',
'javaNoChange', data['lang'])}}</option>
{% for path in data['java_versions'] %}
<option value="{{path}}">{{path}}</option>
{% end %}
</select>
</div>
{% end %}
{% if data['super_user'] %}
<div class="form-group">
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc',
@ -86,8 +103,14 @@
<input type="text" class="form-control" name="execution_command" id="execution_command"
value="{{ data['server_stats']['server_id']['execution_command'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" required>
{% end %}
</div>
{% else %}
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<span style="color: gray;">{{ data['server_stats']['server_id']['execution_command'] }}</span> 🔒
</div>
<br>
{% end %}
<div class="form-group">
<label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small

View File

@ -0,0 +1,50 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - Wiki{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">Wiki</h4>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 grid-margin">
<iframe src="https://wiki.craftycontrol.com" width=100% height=2200px title="crafty's wiki"></iframe>
</div>
</div>
<!-- content-wrapper ends -->
<style>
.popover-body {
color: white !important;
;
}
#desc_id {
-ms-overflow-style: none;
/* for Internet Explorer, Edge */
scrollbar-width: none;
/* for Firefox */
overflow-y: scroll;
}
#desc_id::-webkit-scrollbar {
display: none;
/* for Chrome, Safari, and Opera */
}
</style>
{% end %}

View File

@ -4,7 +4,7 @@
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Crafty Commander</title>
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">

View File

@ -4,7 +4,7 @@
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Crafty Commander</title>
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">

View File

@ -4,7 +4,7 @@
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Crafty Commander</title>
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">

View File

@ -68,7 +68,7 @@
<hr>
<div class="row">
<div class="col-sm-3">
<div class="col-sm-4">
<div class="form-group">
<label for="min_memory1">{{ translate('serverWizard', 'minMem', data['lang']) }} <small> - {{
translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
@ -77,7 +77,7 @@
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="col-sm-4">
<div class="form-group">
<label for="max_memory1">{{ translate('serverWizard', 'maxMem', data['lang']) }} <small> - {{
translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
@ -86,7 +86,7 @@
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="col-sm-4">
<div class="form-group">
<label for="port1">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{
translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>
@ -183,7 +183,7 @@
<hr>
<div class="row">
<div class="col-sm-3">
<div class="col-sm-4">
<div class="form-group">
<label for="min_memory2">{{ translate('serverWizard', 'minMem', data['lang']) }} <small> - {{
translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
@ -192,7 +192,7 @@
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="col-sm-4">
<div class="form-group">
<label for="max_memory2">{{ translate('serverWizard', 'maxMem', data['lang']) }} <small> - {{
translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
@ -201,7 +201,7 @@
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="col-sm-4">
<div class="form-group">
<label for="port2">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{
translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>

View File

@ -0,0 +1,16 @@
import peewee
import datetime
def migrate(migrator, db):
class CraftySettings(peewee.Model):
secret_api_key = peewee.CharField(default="")
class Meta:
table_name = "crafty_settings"
migrator.create_table(CraftySettings)
def rollback(migrator, db):
migrator.drop_table("crafty_settings")

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns("backups", shutdown=peewee.BooleanField(default=False))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("backups", ["shutdown"])
"""
Write your rollback migrations here.
"""

View File

@ -352,7 +352,7 @@
"createDirQuestion": "Welchen Namen wünschen Sie für das neue Verzeichnis?",
"createFile": "Datei erstellen",
"createFileQuestion": "Welchen Namen wünschen Sie für die neue Datei?",
"default": "Standart",
"default": "Standard",
"delete": "Löschen",
"deleteItemQuestion": "Sind Sie sicher, dass Sie \" + name + \" löschen wollen?",
"deleteItemQuestionMessage": "Sie löschen gerade \\\"\" + path + \"\\\"!<br/><br/> Diese Aktion ist unumkehrbar!",
@ -454,7 +454,7 @@
"buildServer": "Server erstellen!",
"clickRoot": "Hier klicken, um das root Verzeichniss auszuwählen",
"close": "Schließen",
"defaultPort": "25565 (Standart)",
"defaultPort": "25565 (Standard)",
"downloading": "Server herunterladen...",
"explainRoot": "Bitte klicken Sie auf die Schaltfläche unterhalb, um das Stammverzeichnis Ihres Servers innerhalb des Archivs auszuwählen",
"importing": "Server importieren...",

View File

@ -271,7 +271,8 @@
"save": "Save",
"size": "Size",
"storageLocation": "Storage Location",
"storageLocationDesc": "Where do you want to store backups?"
"storageLocationDesc": "Where do you want to store backups?",
"shutdown": "Shutdown server for duration of backup"
},
"serverConfig": {
"bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.",
@ -300,6 +301,9 @@
"serverCrashDetection": "Server Crash Detection",
"serverExecutable": "Server Executable",
"serverExecutableDesc": "The server's executable file",
"javaVersion": "Override current Java Version",
"javaVersionDesc": "If you're going to override java. Make sure your current java path in 'execution command' is wrapped in quotes (default 'java' variable excluded)",
"javaNoChange": "Do Not Override",
"serverExecutionCommand": "Server Execution Command",
"serverExecutionCommandDesc": "What will be launched in a hidden terminal",
"serverIP": "Server IP",

View File

@ -7,6 +7,7 @@ import argparse
import logging.config
import signal
import peewee
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.import3 import Import3
from app.classes.shared.console import Console
@ -132,9 +133,9 @@ if __name__ == "__main__":
installer.default_settings()
else:
Console.debug("Existing install detected")
file_helper = FileHelpers(helper)
# now the tables are created, we can load the tasks_manager and server controller
controller = Controller(database, helper)
controller = Controller(database, helper, file_helper)
import3 = Import3(helper, controller)
tasks_manager = TasksManager(helper, controller)
tasks_manager.start_webserver()