Skip to content

Instantly share code, notes, and snippets.

@OpenBagTwo
Created May 27, 2023 02:06
Show Gist options
  • Save OpenBagTwo/42c57359e7791b1f3cb817ee1ed7c5a5 to your computer and use it in GitHub Desktop.
Save OpenBagTwo/42c57359e7791b1f3cb817ee1ed7c5a5 to your computer and use it in GitHub Desktop.
My script for fully 'unloading" my v0.0.4 EnderChest
"""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