Coverage for enderchest/uninstall.py: 95%
65 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-28 20:32 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-28 20:32 +0000
1"""Functionality for copying all files into their instances"""
3import os
4import shutil
5from collections.abc import Iterable
6from pathlib import Path
8from . import filesystem as fs
9from .enderchest import create_ender_chest
10from .instance import InstanceSpec
11from .inventory import load_ender_chest, load_ender_chest_instances
12from .loggers import BREAK_LOGGER, IMPORTANT
13from .prompt import confirm
16def break_ender_chest(minecraft_root: Path) -> None:
17 """Replace all instance symlinks with their actual targets, effectively
18 "uninstalling" EnderChest
20 Parameters
21 ----------
22 minecraft_root : Path
23 The root directory that your minecraft stuff (or, at least, the one
24 that's the parent of your EnderChest folder)
25 """
26 instances = load_ender_chest_instances(minecraft_root, log_level=IMPORTANT)
27 if not instances:
28 BREAK_LOGGER.error("Aborting.")
29 return
31 BREAK_LOGGER.warning(
32 "Are you sure you want to uninstall this EnderChest?"
33 "\nDoing so will replace ALL the symlinks in each of the above instances"
34 "\nwith copies of their EnderChest-linked targets."
35 "\n\nTHIS CANNOT EASILY BE UNDONE!!"
36 )
37 if not confirm(default=False):
38 BREAK_LOGGER.error("Aborting.")
39 return
41 chest_folder = fs.ender_chest_folder(minecraft_root)
42 _break(chest_folder, instances)
44 BREAK_LOGGER.log(
45 IMPORTANT,
46 "EnderChest has been uninstalled."
47 "\nYou may now delete %s"
48 "\nand uninstall the EnderChest package",
49 chest_folder,
50 )
53def break_instances(minecraft_root: Path, instance_names: Iterable[str]) -> None:
54 """Deregister the specified instances from EnderChest, replacing all
55 instance symlinks with their actual targets, and then removing those
56 instances from the enderchest.cfg
58 Parameters
59 ----------
60 minecraft_root : Path
61 The root directory that your minecraft stuff (or, at least, the one
62 that's the parent of your EnderChest folder)
63 instance_names : list of str
64 The names of the instances to break
65 """
66 ender_chest = load_ender_chest(minecraft_root)
68 instance_lookup = {instance.name: instance for instance in ender_chest.instances}
69 instances: list[InstanceSpec] = []
70 for name in instance_names:
71 try:
72 instances.append(instance_lookup[name])
73 except KeyError:
74 BREAK_LOGGER.warning(
75 f'No instance named "%s" is registered to this EnderChest.'
76 "\nSkipping.",
77 name,
78 )
79 if len(instances) == 0:
80 BREAK_LOGGER.error("No valid instances specified.\nAborting.")
81 return
83 BREAK_LOGGER.warning(
84 "Are you sure you want to remove the following instances from your EnderChest?"
85 "\n%s\nDoing so will replace ALL the symlinks in each of the above instances"
86 "\nwith copies of their EnderChest-linked targets."
87 "\n\nTHIS CANNOT EASILY BE UNDONE!!",
88 "\n".join((f" - {instance.name}" for instance in instances)),
89 )
90 if not confirm(default=False):
91 BREAK_LOGGER.error("Aborting.")
92 return
94 _break(fs.ender_chest_folder(minecraft_root), instances)
95 for instance in instances:
96 ender_chest._instances.remove(instance)
97 create_ender_chest(minecraft_root, ender_chest)
100def _break(
101 chest_folder: Path,
102 instances: Iterable[InstanceSpec],
103) -> None:
104 """Actually perform the uninstallation
106 Parameters
107 ----------
108 chest_folder : Path
109 The path of the EnderChest folder that's being "broken"
110 instances : list of InstanceSpec
111 The instances to clear of EnderChest links
112 """
113 chest_folder = chest_folder.expanduser().resolve()
115 for instance in instances:
116 BREAK_LOGGER.info("Copying files into %s", instance.name)
117 for resource_path in instance.root.expanduser().rglob("*"):
118 if not resource_path.is_symlink():
119 continue
121 literal_target = resource_path.readlink()
123 direct_target = Path(
124 os.path.normpath(resource_path.readlink().expanduser().absolute())
125 )
127 final_target = resource_path.resolve().expanduser()
129 if not direct_target.is_relative_to(
130 chest_folder
131 ) and not final_target.is_relative_to(chest_folder):
132 # note: there's a pathological case where someone does something
133 # silly like:
134 # ~/.minecraft/options.txt -> ~/options.txt
135 # -> EnderChest/options.txt -> /configs/minecraft_options.txt
136 # where deleting your EnderChest folder would break the chain,
137 # but EnderChest wouldn't have ever created that chain, so that
138 # person is on their own.
139 continue
141 try:
142 resource_path.unlink()
143 BREAK_LOGGER.debug(
144 "Removed link: %s -> %s", resource_path, literal_target
145 )
146 if final_target.is_relative_to(chest_folder):
147 if final_target.is_dir():
148 shutil.copytree(
149 final_target,
150 resource_path,
151 symlinks=True,
152 dirs_exist_ok=True,
153 )
154 else:
155 shutil.copy2(
156 final_target,
157 resource_path,
158 follow_symlinks=False,
159 )
160 else:
161 resource_path.symlink_to(final_target)
163 BREAK_LOGGER.debug("Copied %s to %s", final_target, resource_path)
164 except OSError as copy_fail:
165 BREAK_LOGGER.warning(
166 "Failed to copy %s to %s:\n %s",
167 final_target,
168 resource_path,
169 copy_fail,
170 )