Coverage for enderchest/uninstall.py: 98%
43 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-06 16:00 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-06 16:00 +0000
1"""Functionality for copying all files into their instances"""
2import os
3import shutil
4from pathlib import Path
5from typing import Iterable
7from . import filesystem as fs
8from .gather import load_ender_chest_instances
9from .instance import InstanceSpec
10from .loggers import BREAK_LOGGER, IMPORTANT
11from .prompt import confirm
14def break_ender_chest(minecraft_root: Path) -> None:
15 """Replace all instance symlinks with their actual targets, effectively
16 "uninstalling" EnderChest
18 Parameters
19 ----------
20 minecraft_root : Path
21 The root directory that your minecraft stuff (or, at least, the one
22 that's the parent of your EnderChest folder)
23 """
24 instances = load_ender_chest_instances(minecraft_root, log_level=IMPORTANT)
25 if not instances:
26 BREAK_LOGGER.error("Aborting.")
27 return
29 BREAK_LOGGER.warning(
30 "Are you sure you want to uninstall this EnderChest?"
31 "\nDoing so will replace ALL the symlinks in each of the above instances"
32 "\nwith copies of their EnderChest-linked targets."
33 "\n\nTHIS CANNOT EASILY BE UNDONE!!"
34 )
35 if not confirm(default=False):
36 BREAK_LOGGER.error("Aborting.")
37 return
39 _break(fs.ender_chest_folder(minecraft_root), instances)
42def _break(
43 chest_folder: Path,
44 instances: Iterable[InstanceSpec],
45) -> None:
46 """Actually perform the uninstallation (separated out for ease of mocking / testing)
48 Parameters
49 ----------
50 chest_folder : Path
51 The path of the EnderChest folder that's being "broken"
52 instances : list of InstanceSpec
53 The instances to clear of EnderChest links
54 """
55 chest_folder = chest_folder.expanduser().resolve()
57 for instance in instances:
58 BREAK_LOGGER.info("Copying files into %s", instance.name)
59 for resource_path in instance.root.expanduser().rglob("*"):
60 if not resource_path.is_symlink():
61 continue
63 literal_target = resource_path.readlink()
65 direct_target = Path(
66 os.path.normpath(resource_path.readlink().expanduser().absolute())
67 )
69 final_target = resource_path.resolve().expanduser()
71 if not direct_target.is_relative_to(
72 chest_folder
73 ) and not final_target.is_relative_to(chest_folder):
74 # note: there's a pathological case where someone does something
75 # silly like:
76 # ~/.minecraft/options.txt -> ~/options.txt
77 # -> EnderChest/options.txt -> /configs/minecraft_options.txt
78 # where deleting your EnderChest folder would break the chain,
79 # but EnderChest wouldn't have ever created that chain, so that
80 # person is on their own.
81 continue
83 try:
84 resource_path.unlink()
85 BREAK_LOGGER.debug(
86 "Removed link: %s -> %s", resource_path, literal_target
87 )
88 if final_target.is_relative_to(chest_folder):
89 if final_target.is_dir():
90 shutil.copytree(
91 final_target,
92 resource_path,
93 symlinks=True,
94 dirs_exist_ok=True,
95 )
96 else:
97 shutil.copy2(
98 final_target,
99 resource_path,
100 follow_symlinks=False,
101 )
102 else:
103 resource_path.symlink_to(final_target)
105 BREAK_LOGGER.debug("Copied %s to %s", final_target, resource_path)
106 except OSError as copy_fail:
107 BREAK_LOGGER.warning(
108 "Failed to copy %s to %s:\n %s",
109 final_target,
110 resource_path,
111 copy_fail,
112 )
114 BREAK_LOGGER.log(
115 IMPORTANT,
116 "EnderChest has been uninstalled."
117 "\nYou may now delete %s"
118 "\nand uninstall the EnderChest package",
119 chest_folder,
120 )