Coverage for enderchest/uninstall.py: 95%

66 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-04 01:41 +0000

1"""Functionality for copying all files into their instances""" 

2 

3import logging 

4import os 

5import shutil 

6from pathlib import Path 

7from typing import Iterable 

8 

9from . import filesystem as fs 

10from .enderchest import create_ender_chest 

11from .instance import InstanceSpec 

12from .inventory import load_ender_chest, load_ender_chest_instances 

13from .loggers import BREAK_LOGGER, IMPORTANT 

14from .prompt import confirm 

15 

16 

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

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

19 "uninstalling" EnderChest 

20 

21 Parameters 

22 ---------- 

23 minecraft_root : Path 

24 The root directory that your minecraft stuff (or, at least, the one 

25 that's the parent of your EnderChest folder) 

26 """ 

27 instances = load_ender_chest_instances(minecraft_root, log_level=IMPORTANT) 

28 if not instances: 

29 BREAK_LOGGER.error("Aborting.") 

30 return 

31 

32 BREAK_LOGGER.warning( 

33 "Are you sure you want to uninstall this EnderChest?" 

34 "\nDoing so will replace ALL the symlinks in each of the above instances" 

35 "\nwith copies of their EnderChest-linked targets." 

36 "\n\nTHIS CANNOT EASILY BE UNDONE!!" 

37 ) 

38 if not confirm(default=False): 

39 BREAK_LOGGER.error("Aborting.") 

40 return 

41 

42 chest_folder = fs.ender_chest_folder(minecraft_root) 

43 _break(chest_folder, instances) 

44 

45 BREAK_LOGGER.log( 

46 IMPORTANT, 

47 "EnderChest has been uninstalled." 

48 "\nYou may now delete %s" 

49 "\nand uninstall the EnderChest package", 

50 chest_folder, 

51 ) 

52 

53 

54def break_instances(minecraft_root: Path, instance_names: Iterable[str]) -> None: 

55 """Deregister the specified instances from EnderChest, replacing all 

56 instance symlinks with their actual targets, and then removing those 

57 instances from the enderchest.cfg 

58 

59 Parameters 

60 ---------- 

61 minecraft_root : Path 

62 The root directory that your minecraft stuff (or, at least, the one 

63 that's the parent of your EnderChest folder) 

64 instance_names : list of str 

65 The names of the instances to break 

66 """ 

67 ender_chest = load_ender_chest(minecraft_root) 

68 

69 instance_lookup = {instance.name: instance for instance in ender_chest.instances} 

70 instances: list[InstanceSpec] = [] 

71 for name in instance_names: 

72 try: 

73 instances.append(instance_lookup[name]) 

74 except KeyError: 

75 BREAK_LOGGER.warning( 

76 f'No instance named "%s" is registered to this EnderChest.' 

77 "\nSkipping.", 

78 name, 

79 ) 

80 if len(instances) == 0: 

81 BREAK_LOGGER.error("No valid instances specified.\nAborting.") 

82 return 

83 

84 BREAK_LOGGER.warning( 

85 "Are you sure you want to remove the following instances from your EnderChest?" 

86 "\n%s\nDoing so will replace ALL the symlinks in each of the above instances" 

87 "\nwith copies of their EnderChest-linked targets." 

88 "\n\nTHIS CANNOT EASILY BE UNDONE!!", 

89 "\n".join((f" - {instance.name}" for instance in instances)), 

90 ) 

91 if not confirm(default=False): 

92 BREAK_LOGGER.error("Aborting.") 

93 return 

94 

95 _break(fs.ender_chest_folder(minecraft_root), instances) 

96 for instance in instances: 

97 ender_chest._instances.remove(instance) 

98 create_ender_chest(minecraft_root, ender_chest) 

99 

100 

101def _break( 

102 chest_folder: Path, 

103 instances: Iterable[InstanceSpec], 

104) -> None: 

105 """Actually perform the uninstallation 

106 

107 Parameters 

108 ---------- 

109 chest_folder : Path 

110 The path of the EnderChest folder that's being "broken" 

111 instances : list of InstanceSpec 

112 The instances to clear of EnderChest links 

113 """ 

114 chest_folder = chest_folder.expanduser().resolve() 

115 

116 for instance in instances: 

117 BREAK_LOGGER.info("Copying files into %s", instance.name) 

118 for resource_path in instance.root.expanduser().rglob("*"): 

119 if not resource_path.is_symlink(): 

120 continue 

121 

122 literal_target = resource_path.readlink() 

123 

124 direct_target = Path( 

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

126 ) 

127 

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

129 

130 if not direct_target.is_relative_to( 

131 chest_folder 

132 ) and not final_target.is_relative_to(chest_folder): 

133 # note: there's a pathological case where someone does something 

134 # silly like: 

135 # ~/.minecraft/options.txt -> ~/options.txt 

136 # -> EnderChest/options.txt -> /configs/minecraft_options.txt 

137 # where deleting your EnderChest folder would break the chain, 

138 # but EnderChest wouldn't have ever created that chain, so that 

139 # person is on their own. 

140 continue 

141 

142 try: 

143 resource_path.unlink() 

144 BREAK_LOGGER.debug( 

145 "Removed link: %s -> %s", resource_path, literal_target 

146 ) 

147 if final_target.is_relative_to(chest_folder): 

148 if final_target.is_dir(): 

149 shutil.copytree( 

150 final_target, 

151 resource_path, 

152 symlinks=True, 

153 dirs_exist_ok=True, 

154 ) 

155 else: 

156 shutil.copy2( 

157 final_target, 

158 resource_path, 

159 follow_symlinks=False, 

160 ) 

161 else: 

162 resource_path.symlink_to(final_target) 

163 

164 BREAK_LOGGER.debug("Copied %s to %s", final_target, resource_path) 

165 except OSError as copy_fail: 

166 BREAK_LOGGER.warning( 

167 "Failed to copy %s to %s:\n %s", 

168 final_target, 

169 resource_path, 

170 copy_fail, 

171 )