def list_instances(filter_str: str = '') -> None: """Print a list of all instances. Output a table of all instances with their respective Name, Server Version String, Status and persistence. Keyword Arguments: filter_str (str): Filter the list by instance name. (default: {''}) """ base_path = storage.get_instance_path(bare=True) server_paths = base_path.iterdir() servers = [x.name for x in server_paths] name_col_width = str(len(max(servers, key=len)) + 1) template = "{:" + name_col_width + "} {:<6} {:20} {:14} {:10} {:10}" title = template.format("Name", "Port", "Server Version", "Player Count", "Status", "Persistent") print(title) for name in servers: if filter_str in name: cfg = config.get_properties(base_path / name / "server.properties") port = int(cfg.get("server-port")) server = MinecraftServer('localhost', port) status_info = status.get_simple_status(server) unit = service.get_unit(name) state = get_online_state(unit, status_info.get("proto")) player_ratio = f"{status_info.get('online')}/{cfg.get('max-players')}" contents = template.format(name, port, status_info.get("version"), player_ratio, state.capitalize(), str(service.is_enabled(unit))) print(contents)
def export(instance: str, zip_path: Path = None, compress: bool = False, world_only: bool = False) -> Path: """Export a minecraft server instance to a Zip-File. Export a minecraft server instance to a Zip-File for archiving or similar. Optionally, the File can also be compressed and all config Files can be excluded. Arguments: instance (str): The name of the Instance to be exported. Keyword Arguments: zip_path (Path): The path of the Zip-File that is generated. (default: {None}) compress (bool): True: Compress the Zip-File using ZIP_DEFLATE. False: Use ZIP_STORE (default: {False}) world_only (bool): Only export the World data without configuration files. (default: {False}) Returns: Path: The Path where the Zip-File was saved to. """ if not zip_path: zip_path = Path( f"{instance}_{datetime.now().strftime('%y-%m-%d-%H.%M.%S')}.zip") server_path = get_instance_path(instance) filter_str = "" if world_only: server_cfg = config.get_properties(server_path / "server.properties") filter_str = server_cfg.get("level-name", "world") file_list = get_relative_paths(server_path, filter_str) total_size = sum((server_path / x).stat().st_size for x in file_list) compress_mode = zf.ZIP_DEFLATED if compress else zf.ZIP_STORED with perms.run_as(0, 0), zf.ZipFile(zip_path, "w", compression=compress_mode, allowZip64=True) as zip_file: written = 0 for file_path in file_list: full_path = server_path / file_path written += full_path.stat().st_size msg = f"\r[{(written * 100 / total_size):3.0f}%] Writing: {file_path}...\033[K" print(msg, end='', flush=True) zip_file.write(full_path, file_path) print() try: login_name = os.getlogin() except FileNotFoundError: login_name = None print("WARN: Unable to retrieve Login Name.") if login_name: with perms.run_as(0, 0): chown(zip_path, login_name) print(f"Archive saved in '{zip_path}'.") return zip_path
def mc_import(zip_path: Path, instance: str = None, world_only: bool = False) -> None: """Import a minecraft server instance from a Zip-File. Import a minecraft server instance from a Zip-File. Also, the world files can be extracted separately. Arguments: zip_path (Path): The path of the Zip-File to import from. (default: {None}) Keyword Arguments: instance (str): The name of the Instance to be imported. Auto-generated from archive name if None. (default: {None}) world_only (bool): Only import the World data without configuration files. (default: {False}) """ if instance is None: instance = zip_path.stem print(f"No Instance specified, importing to instance '{instance}'.") instance_path = get_instance_path(instance) if instance_path.exists() != world_only: if world_only: raise FileNotFoundError( f"Instance Path not found: {instance_path}.") raise FileExistsError("Instance already exists.") if not instance_path.exists(): create_dirs(instance_path) filter_str = "" with zf.ZipFile(zip_path) as zip_file: if world_only: with zip_file.open("server.properties") as prop, tmpf.NamedTemporaryFile() as tmp_file: shutil.copyfileobj(prop, tmp_file) properties = config.get_properties(tmp_file.name) filter_str = properties.get("level-name", "world") + "/" file_list = (x for x in zip_file.infolist() if x.name.startswith(filter_str)) t_size = sum(x.file_size for x in file_list) written = 0 for zpath in file_list: safe_zpath = zpath.filename.replace("../", "") dst_path = instance_path / safe_zpath with zip_file.open(zpath) as zfile, open(dst_path, 'wb') as dst_file: written += zpath.file_size print(f"\r[{(written * 100 / t_size):3.0f}%] Extracting: {safe_zpath}...\033[K", end='', flush=True) shutil.copyfileobj(zfile, dst_file) print() chown(instance_path, user=SERVER_USER)
def is_ready(instance: str) -> bool: """Check if the Server is ready to serve connections/is fully started. Args: instance (str): The Instance ID. Returns: bool: True if the Server is ready to serve connections. """ cfg = config.get_properties( storage.get_instance_path(instance) / "server.properties") port = int(cfg.get("server-port")) try: server = MinecraftServer('localhost', port) mc_status = server.status() proto = mc_status.version.protocol except (ConnectionError, sock_error): return False return proto > -1
def remove_jar(source: str, force: bool = False) -> None: """Remove an .jar-File from disk. Arguments: type_id (str): The type_id of the .jar-File to be deleted. Keyword Arguments: force (bool): Delete the Jar File without a prompt. (default: {False}) """ del_all = source == "all" if not del_all: del_path = get_jar_path(source) msg = f"Are you absolutely sure you want to remove the Server Jar '{source}'?" else: del_path = get_jar_path(bare=True) msg = "Are you sure you want to remove all unused cached Server Jars?" if not del_all: if not del_path.exists(): raise FileNotFoundError(f"Type-ID not found in cache: {del_path}.") all_instances = get_instance_path(bare=True).iterdir() for instance_path in all_instances: env_path = instance_path / CFGVARS.get('system', 'env_file') try: env = config.get_properties(env_path) except OSError: env = {} server_jar = instance_path / env.get("JARFILE", "server.jar") if get_real_abspath(server_jar) == del_path: raise f"Type-ID is associated with Instance {instance_path.name}." else: if not del_path.exists(): raise FileNotFoundError("Cache already cleared.") if force or visuals.bool_selector(msg): if not del_all: del_path.unlink() else: remove_all(del_path)
def collect_server_data(instance: str) -> dict: """Collect Data about the server and return it as a dict. Args: instance (str): The instance of which the data should be collected. Returns: dict: A dict containing various Information about the server. """ instance_path = storage.get_instance_path(instance) if not instance_path.exists(): raise FileNotFoundError(f"Instance not found: {instance_path}.") properties = config.get_properties(instance_path / "server.properties") try: envinfo = config.get_properties(instance_path / CFGVARS.get('system', 'env_file')) except FileNotFoundError: envinfo = None port = properties.get("server-port") server = MinecraftServer('localhost', int(port)) status_info = status.get_simple_status(server) files = storage.get_child_paths(instance_path) total_size = sum(x.stat().st_size for x in files) unit = service.get_unit(instance) state = get_online_state(unit, status_info.get("proto")) cmdvars = { k: v for k, v in (x.decode().split("=") for x in unit.Service.Environment) } cmd = " ".join(x.decode() for x in unit.Service.ExecStart[0][1]) resolved_cmd = cmd.replace("${", "{").format(**cmdvars) jar_path = instance_path / cmdvars.get("JARFILE", "server.jar") resolved_jar_path = storage.get_real_abspath(jar_path) type_id = None if resolved_jar_path != jar_path: type_id = storage.get_type_id(resolved_jar_path) try: with open(instance_path / "whitelist.json") as wlist_hnd: whitelist = json.load(wlist_hnd) except FileNotFoundError: whitelist = None try: plugins = plugin.get_plugins(instance) except FileNotFoundError: plugins = None data = { "instance_name": instance, "instance_path": str(instance_path), "total_file_size": total_size, "status": { "players_online": status_info.get('online'), "players_max": properties.get('max-players', '?'), "protocol_name": status_info.get('proto'), "protocol_version": status_info.get('version'), }, "service": { "description": unit.Unit.Description.decode(), "unit_file_state": unit.UnitFileState.decode(), "state": state, "main_pid": unit.Service.MainPID, "start_command": resolved_cmd, "memory_usage": unit.Service.MemoryCurrent, "env": dict(cmdvars), }, "type_id": type_id, "config": { "server.properties": properties, "whitelist": whitelist, "env_file": envinfo }, "plugins": plugins, } return data