Coverage for enderchest/uninstall.py: 98%

43 statements  

« 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 

6 

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 

12 

13 

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

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

16 "uninstalling" EnderChest 

17 

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 

28 

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 

38 

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

40 

41 

42def _break( 

43 chest_folder: Path, 

44 instances: Iterable[InstanceSpec], 

45) -> None: 

46 """Actually perform the uninstallation (separated out for ease of mocking / testing) 

47 

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

56 

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 

62 

63 literal_target = resource_path.readlink() 

64 

65 direct_target = Path( 

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

67 ) 

68 

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

70 

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 

82 

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) 

104 

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 ) 

113 

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 )