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
« 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
4Handles the actual file movement and directory creation
5during migration processes.
6"""
8import logging
9import shutil
10from pathlib import Path
11from typing import Dict, List
13logger = logging.getLogger(__name__)
16class FileMigrator:
17 """Handles file operations during migrations"""
19 def __init__(self, project_root: Path):
20 """
21 Initialize file migrator
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] = []
30 def create_directory(self, directory: Path) -> bool:
31 """
32 Create a directory if it doesn't exist
34 Args:
35 directory: Directory path to create
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
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
56 Args:
57 source: Source file path
58 destination: Destination file path
59 copy_instead: If True, copy instead of move (safer)
61 Returns:
62 True if operation was successful
63 """
64 source = Path(source)
65 destination = Path(destination)
67 if not source.exists():
68 logger.warning(f"Source file not found: {source}")
69 return False
71 if destination.exists():
72 logger.info(f"Destination already exists, skipping: {destination}")
73 return True
75 try:
76 # Ensure destination directory exists
77 destination.parent.mkdir(parents=True, exist_ok=True)
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}")
87 # Record operation
88 self.moved_files.append({"from": str(source), "to": str(destination)})
90 return True
92 except Exception as e:
93 logger.error(f"Failed to move file {source} to {destination}: {e}")
94 return False
96 def delete_file(self, file_path: Path, safe: bool = True) -> bool:
97 """
98 Delete a file
100 Args:
101 file_path: Path to the file to delete
102 safe: If True, only delete if it's a known safe file
104 Returns:
105 True if deletion was successful
106 """
107 file_path = Path(file_path)
109 if not file_path.exists():
110 logger.debug(f"File already deleted: {file_path}")
111 return True
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
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
135 def execute_migration_plan(self, plan: Dict[str, List]) -> Dict[str, any]:
136 """
137 Execute a migration plan
139 Args:
140 plan: Migration plan dictionary with 'create', 'move', 'cleanup' keys
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 }
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
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"]
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
178 return results
180 def cleanup_old_files(self, cleanup_list: List[str], dry_run: bool = False) -> int:
181 """
182 Clean up old files after successful migration
184 Args:
185 cleanup_list: List of file paths to clean up
186 dry_run: If True, only show what would be deleted
188 Returns:
189 Number of files cleaned up
190 """
191 cleaned = 0
193 for file_path in cleanup_list:
194 full_path = self.project_root / file_path
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
205 return cleaned
207 def get_migration_summary(self) -> Dict[str, any]:
208 """
209 Get summary of migration operations performed
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 }