Coverage for enderchest/uninstall.py: 95%

65 statements  

« 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""" 

2 

3import os 

4import shutil 

5from collections.abc import Iterable 

6from pathlib import Path 

7 

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 

14 

15 

16def break_ender_chest(minecraft_root: Path) -> None: 

17 """Replace all instance symlinks with their actual targets, effectively 

18 "uninstalling" EnderChest 

19 

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 

30 

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 

40 

41 chest_folder = fs.ender_chest_folder(minecraft_root) 

42 _break(chest_folder, instances) 

43 

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 ) 

51 

52 

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 

57 

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) 

67 

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 

82 

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 

93 

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) 

98 

99 

100def _break( 

101 chest_folder: Path, 

102 instances: Iterable[InstanceSpec], 

103) -> None: 

104 """Actually perform the uninstallation 

105 

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() 

114 

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 

120 

121 literal_target = resource_path.readlink() 

122 

123 direct_target = Path( 

124 os.path.normpath(resource_path.readlink().expanduser().absolute()) 

125 ) 

126 

127 final_target = resource_path.resolve().expanduser() 

128 

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 

140 

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) 

162 

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 )