Backup Restore/Root Disable

This commit is contained in:
Andrew 2021-11-29 21:22:46 +00:00
parent 798dac02a5
commit a19ba7dbb6
16 changed files with 638 additions and 78 deletions

View File

@ -51,6 +51,17 @@ class Server_Perms_Controller:
def add_role_server(server_id, role_id, rs_permissions="00000000"): def add_role_server(server_id, role_id, rs_permissions="00000000"):
return server_permissions.add_role_server(server_id, role_id, rs_permissions) return server_permissions.add_role_server(server_id, role_id, rs_permissions)
@staticmethod
def get_server_roles(server_id):
return server_permissions.get_server_roles(server_id)
@staticmethod
def backup_role_swap(old_server_id, new_server_id):
role_list = server_permissions.get_server_roles(old_server_id)
for role in role_list:
server_permissions.add_role_server(new_server_id, role.role_id, server_permissions.get_permissions_mask(int(role.role_id), int(old_server_id)))
#server_permissions.add_role_server(new_server_id, role.role_id, '00001000')
#************************************************************************************************ #************************************************************************************************
# Servers Permissions Methods # Servers Permissions Methods
#************************************************************************************************ #************************************************************************************************

View File

@ -227,6 +227,10 @@ class helpers_management:
def update_scheduled_task(schedule_id, updates): def update_scheduled_task(schedule_id, updates):
Schedules.update(updates).where(Schedules.schedule_id == schedule_id).execute() Schedules.update(updates).where(Schedules.schedule_id == schedule_id).execute()
@staticmethod
def delete_scheduled_task_by_server(server_id):
Schedules.delete().where(Schedules.server_id == int(server_id)).execute()
@staticmethod @staticmethod
def get_scheduled_task(schedule_id): def get_scheduled_task(schedule_id):
return model_to_dict(Schedules.get(Schedules.schedule_id == schedule_id)).execute() return model_to_dict(Schedules.get(Schedules.schedule_id == schedule_id)).execute()

View File

@ -118,10 +118,18 @@ class Permissions_Servers:
@staticmethod @staticmethod
def get_permissions_mask(role_id, server_id): def get_permissions_mask(role_id, server_id):
permissions_mask = '' permissions_mask = ''
role_server = Role_Servers.select().where(Role_Servers.role_id == role_id).where(Role_Servers.server_id == server_id).execute() role_server = Role_Servers.select().where(Role_Servers.role_id == role_id).where(Role_Servers.server_id == server_id).get()
permissions_mask = role_server.permissions permissions_mask = role_server.permissions
return permissions_mask return permissions_mask
@staticmethod
def get_server_roles(server_id):
role_list = []
roles = Role_Servers.select().where(Role_Servers.server_id == server_id).execute()
for role in roles:
role_list.append(role.role_id)
return role_list
@staticmethod @staticmethod
def get_role_permissions_list(role_id): def get_role_permissions_list(role_id):
permissions_mask = '00000000' permissions_mask = '00000000'

View File

@ -338,6 +338,12 @@ class Helpers:
logger.critical("Unable to write to {} - Error: {}".format(path, e)) logger.critical("Unable to write to {} - Error: {}".format(path, e))
return False return False
def checkRoot(self):
if os.geteuid() == 0:
return True
else:
return False
def unzipFile(self, zip_path): def unzipFile(self, zip_path):
new_dir_list = zip_path.split('/') new_dir_list = zip_path.split('/')
new_dir = '' new_dir = ''

View File

@ -1,13 +1,17 @@
import os import os
import pathlib
import time import time
import logging import logging
import sys import sys
from peewee import DoesNotExist
import schedule
import yaml import yaml
import asyncio import asyncio
import shutil import shutil
import tempfile import tempfile
import zipfile import zipfile
from distutils import dir_util from distutils import dir_util
from app.classes.models.management import helpers_management
from app.classes.shared.helpers import helper from app.classes.shared.helpers import helper
from app.classes.shared.console import console from app.classes.shared.console import console
@ -285,34 +289,55 @@ class Controller:
helper.ensure_dir_exists(new_server_dir) helper.ensure_dir_exists(new_server_dir)
helper.ensure_dir_exists(backup_path) helper.ensure_dir_exists(backup_path)
tempDir = tempfile.mkdtemp() tempDir = tempfile.mkdtemp()
has_properties = False
with zipfile.ZipFile(zip_path, 'r') as zip_ref: with zipfile.ZipFile(zip_path, 'r') as zip_ref:
#extracts archive to temp directory
zip_ref.extractall(tempDir) zip_ref.extractall(tempDir)
for i in range(len(zip_ref.filelist)): if len(zip_ref.filelist) > 1:
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[i].filename.endswith('/'): for item in os.listdir(tempDir):
test = zip_ref.filelist[i].filename if str(item) == 'server.properties':
break has_properties = True
path_list = test.split('/') try:
root_path = path_list[0] shutil.move(os.path.join(tempDir, item), os.path.join(new_server_dir, item))
if len(path_list) > 1: except Exception as ex:
for i in range(len(path_list)-2): logger.error('ERROR IN ZIP IMPORT: {}'.format(ex))
root_path = os.path.join(root_path, path_list[i+1]) if not has_properties:
logger.info("No server.properties found on zip file import. Creating one with port selection of {}".format(str(port)))
with open(os.path.join(new_server_dir, "server.properties"), "w") as f:
f.write("server-port={}".format(port))
f.close()
zip_ref.close()
else:
full_root_path = os.path.join(tempDir, root_path) #iterates list of files
for i in range(len(zip_ref.filelist)):
#checks if the list of files inside of a dir is greater than 1 or if it's not a directory.
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[i].is_dir():
#sets local variable to be that filename and we break out of the loop since we found our root dir.
test = zip_ref.filelist[i-1].filename
break
path_list = test.split('/')
root_path = path_list[0]
if len(path_list) > 1:
for i in range(len(path_list)-2):
root_path = os.path.join(root_path, path_list[i+1])
has_properties = False full_root_path = os.path.join(tempDir, root_path)
for item in os.listdir(full_root_path):
if str(item) == 'server.properties':
has_properties = True for item in os.listdir(full_root_path):
try: if str(item) == 'server.properties':
shutil.move(os.path.join(full_root_path, item), os.path.join(new_server_dir, item)) has_properties = True
except Exception as ex: try:
logger.error('ERROR IN ZIP IMPORT: {}'.format(ex)) shutil.move(os.path.join(full_root_path, item), os.path.join(new_server_dir, item))
if not has_properties: except Exception as ex:
logger.info("No server.properties found on zip file import. Creating one with port selection of {}".format(str(port))) logger.error('ERROR IN ZIP IMPORT: {}'.format(ex))
with open(os.path.join(new_server_dir, "server.properties"), "w") as f: if not has_properties:
f.write("server-port={}".format(port)) logger.info("No server.properties found on zip file import. Creating one with port selection of {}".format(str(port)))
f.close() with open(os.path.join(new_server_dir, "server.properties"), "w") as f:
zip_ref.close() f.write("server-port={}".format(port))
f.close()
zip_ref.close()
else: else:
return "false" return "false"
@ -328,20 +353,30 @@ class Controller:
server_log_file, server_stop, port) server_log_file, server_stop, port)
return new_id return new_id
def rename_backup_dir(self, old_server_id, new_server_id, new_uuid):
server_data = self.servers.get_server_data_by_id(old_server_id)
old_bu_path = server_data['backup_path']
Server_Perms_Controller.backup_role_swap(old_server_id, new_server_id)
backup_path = helper.validate_traversal(helper.backup_path, old_bu_path)
backup_path_components = list(backup_path.parts)
backup_path_components[-1] = new_uuid
new_bu_path = pathlib.PurePath(os.path.join(*backup_path_components))
backup_path.rename(new_bu_path)
def register_server(self, name: str, server_uuid: str, server_dir: str, backup_path: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port: int): def register_server(self, name: str, server_uuid: str, server_dir: str, backup_path: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port: int):
# put data in the db # put data in the db
new_id = self.servers.create_server(name, server_uuid, server_dir, backup_path, server_command, server_file, server_log_file, server_stop, server_port) new_id = self.servers.create_server(name, server_uuid, server_dir, backup_path, server_command, server_file, server_log_file, server_stop, server_port)
if not helper.check_file_exists(os.path.join(server_dir, "crafty_managed.txt")):
try:
# place a file in the dir saying it's owned by crafty
with open(os.path.join(server_dir, "crafty_managed.txt"), 'w') as f:
f.write(
"The server is managed by Crafty Controller.\n Leave this directory/files alone please")
f.close()
try: except Exception as e:
# place a file in the dir saying it's owned by crafty logger.error("Unable to create required server files due to :{}".format(e))
with open(os.path.join(server_dir, "crafty_managed.txt"), 'w') as f: return False
f.write(
"The server is managed by Crafty Controller.\n Leave this directory/files alone please")
f.close()
except Exception as e:
logger.error("Unable to create required server files due to :{}".format(e))
return False
# let's re-init all servers # let's re-init all servers
self.init_all_servers() self.init_all_servers()
@ -356,6 +391,7 @@ class Controller:
if int(s['server_id']) == int(server_id): if int(s['server_id']) == int(server_id):
server_data = self.get_server_data(server_id) server_data = self.get_server_data(server_id)
server_name = server_data['server_name'] server_name = server_data['server_name']
backup_dir = self.servers.get_server_data_by_id(server_id)['backup_path']
logger.info("Deleting Server: ID {} | Name: {} ".format(server_id, server_name)) logger.info("Deleting Server: ID {} | Name: {} ".format(server_id, server_name))
console.info("Deleting Server: ID {} | Name: {} ".format(server_id, server_name)) console.info("Deleting Server: ID {} | Name: {} ".format(server_id, server_name))
@ -366,7 +402,19 @@ class Controller:
if running: if running:
self.stop_server(server_id) self.stop_server(server_id)
if files: if files:
shutil.rmtree(helper.get_os_understandable_path(self.servers.get_server_data_by_id(server_id)['path'])) try:
shutil.rmtree(helper.get_os_understandable_path(self.servers.get_server_data_by_id(server_id)['path']))
except Exception as e:
logger.error("Unable to delete server files for server with ID: {} with error logged: {}".format(server_id, e))
if helper.check_path_exists(self.servers.get_server_data_by_id(server_id)['backup_path']):
shutil.rmtree(helper.get_os_understandable_path(self.servers.get_server_data_by_id(server_id)['backup_path']))
#Cleanup scheduled tasks
try:
helpers_management.delete_scheduled_task_by_server(server_id)
except DoesNotExist:
logger.info("No scheduled jobs exist. Continuing.")
# remove the server from the DB # remove the server from the DB
self.servers.remove_server(server_id) self.servers.remove_server(server_id)
@ -374,3 +422,4 @@ class Controller:
self.servers_list.pop(counter) self.servers_list.pop(counter)
counter += 1 counter += 1
return

View File

@ -243,7 +243,16 @@ class Server:
try: try:
self.process = subprocess.Popen(self.server_command, cwd=self.server_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) self.process = subprocess.Popen(self.server_command, cwd=self.server_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except Exception as ex: except Exception as ex:
msg = "Server {} failed to start with error code: {}".format(self.name, ex) #Checks for java on initial fail
if os.system("java -version") == 32512:
msg = "Server {} failed to start with error code: {}".format(self.name, "Java not found. Please install Java then try again.")
if user_id:
websocket_helper.broadcast_user(user_id, 'send_start_error',{
'error': translation.translate('error', 'noJava', user_lang).format(self.name)
})
return False
else:
msg = "Server {} failed to start with error code: {}".format(self.name, ex)
logger.error(msg) logger.error(msg)
if user_id: if user_id:
websocket_helper.broadcast_user(user_id, 'send_start_error',{ websocket_helper.broadcast_user(user_id, 'send_start_error',{
@ -512,14 +521,14 @@ class Server:
return return
def list_backups(self): def list_backups(self):
conf = management_helper.get_backup_config(self.server_id)
if self.settings['backup_path']: if self.settings['backup_path']:
if helper.check_path_exists(helper.get_os_understandable_path(self.settings['backup_path'])): if helper.check_path_exists(helper.get_os_understandable_path(self.settings['backup_path'])):
files = helper.get_human_readable_files_sizes(helper.list_dir_by_date(helper.get_os_understandable_path(self.settings['backup_path']))) files = helper.get_human_readable_files_sizes(helper.list_dir_by_date(helper.get_os_understandable_path(self.settings['backup_path'])))
return [{"path": os.path.relpath(f['path'], start=helper.get_os_understandable_path(conf['backup_path'])), "size": f["size"]} for f in files] return [{"path": os.path.relpath(f['path'], start=helper.get_os_understandable_path(self.settings['backup_path'])), "size": f["size"]} for f in files]
else: else:
return [] return []
else: else:
logger.info("Error putting backup file list for server with ID: {}".format(self.server_id))
return[] return[]
def jar_update(self): def jar_update(self):

View File

@ -212,6 +212,21 @@ class AjaxHandler(BaseHandler):
svr = self.controller.get_server_obj(server_id) svr = self.controller.get_server_obj(server_id)
svr.agree_eula(user_data['user_id']) svr.agree_eula(user_data['user_id'])
elif page == "restore_backup":
server_id = bleach.clean(self.get_argument('id', None))
zip_name = bleach.clean(self.get_argument('zip_file', None))
svr_obj = self.controller.servers.get_server_obj(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
backup_path = svr_obj.backup_path
if helper.validate_traversal(backup_path, zip_name):
new_server = self.controller.import_zip_server(svr_obj.server_name, os.path.join(backup_path, zip_name), server_data['executable'], '1', '2', server_data['server_port'])
new_server_id = new_server
new_server = self.controller.get_server_data(new_server)
self.controller.rename_backup_dir(server_id, new_server_id, new_server['server_uuid'])
self.controller.remove_server(server_id, True)
self.redirect('/panel/dashboard')
@tornado.web.authenticated @tornado.web.authenticated
def delete(self, page): def delete(self, page):
if page == "del_file": if page == "del_file":

View File

@ -121,14 +121,23 @@ class PanelHandler(BaseHandler):
elif page == 'dashboard': elif page == 'dashboard':
if exec_user['superuser'] == 1: if exec_user['superuser'] == 1:
page_data['servers'] = self.controller.servers.get_all_servers_stats() try:
page_data['servers'] = self.controller.servers.get_all_servers_stats()
except IndexError:
self.controller.stats.record_stats()
page_data['servers'] = self.controller.servers.get_all_servers_stats()
for data in page_data['servers']: for data in page_data['servers']:
try: try:
data['stats']['waiting_start'] = self.controller.servers.get_waiting_start(int(data['stats']['server_id']['server_id'])) data['stats']['waiting_start'] = self.controller.servers.get_waiting_start(int(data['stats']['server_id']['server_id']))
except: except:
data['stats']['waiting_start'] = False data['stats']['waiting_start'] = False
else: else:
user_auth = self.controller.servers.get_authorized_servers_stats(exec_user_id) try:
user_auth = self.controller.servers.get_authorized_servers_stats(exec_user_id)
except IndexError:
self.controller.stats.record_stats()
user_auth = self.controller.servers.get_authorized_servers_stats(exec_user_id)
logger.debug("ASFR: {}".format(user_auth)) logger.debug("ASFR: {}".format(user_auth))
page_data['servers'] = user_auth page_data['servers'] = user_auth
page_data['server_stats']['running'] = 0 page_data['server_stats']['running'] = 0
@ -208,9 +217,12 @@ class PanelHandler(BaseHandler):
if subpage == "backup": if subpage == "backup":
server_info = self.controller.servers.get_server_data_by_id(server_id) server_info = self.controller.servers.get_server_data_by_id(server_id)
page_data['backup_config'] = self.controller.management.get_backup_config(server_id) page_data['backup_config'] = self.controller.management.get_backup_config(server_id)
page_data['backup_list'] = server.list_backups() self.controller.refresh_server_settings(server_id)
try:
page_data['backup_list'] = server.list_backups()
except:
page_data['backup_list'] = []
page_data['backup_path'] = helper.wtol_path(server_info["backup_path"]) page_data['backup_path'] = helper.wtol_path(server_info["backup_path"])
print(page_data['backup_path'])
def get_banned_players_html(): def get_banned_players_html():
banned_players = self.controller.servers.get_banned_players(server_id) banned_players = self.controller.servers.get_banned_players(server_id)
@ -416,8 +428,6 @@ class PanelHandler(BaseHandler):
return return
elif Enum_Permissions_Crafty.User_Config not in exec_user_crafty_permissions: elif Enum_Permissions_Crafty.User_Config not in exec_user_crafty_permissions:
if str(user_id) != str(exec_user_id): if str(user_id) != str(exec_user_id):
print("USER ID ", user_id)
print("EXEC ID ", exec_user_id)
self.redirect("/panel/error?error=Unauthorized access: not a user editor") self.redirect("/panel/error?error=Unauthorized access: not a user editor")
return return

View File

@ -45,8 +45,10 @@
<a href="/panel/backup_now?id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-primary" onclick="backup_started()">{{ translate('serverBackups', 'backupNow', data['lang']) }}</a> <a href="/panel/backup_now?id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-primary" onclick="backup_started()">{{ translate('serverBackups', 'backupNow', data['lang']) }}</a>
</div> </div>
<div class="form-group"> <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> <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']) }}" > <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>
<div class="form-group"> <div class="form-group">
@ -96,10 +98,14 @@
</a> </a>
<br> <br>
<br> <br>
<button data-file="{{ backup['path'] }}" class="btn btn-danger del_button"> <button data-file="{{ backup['path'] }}" class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }} {{ translate('serverBackups', 'delete', data['lang']) }}
</button> </button>
<button data-file="{{ backup['path'] }}" class="btn btn-warning restore_button">
<i class="fas fa-undo-alt" aria-hidden="true"></i>
{{ translate('serverBackups', 'restore', data['lang']) }}
</button>
</td> </td>
<td>{{ backup['path'] }}</td> <td>{{ backup['path'] }}</td>
<td>{{ backup['size'] }}</td> <td>{{ backup['size'] }}</td>
@ -162,6 +168,31 @@
}); });
} }
function restore_backup(filename, id){
var token = getCookie("_xsrf")
console.log('Sending Command to restore backup: ' + filename)
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/restore_backup?server_id='+id,
data: {
zip_file: filename,
id: id
},
success: function(data) {
var dialog = bootbox.dialog({
message: '<i class="fa fa-spin fa-spinner"></i> {{ translate('serverBackups', 'restoring', data['lang']) }}',
closeButton: false
});
setTimeout(function(){
location.href=('/panel/dashboard');
}, 15000);
},
});
}
$( document ).ready(function() { $( document ).ready(function() {
console.log( "ready!" ); console.log( "ready!" );
$("#backup_config_box").hide(); $("#backup_config_box").hide();
@ -210,9 +241,33 @@
} }
}); });
}); });
$( ".restore_button" ).click(function() {
var file_to_restore = $(this).data("file");
bootbox.confirm({
title: "{{ translate('serverBackups', 'restore', data['lang']) }} "+file_to_restore,
message: "{{ translate('serverBackups', 'confirmRestore', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "restore", data['lang']) }}'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
restore_backup(file_to_restore, {{ data['server_stats']['server_id']['server_id'] }} );
}
}
});
}); });
});
</script> </script>

View File

@ -236,23 +236,32 @@ let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
function deleteServer (){ function deleteServer (){
path = "{{data['server_stats']['server_id']['path']}}"; path = "{{data['server_stats']['server_id']['path']}}";
name = "{{data['server_stats']['server_id']['server_name']}}"; name = "{{data['server_stats']['server_id']['server_name']}}";
bootbox.confirm({ bootbox.dialog({
size: "", size: "",
title: "{% raw translate('serverConfig', 'deleteFilesQuestion', data['lang']) %}", title: "{% raw translate('serverConfig', 'deleteFilesQuestion', data['lang']) %}",
closeButton: false, closeButton: false,
message: "{% raw translate('serverConfig', 'deleteFilesQuestionMessage', data['lang']) %}", message: "{% raw translate('serverConfig', 'deleteFilesQuestionMessage', data['lang']) %}",
buttons: { buttons: {
confirm: { files: {
label: "{{ translate('serverConfig', 'yesDeleteFiles', data['lang']) }}", label: "{{ translate('serverConfig', 'yesDeleteFiles', data['lang']) }}",
className: 'btn-danger', className: 'btn-danger',
}, callback: function(){
cancel: { deleteServerFilesE();
label: "{{ translate('serverConfig', 'noDeleteFiles', data['lang']) }}", setTimeout(function(){ window.location = '/panel/dashboard'; }, 5000);
className: 'btn-link', bootbox.dialog({
backdrop: true,
title: '{% raw translate("serverConfig", "sendingDelete", data['lang']) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDeleteFiles", data['lang']) %} </div>',
closeButton: false
})
return;
} }
}, },
callback: function(result) { noFiles: {
if (!result){ label: "{{ translate('serverConfig', 'noDeleteFiles', data['lang']) }}",
className: 'btn-outline-danger',
callback: function(){
deleteServerE() deleteServerE()
setTimeout(function(){ window.location = '/panel/dashboard'; }, 5000); setTimeout(function(){ window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({ bootbox.dialog({
@ -261,18 +270,18 @@ let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDelete", data['lang']) %} </div>', message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDelete", data['lang']) %} </div>',
closeButton: false closeButton: false
}) })
return;
return;} }
else{ },
deleteServerFilesE(); cancel: {
setTimeout(function(){ window.location = '/panel/dashboard'; }, 5000); label: "{{ translate('serverConfig', 'cancel', data['lang']) }}",
bootbox.dialog({ className: 'btn-secondary',
backdrop: true, callback: function(){
title: '{% raw translate("serverConfig", "sendingDelete", data['lang']) %}', return;
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDeleteFiles", data['lang']) %} </div>', }
closeButton: false }
}) },
} callback: function(result) {
} }
}); });

View File

@ -16,7 +16,8 @@
"internet": "We have detected the machine running Crafty has no connection to the internet. Client connections to the server may be limited.", "internet": "We have detected the machine running Crafty has no connection to the internet. Client connections to the server may be limited.",
"eulaTitle": "Agree To EULA", "eulaTitle": "Agree To EULA",
"eulaMsg": "You must agree to the EULA. A copy of the Mojang EULA is linked under this message.", "eulaMsg": "You must agree to the EULA. A copy of the Mojang EULA is linked under this message.",
"eulaAgree": "Do you agree?" "eulaAgree": "Do you agree?",
"noJava": "Server {} failed to start with error code: We have detected Java is not installed. Please install java then start the server."
}, },
"404": { "404": {
"contact": "Contact Crafty Control Support via Discord", "contact": "Contact Crafty Control Support via Discord",
@ -178,7 +179,10 @@
"destroyBackup": "Destroy backup \" + file_to_del + \"?", "destroyBackup": "Destroy backup \" + file_to_del + \"?",
"confirmDelete": "Do you want to delete this backup? This cannot be undone.", "confirmDelete": "Do you want to delete this backup? This cannot be undone.",
"confirm": "Confirm", "confirm": "Confirm",
"options": "Options" "options": "Options",
"restoring": "Restoring Backup. This may take a while. Please be patient.",
"restore": "Restore",
"confirmRestore": "Are you sure you want to restore from this backup. All current server files will changed to backup state and will be unrecoverable."
}, },
"serverFiles": { "serverFiles": {
"noscript": "The file manager does not work without JavaScript", "noscript": "The file manager does not work without JavaScript",
@ -247,7 +251,7 @@
"yesDelete": "Yes, delete", "yesDelete": "Yes, delete",
"noDelete": "No, go back", "noDelete": "No, go back",
"deleteFilesQuestion": "Delete server files from machine?", "deleteFilesQuestion": "Delete server files from machine?",
"deleteFilesQuestionMessage": "Would you like Crafty to delete all server files from the host machine?", "deleteFilesQuestionMessage": "Would you like Crafty to delete all server files from the host machine? <br><br><strong>This includes server backups.</strong>",
"yesDeleteFiles": "Yes, delete files", "yesDeleteFiles": "Yes, delete files",
"noDeleteFiles": "No, just remove from panel", "noDeleteFiles": "No, just remove from panel",
"sendingDelete": "Deleting Server", "sendingDelete": "Deleting Server",

View File

@ -16,7 +16,8 @@
"internet": "Olemme havainneet, että Crafty -koneella ei ole Internet -yhteyttä. Asiakasyhteydet palvelimelle voivat olla rajalliset.", "internet": "Olemme havainneet, että Crafty -koneella ei ole Internet -yhteyttä. Asiakasyhteydet palvelimelle voivat olla rajalliset.",
"eulaTitle": "Hyväksy EULA", "eulaTitle": "Hyväksy EULA",
"eulaMsg": "Sinun on hyväksyttävä EULA. Kopio Mojang EULA:sta on linkitetty tämän viestin alla.", "eulaMsg": "Sinun on hyväksyttävä EULA. Kopio Mojang EULA:sta on linkitetty tämän viestin alla.",
"eulaAgree": "Oletko samaa mieltä?" "eulaAgree": "Oletko samaa mieltä?",
"noJava": "Server {} failed to start with error code: We have detected Java is not installed. Please install java then start the server."
}, },
"404": { "404": {
"contact": "Ota yhteyttä Crafty Control -tukeen Discordin kautta", "contact": "Ota yhteyttä Crafty Control -tukeen Discordin kautta",
@ -179,7 +180,10 @@
"destroyBackup": "Tuhotaanko varmuuskopio \" + file_to_del + \"?", "destroyBackup": "Tuhotaanko varmuuskopio \" + file_to_del + \"?",
"confirmDelete": "Haluatko poistaa tämän varmuuskopion? Tätä ei voi peruuttaa.", "confirmDelete": "Haluatko poistaa tämän varmuuskopion? Tätä ei voi peruuttaa.",
"confirm": "Vahvista", "confirm": "Vahvista",
"options": "Vaihtoehtoja" "options": "Vaihtoehtoja",
"restoring": "Varmuuskopion palauttaminen. Tämä voi kestää hetken. Olkaa kärsivällisiä.",
"restore": "Palauttaa",
"confirmRestore": "Haluatko varmasti palauttaa tämän varmuuskopion. Kaikki nykyiset palvelintiedostot muutetaan varmuuskopiotilaan, eikä niitä voida palauttaa."
}, },
"serverFiles": { "serverFiles": {
"noscript": "Tiedostojenhallinta ei toimi ilman JavaScriptiä", "noscript": "Tiedostojenhallinta ei toimi ilman JavaScriptiä",
@ -237,7 +241,23 @@
"save": "Tallenna", "save": "Tallenna",
"cancel": "Peruuta", "cancel": "Peruuta",
"deleteServer": "Poista palvelin", "deleteServer": "Poista palvelin",
"stopBeforeDeleting": "Pysäytä palvelin ennen sen poistamista" "stopBeforeDeleting": "Pysäytä palvelin ennen sen poistamista",
"exeUpdateURLDesc": "Direct Download URL for updates.",
"exeUpdateURL": "Palvelimen suoritettavan päivityksen URL-osoite",
"update": "Päivitä suoritettava",
"bePatientUpdate": "Ole kärsivällinen, kun päivitämme palvelinta. Latausajat voivat vaihdella Internet-nopeutesi mukaan.<br /> Tämä näyttö päivittyy hetkessä",
"sendingRequest": "Pyyntöäsi lähetetään...",
"deleteServerQuestion": "Poistetaanko palvelin?",
"deleteServerQuestionMessage": "Haluatko varmasti poistaa tämän palvelimen? Tämän jälkeen ei ole paluuta...",
"yesDelete": "Kyllä, poista",
"noDelete": "Ei, mene takaisin",
"deleteFilesQuestion": "Poistetaanko palvelintiedostot koneelta?",
"deleteFilesQuestionMessage": "Haluatko Craftyn poistavan kaikki palvelintiedostot isäntäkoneelta? <br><br><strong> Tämä sisältää palvelimen varmuuskopiot. <strong>",
"yesDeleteFiles": "Kyllä, poista tiedostoja",
"noDeleteFiles": "Ei, poista vain paneelista",
"sendingDelete": "Poistetaan palvelinta",
"bePatientDelete": "Ole kärsivällinen, kun poistamme palvelimesi Crafty-paneelista. Tämä näyttö sulkeutuu hetken kuluttua.",
"bePatientDeleteFiles" : "Ole kärsivällinen, kun poistamme palvelimesi Crafty-paneelista ja poistamme kaikki tiedostot. Tämä näyttö sulkeutuu hetken kuluttua."
}, },
"serverConfigHelp": { "serverConfigHelp": {
"title": "Palvelimen asetukset", "title": "Palvelimen asetukset",

View File

@ -16,7 +16,8 @@
"internet": "Nous avons détecté que la machine exécutant Crafty n'a pas de connexion à Internet. Les connexions client au serveur peuvent être limitées.", "internet": "Nous avons détecté que la machine exécutant Crafty n'a pas de connexion à Internet. Les connexions client au serveur peuvent être limitées.",
"eulaTitle": "Accepter le EULA", "eulaTitle": "Accepter le EULA",
"eulaMsg": "Vous devez accepter le EULA. Une copie du CLUF de Mojang est liée sous ce message.", "eulaMsg": "Vous devez accepter le EULA. Une copie du CLUF de Mojang est liée sous ce message.",
"eulaAgree": "Êtes-vous d'accord?" "eulaAgree": "Êtes-vous d'accord?",
"noJava": "Server {} failed to start with error code: We have detected Java is not installed. Please install java then start the server."
}, },
"404": { "404": {
"contact": "Contacter le Support de Crafty Control via Discord", "contact": "Contacter le Support de Crafty Control via Discord",
@ -178,7 +179,10 @@
"destroyBackup": "Supprimer la sauvegarde \" + file_to_del + \" ?", "destroyBackup": "Supprimer la sauvegarde \" + file_to_del + \" ?",
"confirmDelete": "Es-tu sûr de vouloir supprimer cette sauvegarde ? Tu ne pourras pas revenir en arrière.", "confirmDelete": "Es-tu sûr de vouloir supprimer cette sauvegarde ? Tu ne pourras pas revenir en arrière.",
"confirm": "Confirmer", "confirm": "Confirmer",
"options": "Options" "options": "Options",
"restoring": "Restauration de la sauvegarde. Cela peut prendre un peu de temps. S'il vous plaît soyez patient.",
"restore": "Restaurer",
"restoreConfirm": "Êtes-vous sûr de vouloir restaurer à partir de cette sauvegarde. Tous les fichiers du serveur actuel passeront à l'état de sauvegarde et seront irrécupérables."
}, },
"serverFiles": { "serverFiles": {
"noscript": "Le gestionnaire de fichiers ne fonctionne pas sans JavaScript", "noscript": "Le gestionnaire de fichiers ne fonctionne pas sans JavaScript",
@ -247,7 +251,7 @@
"yesDelete": "Oui, Supprimer", "yesDelete": "Oui, Supprimer",
"noDelete": "Non, revenir en arrière", "noDelete": "Non, revenir en arrière",
"deleteFilesQuestion": "Supprimer les fichiers de la machine ?", "deleteFilesQuestion": "Supprimer les fichiers de la machine ?",
"deleteFilesQuestionMessage": "Veux-tu que Crafty supprime tous les fichiers du serveur de la machine hôte ?", "deleteFilesQuestionMessage": "Veux-tu que Crafty supprime tous les fichiers du serveur de la machine hôte? <br><br><strong>Cela inclut les sauvegardes du serveur.</strong>",
"yesDeleteFiles": "Oui, Supprimer les fichier", "yesDeleteFiles": "Oui, Supprimer les fichier",
"noDeleteFiles": "Non, Supprimer uniquement du tabelau de bord", "noDeleteFiles": "Non, Supprimer uniquement du tabelau de bord",
"sendingDelete": "Suppression du Serveur", "sendingDelete": "Suppression du Serveur",

346
app/translations/zh_CN.json Normal file
View File

@ -0,0 +1,346 @@
{
"login": {
"forgotPassword": "忘记密码",
"login": "登录",
"password": "密码",
"username": "用户名"
},
"error": {
"hereIsTheError": "错误如下",
"contact": "通过 Discord 联系 Crafty Control 支持",
"terribleFailure": "多糟糕的错误!",
"embarassing": "哦,天哪,这太尴尬了。",
"error": "错误!",
"start-error": "服务器 {} 启动失败,错误代码为:{}",
"closedPort": "我们检测到端口 {} 在主机上可能没有打开,或者被防火墙阻断了。远程客户端到服务器的连接可能受限。",
"internet": "我们检测到运行 Crafty 的设备没有网络连接。客户端到服务器的连接可能受限。",
"eulaTitle": "同意最终用户许可协议EULA",
"eulaMsg": "你必须同意最终用户许可协议EULA。一份 Mojang EULA 副本的链接在此消息下方。",
"eulaAgree": "你同意吗?"
},
"404": {
"contact": "通过 Discord 联系 Crafty Control 支持",
"unableToFind": "我们无法找到您想要查看的页面。请再试一次,或者返回上一页并刷新。",
"notFound": "页面未找到"
},
"footer": {
"version": "版本",
"copyright": "版权",
"allRightsReserved": "保留所有权利"
},
"sidebar": {
"dashboard": "仪表板",
"servers": "服务器",
"documentation": "文档",
"credits": "鸣谢",
"contribute": "贡献",
"newServer": "创建新服务器",
"navigation": "导航栏"
},
"serverWizard": {
"newServer": "创建新服务器",
"importServer": "导入现有服务器",
"importZip": "从 Zip 文件导入",
"serverName": "服务器名称",
"serverPath": "服务器路径",
"serverType": "服务器类型",
"selectType": "选择一种类型",
"serverVersion": "服务器版本",
"selectVersion": "选择一个版本",
"absoluteServerPath": "您的服务器的绝对路径",
"serverJar": "服务器 Jar 核心",
"minMem": "最小内存",
"maxMem": "最大内存",
"serverPort": "服务器端口",
"defaultPort": "默认值为 25565",
"sizeInGB": "大小(以 GB 为单位)",
"zipPath": "服务器路径",
"absoluteZipPath": "您的服务器的绝对路径",
"resetForm": "重置表单",
"importServerButton": "导入服务器!",
"buildServer": "建立服务器!",
"quickSettings": "快捷设置",
"quickSettingsDescription": "别担心,你可以稍后再更改这些设置",
"myNewServer": "我的新服务器",
"bePatient": "请耐心等待,我们正在 ' + (importing ? '导入' : '下载') + ' 服务器",
"importing": "导入服务器中……",
"downloading": "下载服务器中……",
"addRole": "将服务器添加到现有角色",
"autoCreate": "如果没有选择任何角色Crafty 将会为您创建一个!",
"selectRole": "选择角色"
},
"dashboard": {
"dashboard": "仪表板",
"memUsage": "内存使用量",
"cpuUsage": "CPU 使用量",
"host": "主机",
"players": "玩家",
"backups": "备份",
"newServer": "创建新服务器",
"allServers": "所有服务器",
"server": "服务器",
"actions": "操作",
"world": "世界",
"motd": "今日消息MOTD",
"version": "版本",
"status": "状态",
"online": "在线",
"offline": "离线",
"lastBackup": "上次:",
"nextBackup": "下次:",
"servers": "服务器",
"cannotSeeOnMobile": "在移动设备上什么都看不到?",
"cannotSee": "什么都看不到?",
"cannotSeeOnMobile2": "尝试横向滚动表格。",
"max": "最大",
"avg": "平均",
"bePatientStart": "请耐心等待,我们正在启动服务器。<br /> 稍后此页面会刷新",
"bePatientStop": "请耐心等待,我们正在停止服务器。<br /> 稍后此页面会刷新",
"bePatientRestart": "请耐心等待,我们正在重启服务器。<br /> 稍后此页面会刷新",
"bePatientClone": "请耐心等待,我们正在克隆服务器。<br /> 稍后此页面会刷新",
"sendingCommand": "正在发送您的指令",
"cpuCurFreq": "当前 CPU 时钟",
"cpuMaxFreq": "最大 CPU 时钟",
"cpuCores": "CPU 核心",
"start": "启动",
"stop": "停止",
"clone": "克隆",
"kill": "杀死进程",
"restart": "重启",
"killing": "正在杀死进程……",
"starting": "延迟启动",
"delay-explained": "服务进程已经在刚才启动,并且正在延迟 Minecraft 服务器实例的启动",
"no-servers": "当前没有服务器。要开始,请点击",
"welcome": "欢迎来到 Crafty Controller"
},
"accessDenied": {
"accessDenied": "拒绝访问",
"noAccess": "您没有权限访问此资源",
"contactAdmin": "联系您的服务器管理员来获得访问此资源的权限,或者如果您认为您应该有权限访问此资源,请联系支持。",
"contact": "通过 Discord 联系 Crafty Control 支持"
},
"serverStats": {
"online": "运行中",
"offline": "已停止",
"serverStatus": "服务器状态",
"serverStarted": "服务器已启动",
"serverUptime": "服务器正常运行时间",
"players": "玩家",
"memUsage": "内存使用量",
"cpuUsage": "CPU 使用量",
"version": "版本",
"description": "简介",
"errorCalculatingUptime": "计算正常运行时间时发生错误",
"serverTime": "UTC 时间",
"unableToConnect": "无法连接"
},
"serverDetails": {
"serverDetails": "服务器详情",
"terminal": "终端",
"logs": "日志",
"schedule": "计划",
"backup": "备份",
"files": "文件",
"config": "配置",
"playerControls": "玩家管理"
},
"serverTerm": {
"stopScroll": "停止自动滚动",
"commandInput": "输入您的指令",
"sendCommand": "发送指令",
"start": "启动",
"restart": "重启",
"stop": "停止",
"updating": "更新中……",
"starting": "延迟启动",
"delay-explained": "服务进程已经在刚才启动,并且正在延迟 Minecraft 服务器实例的启动"
},
"serverPlayerManagement": {
"players": "玩家",
"bannedPlayers": "已封禁的玩家",
"loadingBannedPlayers": "正在加载已封禁的玩家"
},
"serverBackups": {
"backupNow": "现在备份!",
"backupAtMidnight": "午夜自动备份?",
"storageLocation": "存储位置",
"storageLocationDesc": "您想要在哪里存储备份?",
"maxBackups": "最大备份数量",
"maxBackupsDesc": "Crafty 不会存储多于 N 个备份,并且会删除最旧的备份(输入 0 以保留所有备份)",
"save": "保存",
"cancel": "取消",
"currentBackups": "现有备份",
"download": "下载",
"path": "路径",
"size": "大小",
"delete": "删除",
"backupTask": "一个备份任务已开始。",
"destroyBackup": "删除备份 \" + file_to_del + \"",
"confirmDelete": "您想要删除这个备份吗?此操作不能撤销。",
"confirm": "确认",
"options": "选项"
},
"serverFiles": {
"noscript": "文件管理器无法在没有 JavaScript 的情况下使用",
"error": "获取文件时发生错误",
"files": "文件",
"default": "默认",
"save": "保存",
"editingFile": "正在编辑文件",
"delete": "删除",
"createFile": "创建文件",
"createDir": "创建目录",
"rename": "重命名",
"createFileQuestion": "您希望新文件叫什么名字?",
"createDirQuestion": "您希望新目录叫什么名字?",
"renameItemQuestion": "新名称应当是什么?",
"deleteItemQuestion": "您确定要删除 \" + name + \" 吗?",
"deleteItemQuestionMessage": "您正在删除 \\\"\" + path + \"\\\"<br/><br/>此操作不可逆转,文件将永远遗失!",
"yesDelete": "是,我知道结果",
"noDelete": "否",
"unsupportedLanguage": "警告:这不是一个受支持的文件类型",
"keybindings": "按键绑定",
"fileReadError": "文件读取错误",
"upload": "上传",
"unzip": "解压",
"clickUpload": "点击这里来选择您的文件",
"uploadTitle": "上传文件到:",
"waitUpload": "请等待,我们正在上传您的文件……这需要一点时间。",
"stayHere": "请不要离开此页面!",
"close": "关闭",
"download": "下载"
},
"serverConfig": {
"serverName": "服务器名称",
"serverNameDesc": "您希望把这个服务器叫做什么",
"serverPath": "服务器运行目录",
"serverPathDesc": "完整绝对路径(不包含可执行文件)",
"serverLogLocation": "服务器日志路径",
"serverLogLocationDesc": "日志文件的完整绝对路径",
"serverExecutable": "服务器可执行文件",
"serverExecutableDesc": "服务器的可执行文件",
"serverExecutionCommand": "服务器运行命令",
"serverExecutionCommandDesc": "在隐藏的终端内要如何启动服务器",
"serverStopCommand": "服务器停止指令",
"serverStopCommandDesc": "要发送给程序以关闭它的指令",
"serverAutostartDelay": "服务器自动启动延迟",
"serverAutostartDelayDesc": "自动启动前的延迟(如果已在下方启用)",
"serverIP": "服务器 IP",
"serverIPDesc": "Crafty 要连接以获取状态的 IP如果遇到问题尝试使用真实 IP 而非 127.0.0.1",
"serverPort": "服务器端口",
"serverPortDesc": "Crafty 要连接以获取状态的端口",
"removeOldLogsAfter": "此时间后删除旧日志",
"removeOldLogsAfterDesc": "日志文件要在多少天后视为旧文件被删除0 为关闭)",
"serverAutoStart": "服务器自动启动",
"serverCrashDetection": "服务器崩溃检测",
"save": "保存",
"cancel": "取消",
"deleteServer": "删除服务器",
"stopBeforeDeleting": "请在删除之前停止服务器",
"exeUpdateURLDesc": "用于下载更新的直接链接。",
"exeUpdateURL": "服务器可执行文件更新地址",
"update": "更新可执行文件",
"bePatientUpdate": "请耐心等待,我们正在更新服务器。下载时长可能因您的网络速度而异。<br /> 稍后此页面会刷新",
"sendingRequest": "正在发送您的请求……",
"deleteServerQuestion": "删除服务器?",
"deleteServerQuestionMessage": "您确定要删除此服务器吗?在此之后将无法撤销……",
"yesDelete": "是,删除",
"noDelete": "否,返回",
"deleteFilesQuestion": "从设备上删除服务器文件?",
"deleteFilesQuestionMessage": "您想要 Crafty 从主机上删除所有的服务器文件吗?",
"yesDeleteFiles": "是,删除文件",
"noDeleteFiles": "否,只从面板中移除",
"sendingDelete": "正在删除服务器",
"bePatientDelete": "请耐心等待,我们正在从 Crafty 面板中移除服务器。稍后此页面会关闭。",
"bePatientDeleteFiles" : "请耐心等待,我们正在从 Crafty 面板中移除服务器并删除所有文件。稍后此页面会关闭。"
},
"serverConfigHelp": {
"title": "服务器配置区",
"desc": "您可以在这里更改您的服务器配置",
"perms": [
"我们<code>不推荐</code>更改由 Crafty 管理的服务器的路径。",
"更改路径<code>可能会</code>破坏一些东西,尤其是在 Linux 这类文件权限锁定得更加严格的操作系统上。",
"<br /><br/>",
"如果您认为您必须更改服务器存放的位置,你可能需要给予 \"crafty\" 用户对服务器路径的读取/写入权限。",
"<br />",
"<br />",
"在 Linux 上,最好通过执行如下命令来完成:<br />",
"<code>",
" sudo chown crafty:crafty /您的/服务器/路径 -R<br />",
" sudo chmod 2775 /您的/服务器/路径 -R<br />",
"</code>"
]
},
"panelConfig": {
"save": "保存",
"cancel": "取消",
"delete": "删除"
},
"datatables": {
"i18n": {
"decimal": "",
"emptyTable": "数据表中没有可用的数据",
"info": "正在显示从 _START_ 到 _END_ 的共 _TOTAL_ 个项目",
"infoEmpty": "正在显示从 0 到 0 的共 0 个项目",
"infoFiltered": "(从 _MAX_ 个项目中筛选出)",
"infoPostFix": "",
"thousands": ",",
"lengthMenu": "显示 _MENU_ 个项目",
"loadingRecords": "正在加载……",
"processing": "正在处理……",
"search": "搜索:",
"zeroRecords": "没有找到匹配的记录",
"paginate": {
"first": "首页",
"last": "末页",
"next": "下一页",
"previous": "上一页"
},
"aria": {
"sortAscending": ":激活对队列的升序排列",
"sortDescending": ":激活对队列的降序排列"
},
"buttons": {
"collection": "合集 <span class='ui-button-icon-primary ui-icon ui-icon-triangle-1-s'\/>",
"colvis": "列可见性",
"colvisRestore": "恢复可见性",
"copy": "复制",
"copyKeys": "按 ctrl 或 u2318 + C 以复制表中的数据到您的系统剪贴板。<br><br>点击这条消息或者按 escapeESC来取消。",
"copySuccess": {
"1": "复制了 1 行到剪贴板",
"_": "复制了 %d 行到剪贴板"
},
"copyTitle": "复制到剪贴板",
"csv": "CSV",
"excel": "Excel",
"pageLength": {
"-1": "显示所有行",
"1": "显示 1 行",
"_": "显示 %d 行"
},
"pdf": "PDF",
"print": "打印"
},
"select": {
"rows": {
"0": "点击某一行以选择",
"1": "%d 行已选中",
"_": "%d 行已选中"
},
"cells": {
"0": "点击某个单元格以选择",
"1": "%d 个单元格已选中",
"_": "%d 个单元格已选中"
},
"columns": {
"0": "点击某一列以选择",
"1": "%d 列已选中",
"_": "%d 列已选中"
}
}
}
},
"base": {
"doesNotWorkWithoutJavascript": "<strong>警告:</strong>Crafty 无法在没有 JavaScript 的情况下使用!"
}
}

11
main.py
View File

@ -1,3 +1,4 @@
from cmd import Cmd
import os import os
import sys import sys
import json import json
@ -78,13 +79,21 @@ if __name__ == '__main__':
args = parser.parse_args() args = parser.parse_args()
if helper.check_file_exists('/.dockerenv'):
console.cyan("Docker environment detected!")
else:
if helper.checkRoot():
console.critical("Root detected. Root/Admin access denied. Run Crafty again with non-elevated permissions.")
time.sleep(5)
console.critical("Crafty shutting down. Root/Admin access denied.")
sys.exit(0)
helper.ensure_logging_setup() helper.ensure_logging_setup()
setup_logging(debug=args.verbose) setup_logging(debug=args.verbose)
# setting up the logger object # setting up the logger object
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
print("Logging set to: {} ".format(logger.level)) console.cyan("Logging set to: {} ".format(logger.level))
# print our pretty start message # print our pretty start message
do_intro() do_intro()

View File

@ -1,7 +1,7 @@
cryptography~=3.4
argon2-cffi~=20.1 argon2-cffi~=20.1
bleach~=3.1 bleach~=3.1
colorama~=0.4 colorama~=0.4
cryptography~=3.4
peewee~=3.13 peewee~=3.13
pexpect~=4.8 pexpect~=4.8
psutil~=5.7 psutil~=5.7
@ -11,3 +11,4 @@ requests~=2.26
schedule~=1.1.0 schedule~=1.1.0
termcolor~=1.1 termcolor~=1.1
tornado~=6.0 tornado~=6.0
cached_property==1.5.2