Coverage for src / moai_adk / core / migration / file_migrator.py: 14.44%

90 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-20 20:52 +0900

1""" 

2File migration module for MoAI-ADK version upgrades 

3 

4Handles the actual file movement and directory creation 

5during migration processes. 

6""" 

7 

8import logging 

9import shutil 

10from pathlib import Path 

11from typing import Dict, List 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16class FileMigrator: 

17 """Handles file operations during migrations""" 

18 

19 def __init__(self, project_root: Path): 

20 """ 

21 Initialize file migrator 

22 

23 Args: 

24 project_root: Root directory of the project 

25 """ 

26 self.project_root = Path(project_root) 

27 self.moved_files: List[Dict[str, str]] = [] 

28 self.created_dirs: List[str] = [] 

29 

30 def create_directory(self, directory: Path) -> bool: 

31 """ 

32 Create a directory if it doesn't exist 

33 

34 Args: 

35 directory: Directory path to create 

36 

37 Returns: 

38 True if directory was created or already exists 

39 """ 

40 try: 

41 directory = Path(directory) 

42 directory.mkdir(parents=True, exist_ok=True) 

43 self.created_dirs.append(str(directory)) 

44 logger.debug(f"Created directory: {directory}") 

45 return True 

46 except Exception as e: 

47 logger.error(f"Failed to create directory {directory}: {e}") 

48 return False 

49 

50 def move_file( 

51 self, source: Path, destination: Path, copy_instead: bool = True 

52 ) -> bool: 

53 """ 

54 Move a file from source to destination 

55 

56 Args: 

57 source: Source file path 

58 destination: Destination file path 

59 copy_instead: If True, copy instead of move (safer) 

60 

61 Returns: 

62 True if operation was successful 

63 """ 

64 source = Path(source) 

65 destination = Path(destination) 

66 

67 if not source.exists(): 

68 logger.warning(f"Source file not found: {source}") 

69 return False 

70 

71 if destination.exists(): 

72 logger.info(f"Destination already exists, skipping: {destination}") 

73 return True 

74 

75 try: 

76 # Ensure destination directory exists 

77 destination.parent.mkdir(parents=True, exist_ok=True) 

78 

79 # Copy or move file 

80 if copy_instead: 

81 shutil.copy2(source, destination) 

82 logger.debug(f"Copied: {source}{destination}") 

83 else: 

84 shutil.move(str(source), str(destination)) 

85 logger.debug(f"Moved: {source}{destination}") 

86 

87 # Record operation 

88 self.moved_files.append({"from": str(source), "to": str(destination)}) 

89 

90 return True 

91 

92 except Exception as e: 

93 logger.error(f"Failed to move file {source} to {destination}: {e}") 

94 return False 

95 

96 def delete_file(self, file_path: Path, safe: bool = True) -> bool: 

97 """ 

98 Delete a file 

99 

100 Args: 

101 file_path: Path to the file to delete 

102 safe: If True, only delete if it's a known safe file 

103 

104 Returns: 

105 True if deletion was successful 

106 """ 

107 file_path = Path(file_path) 

108 

109 if not file_path.exists(): 

110 logger.debug(f"File already deleted: {file_path}") 

111 return True 

112 

113 # Safety check for safe mode 

114 if safe: 

115 safe_patterns = [ 

116 ".moai/config.json", 

117 ".claude/statusline-config.yaml", 

118 ] 

119 is_safe = any( 

120 str(file_path.relative_to(self.project_root)).endswith(pattern) 

121 for pattern in safe_patterns 

122 ) 

123 if not is_safe: 

124 logger.warning(f"Refusing to delete non-safe file: {file_path}") 

125 return False 

126 

127 try: 

128 file_path.unlink() 

129 logger.debug(f"Deleted: {file_path}") 

130 return True 

131 except Exception as e: 

132 logger.error(f"Failed to delete file {file_path}: {e}") 

133 return False 

134 

135 def execute_migration_plan(self, plan: Dict[str, List]) -> Dict[str, any]: 

136 """ 

137 Execute a migration plan 

138 

139 Args: 

140 plan: Migration plan dictionary with 'create', 'move', 'cleanup' keys 

141 

142 Returns: 

143 Dictionary with execution results 

144 """ 

145 results = { 

146 "success": True, 

147 "created_dirs": 0, 

148 "moved_files": 0, 

149 "cleaned_files": 0, 

150 "errors": [], 

151 } 

152 

153 # Create directories 

154 for directory in plan.get("create", []): 

155 dir_path = self.project_root / directory 

156 if self.create_directory(dir_path): 

157 results["created_dirs"] += 1 

158 else: 

159 results["errors"].append(f"Failed to create directory: {directory}") 

160 results["success"] = False 

161 

162 # Move files 

163 for move_op in plan.get("move", []): 

164 source = self.project_root / move_op["from"] 

165 dest = self.project_root / move_op["to"] 

166 

167 if self.move_file(source, dest, copy_instead=True): 

168 results["moved_files"] += 1 

169 logger.info( 

170 f"{move_op['description']}: {move_op['from']}{move_op['to']}" 

171 ) 

172 else: 

173 results["errors"].append( 

174 f"Failed to move: {move_op['from']}{move_op['to']}" 

175 ) 

176 results["success"] = False 

177 

178 return results 

179 

180 def cleanup_old_files(self, cleanup_list: List[str], dry_run: bool = False) -> int: 

181 """ 

182 Clean up old files after successful migration 

183 

184 Args: 

185 cleanup_list: List of file paths to clean up 

186 dry_run: If True, only show what would be deleted 

187 

188 Returns: 

189 Number of files cleaned up 

190 """ 

191 cleaned = 0 

192 

193 for file_path in cleanup_list: 

194 full_path = self.project_root / file_path 

195 

196 if dry_run: 

197 if full_path.exists(): 

198 logger.info(f"Would delete: {file_path}") 

199 cleaned += 1 

200 else: 

201 if self.delete_file(full_path, safe=True): 

202 logger.info(f"🗑️ Cleaned up: {file_path}") 

203 cleaned += 1 

204 

205 return cleaned 

206 

207 def get_migration_summary(self) -> Dict[str, any]: 

208 """ 

209 Get summary of migration operations performed 

210 

211 Returns: 

212 Dictionary with migration summary 

213 """ 

214 return { 

215 "moved_files": len(self.moved_files), 

216 "created_directories": len(self.created_dirs), 

217 "operations": self.moved_files, 

218 }