Created
May 27, 2023 02:06
-
-
Save OpenBagTwo/42c57359e7791b1f3cb817ee1ed7c5a5 to your computer and use it in GitHub Desktop.
My script for fully 'unloading" my v0.0.4 EnderChest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Script to "break" an EnderChest and copy all of its assets into the various | |
instance folders""" | |
import logging | |
import os | |
from pathlib import Path | |
import shutil | |
from typing import NamedTuple | |
LOGGER = logging.getLogger("chest_breaker") | |
class CLIFormatter(logging.Formatter): | |
"""Colorful formatter for the CLI | |
h/t https://stackoverflow.com/a/56944256""" | |
grey = "\x1b[38;20m" | |
yellow = "\x1b[33;20m" | |
bold_red = "\x1b[31;1m" | |
reset = "\x1b[0m" | |
FORMATS = { | |
logging.DEBUG: grey + "%(message)s" + reset, | |
logging.INFO: "%(message)s", | |
logging.WARNING: yellow + "%(message)s" + reset, | |
logging.ERROR: bold_red + "%(message)s" + reset, | |
logging.CRITICAL: bold_red + "%(message)s" + reset, | |
} | |
def format(self, record: logging.LogRecord) -> str: | |
return logging.Formatter(self.FORMATS.get(record.levelno)).format(record) | |
class Contexts(NamedTuple): | |
universal: Path | |
client_only: Path | |
server_only: Path | |
local_only: Path | |
other_locals: Path | |
def contexts(root: str | os.PathLike) -> Contexts: | |
"""Centrally define context directories based on the root folder | |
Returns | |
------- | |
Tuple of Paths | |
The contexts, in order, | |
- global : for syncing across all instances and servers | |
- client-only : for syncing across all client instances | |
- server-only : for syncing across all server instances | |
- local-only : for local use only (don't sync) | |
- other-locals : "local-only" folders from other installations | |
(for distributed backups) | |
Notes | |
----- | |
- Because "global" is a restricted keyword in Python, the namedtuple key for | |
this context is "universal" | |
- For all other contexts, the namedtuple key replaces a dash (not a valid token | |
character) with an underscore ` | |
""" | |
ender_chest = Path(root).expanduser().resolve() / "EnderChest" | |
return Contexts( | |
ender_chest / "global", | |
ender_chest / "client-only", | |
ender_chest / "server-only", | |
ender_chest / "local-only", | |
ender_chest / "other-locals", | |
) | |
def _tokenize_server_name(tag: str) -> str: | |
"""For easier integration into systemd, and because spaces in paths are a hassle | |
in general, assume (enforce?) that server folders will have "tokenized" names and | |
thus map any tags (where spaces are fine) to the correct server folder. | |
Parameters | |
---------- | |
tag : str | |
The unprocessed tag value, which can have spaces and capital letters | |
Returns | |
------- | |
str | |
The expected "tokenized" server folder name, which: | |
- will be all lowercase | |
- has all spaces replaced with periods | |
Examples | |
-------- | |
>>> _tokenize_server_name("Chaos Awakening") | |
'chaos.awakening' | |
""" | |
return tag.lower().replace(" ", ".") | |
def place_enderchest(root: str | os.PathLike) -> None: | |
"""Link all instance files and folders | |
Parameters | |
---------- | |
root : path | |
The root directory that contains the EnderChest directory, instances and servers | |
cleanup : bool, optional | |
By default, this method will remove any broken links in your instances and | |
servers folders. To disable this behavior, pass in cleanup=False | |
""" | |
instances = Path(root) / "instances" | |
servers = Path(root) / "servers" | |
for context_type, context_root in contexts(root)._asdict().items(): | |
LOGGER.info(f"Starting on {context_type}") | |
make_server_links = context_type in ("universal", "server_only") | |
make_instance_links = context_type in ("universal", "client_only", "local_only") | |
assets = sorted(context_root.rglob("*@*")) | |
for asset in assets: | |
LOGGER.debug(f"Placing {asset}") | |
if not asset.exists(): | |
LOGGER.error(f"{asset} does not exist!!") | |
continue | |
path, *tags = str(asset.relative_to(context_root)).split("@") | |
for tag in tags: | |
if make_instance_links: | |
link_instance(path, instances / tag, asset) | |
if make_server_links: | |
link_server(path, servers / _tokenize_server_name(tag), asset) | |
for file in (*instances.rglob("*"), *servers.rglob("*")): | |
if not file.exists(): | |
LOGGER.error(f"{file} is a still a broken link") | |
def link_instance( | |
resource_path: str, instance_folder: Path, destination: Path, check_exists=True | |
) -> None: | |
"""Create a symlink for the specified resource from an instance's space pointing to | |
the tagged file / folder living in the EnderChest folder. | |
Parameters | |
---------- | |
resource_path : str | |
Location of the resource relative to the instance's ".minecraft" folder | |
instance_folder : Path | |
the instance's folder (parent of ".minecraft") | |
destination : Path | |
the location to link, where the file or older actually lives (inside the | |
EnderChest folder) | |
check_exists : bool, optional | |
By default, this method will only create links if a ".minecraft" folder exists | |
in the instance_folder. To create links regardless, pass check_exists=False | |
Returns | |
------- | |
None | |
Notes | |
----- | |
- This method will create any folders that do not exist within an instance, but only | |
if the instance folder exists and has contains a ".minecraft" folder *or* if | |
check_exists is set to False | |
- This method will overwrite existing symlinks but will not overwrite any actual | |
files. | |
""" | |
if not (instance_folder / ".minecraft").exists() and check_exists: | |
return | |
instance_file = instance_folder / ".minecraft" / resource_path | |
instance_file.parent.mkdir(parents=True, exist_ok=True) | |
if instance_file.is_symlink(): | |
# remove previous symlink in this spot | |
instance_file.unlink() | |
if destination.is_symlink(): | |
os.symlink(destination.resolve(), instance_file) | |
elif destination.is_dir(): | |
shutil.copytree( | |
destination, | |
instance_file, | |
symlinks=True, | |
) | |
else: | |
shutil.copy(destination, instance_file) | |
def link_server( | |
resource_path: str, server_folder: Path, destination: Path, check_exists=True | |
) -> None: | |
"""Create a symlink for the specified resource from an server's space pointing to | |
the tagged file / folder living in the EnderChest folder. | |
Parameters | |
---------- | |
resource_path : str | |
Location of the resource relative to the instance's ".minecraft" folder | |
server_folder : Path | |
the server's folder | |
destination : Path | |
the location to link, where the file or older actually lives (inside the | |
EnderChest folder) | |
check_exists : bool, optional | |
By default, this method will only create links if the server_folder exists. | |
To create links regardless, pass check_exists=False | |
Returns | |
------- | |
None | |
Notes | |
----- | |
- This method will create any folders that do not exist within a server folder | |
- This method will overwrite existing symlinks but will not overwrite any actual | |
files | |
""" | |
if not server_folder.exists() and check_exists: | |
return | |
server_file = server_folder / resource_path | |
server_file.parent.mkdir(parents=True, exist_ok=True) | |
if server_file.is_symlink(): | |
# remove previous symlink in this spot | |
server_file.unlink() | |
if destination.is_symlink(): | |
os.symlink(destination.resolve(), server_file) | |
elif destination.is_dir(): | |
shutil.copytree( | |
destination, | |
server_file, | |
symlinks=True, | |
) | |
else: | |
shutil.copy(destination, server_file) | |
if __name__ == "__main__": | |
cli_handler = logging.StreamHandler() | |
cli_handler.setFormatter(CLIFormatter()) | |
LOGGER.addHandler(cli_handler) | |
cli_handler.setLevel(logging.DEBUG) | |
LOGGER.setLevel(logging.DEBUG) | |
place_enderchest(os.getcwd()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment