crackerjack 0.31.10__py3-none-any.whl → 0.31.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import shutil
|
|
3
|
+
import time
|
|
4
|
+
import typing as t
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
|
9
|
+
|
|
10
|
+
from ..errors import ErrorCode, ExecutionError
|
|
11
|
+
from .secure_path_utils import AtomicFileOperations, SecurePathValidator
|
|
12
|
+
from .security_logger import SecurityEventLevel, SecurityEventType, get_security_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackupMetadata(BaseModel):
|
|
16
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
17
|
+
|
|
18
|
+
backup_id: str
|
|
19
|
+
timestamp: datetime
|
|
20
|
+
package_directory: Path
|
|
21
|
+
backup_directory: Path
|
|
22
|
+
total_files: int
|
|
23
|
+
total_size: int
|
|
24
|
+
checksum: str
|
|
25
|
+
file_checksums: dict[str, str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BackupValidationResult(BaseModel):
|
|
29
|
+
is_valid: bool
|
|
30
|
+
missing_files: list[Path]
|
|
31
|
+
corrupted_files: list[Path]
|
|
32
|
+
total_validated: int
|
|
33
|
+
validation_errors: list[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PackageBackupService(BaseModel):
|
|
37
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
38
|
+
|
|
39
|
+
logger: t.Any = None
|
|
40
|
+
security_logger: t.Any = None
|
|
41
|
+
backup_root: Path | None = None
|
|
42
|
+
|
|
43
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
44
|
+
if self.logger is None:
|
|
45
|
+
import logging
|
|
46
|
+
|
|
47
|
+
self.logger = logging.getLogger("crackerjack.backup_service")
|
|
48
|
+
|
|
49
|
+
if self.security_logger is None:
|
|
50
|
+
self.security_logger = get_security_logger()
|
|
51
|
+
|
|
52
|
+
if self.backup_root is None:
|
|
53
|
+
import tempfile
|
|
54
|
+
|
|
55
|
+
self.backup_root = Path(tempfile.gettempdir()) / "crackerjack_backups"
|
|
56
|
+
|
|
57
|
+
def create_package_backup(
|
|
58
|
+
self,
|
|
59
|
+
package_directory: Path,
|
|
60
|
+
base_directory: Path | None = None,
|
|
61
|
+
) -> BackupMetadata:
|
|
62
|
+
validated_pkg_dir = SecurePathValidator.validate_file_path(
|
|
63
|
+
package_directory, base_directory
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not validated_pkg_dir.is_dir():
|
|
67
|
+
raise ExecutionError(
|
|
68
|
+
message=f"Package directory does not exist: {validated_pkg_dir}",
|
|
69
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
backup_id = self._generate_backup_id()
|
|
73
|
+
backup_dir = self._create_backup_directory(backup_id)
|
|
74
|
+
|
|
75
|
+
self.logger.info(f"Creating package backup: {backup_id}")
|
|
76
|
+
self.security_logger.log_security_event(
|
|
77
|
+
SecurityEventType.BACKUP_CREATED,
|
|
78
|
+
SecurityEventLevel.MEDIUM,
|
|
79
|
+
f"Starting package backup: {backup_id}",
|
|
80
|
+
file_path=validated_pkg_dir,
|
|
81
|
+
backup_id=backup_id,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
python_files = list(validated_pkg_dir.rglob("*.py"))
|
|
86
|
+
|
|
87
|
+
files_to_backup = self._filter_package_files(
|
|
88
|
+
python_files, validated_pkg_dir
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not files_to_backup:
|
|
92
|
+
raise ExecutionError(
|
|
93
|
+
message="No package files found to backup",
|
|
94
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
backup_metadata = self._perform_backup(
|
|
98
|
+
files_to_backup,
|
|
99
|
+
validated_pkg_dir,
|
|
100
|
+
backup_dir,
|
|
101
|
+
backup_id,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
validation_result = self._validate_backup(backup_metadata)
|
|
105
|
+
|
|
106
|
+
if not validation_result.is_valid:
|
|
107
|
+
self._cleanup_backup_directory(backup_dir)
|
|
108
|
+
raise ExecutionError(
|
|
109
|
+
message=f"Backup validation failed: {validation_result.validation_errors}",
|
|
110
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self.logger.info(
|
|
114
|
+
f"Package backup completed successfully: {backup_id} "
|
|
115
|
+
f"({backup_metadata.total_files} files, {backup_metadata.total_size} bytes)"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.security_logger.log_backup_created(
|
|
119
|
+
validated_pkg_dir,
|
|
120
|
+
backup_dir,
|
|
121
|
+
backup_id=backup_id,
|
|
122
|
+
file_count=backup_metadata.total_files,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return backup_metadata
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self._cleanup_backup_directory(backup_dir)
|
|
129
|
+
|
|
130
|
+
if isinstance(e, ExecutionError):
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
raise ExecutionError(
|
|
134
|
+
message=f"Failed to create package backup: {e}",
|
|
135
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
136
|
+
) from e
|
|
137
|
+
|
|
138
|
+
def restore_from_backup(
|
|
139
|
+
self,
|
|
140
|
+
backup_metadata: BackupMetadata,
|
|
141
|
+
base_directory: Path | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
backup_dir = backup_metadata.backup_directory
|
|
144
|
+
|
|
145
|
+
if not backup_dir.exists():
|
|
146
|
+
raise ExecutionError(
|
|
147
|
+
message=f"Backup directory not found: {backup_dir}",
|
|
148
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
self.logger.info(f"Restoring from backup: {backup_metadata.backup_id}")
|
|
152
|
+
self.security_logger.log_security_event(
|
|
153
|
+
SecurityEventType.BACKUP_RESTORED,
|
|
154
|
+
SecurityEventLevel.HIGH,
|
|
155
|
+
f"Starting backup restoration: {backup_metadata.backup_id}",
|
|
156
|
+
file_path=backup_dir,
|
|
157
|
+
backup_id=backup_metadata.backup_id,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
validation_result = self._validate_backup(backup_metadata)
|
|
161
|
+
if not validation_result.is_valid:
|
|
162
|
+
raise ExecutionError(
|
|
163
|
+
message=f"Cannot restore corrupted backup: {validation_result.validation_errors}",
|
|
164
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
temp_restore_dir = None
|
|
168
|
+
try:
|
|
169
|
+
temp_restore_dir = self._create_temp_restore_directory(
|
|
170
|
+
backup_metadata.backup_id
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
self._stage_backup_files(backup_metadata, temp_restore_dir)
|
|
174
|
+
|
|
175
|
+
self._commit_restoration(backup_metadata, temp_restore_dir, base_directory)
|
|
176
|
+
|
|
177
|
+
self.logger.info(
|
|
178
|
+
f"Backup restoration completed successfully: {backup_metadata.backup_id}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self.security_logger.log_security_event(
|
|
182
|
+
SecurityEventType.BACKUP_RESTORED,
|
|
183
|
+
SecurityEventLevel.HIGH,
|
|
184
|
+
f"Backup restoration completed: {backup_metadata.backup_id}",
|
|
185
|
+
file_path=backup_metadata.package_directory,
|
|
186
|
+
backup_id=backup_metadata.backup_id,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
if isinstance(e, ExecutionError):
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
raise ExecutionError(
|
|
194
|
+
message=f"Failed to restore from backup {backup_metadata.backup_id}: {e}",
|
|
195
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
196
|
+
) from e
|
|
197
|
+
|
|
198
|
+
finally:
|
|
199
|
+
if temp_restore_dir and temp_restore_dir.exists():
|
|
200
|
+
self._cleanup_backup_directory(temp_restore_dir)
|
|
201
|
+
|
|
202
|
+
def cleanup_backup(self, backup_metadata: BackupMetadata) -> None:
|
|
203
|
+
backup_dir = backup_metadata.backup_directory
|
|
204
|
+
|
|
205
|
+
if backup_dir.exists():
|
|
206
|
+
self._cleanup_backup_directory(backup_dir)
|
|
207
|
+
|
|
208
|
+
self.logger.info(f"Backup cleaned up: {backup_metadata.backup_id}")
|
|
209
|
+
self.security_logger.log_security_event(
|
|
210
|
+
SecurityEventType.BACKUP_DELETED,
|
|
211
|
+
SecurityEventLevel.LOW,
|
|
212
|
+
f"Backup cleanup completed: {backup_metadata.backup_id}",
|
|
213
|
+
file_path=backup_dir,
|
|
214
|
+
backup_id=backup_metadata.backup_id,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _generate_backup_id(self) -> str:
|
|
218
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
219
|
+
random_hash = hashlib.md5(
|
|
220
|
+
f"{timestamp}_{time.time()}".encode(),
|
|
221
|
+
usedforsecurity=False,
|
|
222
|
+
).hexdigest()[:8]
|
|
223
|
+
return f"backup_{timestamp}_{random_hash}"
|
|
224
|
+
|
|
225
|
+
def _create_backup_directory(self, backup_id: str) -> Path:
|
|
226
|
+
if not self.backup_root:
|
|
227
|
+
raise ExecutionError(
|
|
228
|
+
message="Backup root directory not configured",
|
|
229
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
self.backup_root.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
|
|
234
|
+
backup_dir = self.backup_root / backup_id
|
|
235
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
|
|
237
|
+
import stat
|
|
238
|
+
|
|
239
|
+
backup_dir.chmod(stat.S_IRWXU)
|
|
240
|
+
|
|
241
|
+
return backup_dir
|
|
242
|
+
|
|
243
|
+
def _create_temp_restore_directory(self, backup_id: str) -> Path:
|
|
244
|
+
import tempfile
|
|
245
|
+
|
|
246
|
+
temp_dir = Path(tempfile.mkdtemp(prefix=f"restore_{backup_id}_"))
|
|
247
|
+
|
|
248
|
+
import stat
|
|
249
|
+
|
|
250
|
+
temp_dir.chmod(stat.S_IRWXU)
|
|
251
|
+
|
|
252
|
+
return temp_dir
|
|
253
|
+
|
|
254
|
+
def _filter_package_files(
|
|
255
|
+
self,
|
|
256
|
+
python_files: list[Path],
|
|
257
|
+
package_directory: Path,
|
|
258
|
+
) -> list[Path]:
|
|
259
|
+
ignore_patterns = {
|
|
260
|
+
"__pycache__",
|
|
261
|
+
".git",
|
|
262
|
+
".venv",
|
|
263
|
+
"site-packages",
|
|
264
|
+
".pytest_cache",
|
|
265
|
+
"build",
|
|
266
|
+
"dist",
|
|
267
|
+
"tests",
|
|
268
|
+
"test",
|
|
269
|
+
"examples",
|
|
270
|
+
"example",
|
|
271
|
+
".tox",
|
|
272
|
+
"htmlcov",
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
files_to_backup = []
|
|
276
|
+
|
|
277
|
+
for file_path in python_files:
|
|
278
|
+
skip_file = False
|
|
279
|
+
for parent in file_path.parents:
|
|
280
|
+
if parent.name in ignore_patterns:
|
|
281
|
+
skip_file = True
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
if skip_file:
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
if file_path.name.startswith("."):
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
if file_path.suffix != ".py":
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
files_to_backup.append(file_path)
|
|
294
|
+
|
|
295
|
+
return files_to_backup
|
|
296
|
+
|
|
297
|
+
def _perform_backup(
|
|
298
|
+
self,
|
|
299
|
+
files_to_backup: list[Path],
|
|
300
|
+
package_directory: Path,
|
|
301
|
+
backup_dir: Path,
|
|
302
|
+
backup_id: str,
|
|
303
|
+
) -> BackupMetadata:
|
|
304
|
+
file_checksums: dict[str, str] = {}
|
|
305
|
+
total_size = 0
|
|
306
|
+
|
|
307
|
+
for file_path in files_to_backup:
|
|
308
|
+
try:
|
|
309
|
+
relative_path = file_path.relative_to(package_directory)
|
|
310
|
+
backup_file_path = backup_dir / relative_path
|
|
311
|
+
|
|
312
|
+
backup_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
|
|
314
|
+
content = file_path.read_bytes()
|
|
315
|
+
total_size += len(content)
|
|
316
|
+
|
|
317
|
+
checksum = hashlib.sha256(content, usedforsecurity=False).hexdigest()
|
|
318
|
+
file_checksums[str(relative_path)] = checksum
|
|
319
|
+
|
|
320
|
+
AtomicFileOperations.atomic_write(backup_file_path, content)
|
|
321
|
+
|
|
322
|
+
self.logger.debug(f"Backed up file: {relative_path}")
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
raise ExecutionError(
|
|
326
|
+
message=f"Failed to backup file {file_path}: {e}",
|
|
327
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
328
|
+
) from e
|
|
329
|
+
|
|
330
|
+
overall_checksum = self._calculate_backup_checksum(file_checksums)
|
|
331
|
+
|
|
332
|
+
return BackupMetadata(
|
|
333
|
+
backup_id=backup_id,
|
|
334
|
+
timestamp=datetime.now(),
|
|
335
|
+
package_directory=package_directory,
|
|
336
|
+
backup_directory=backup_dir,
|
|
337
|
+
total_files=len(files_to_backup),
|
|
338
|
+
total_size=total_size,
|
|
339
|
+
checksum=overall_checksum,
|
|
340
|
+
file_checksums=file_checksums,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _calculate_backup_checksum(self, file_checksums: dict[str, str]) -> str:
|
|
344
|
+
sorted_items = sorted(file_checksums.items())
|
|
345
|
+
combined = "".join(f"{path}:{checksum}" for path, checksum in sorted_items)
|
|
346
|
+
return hashlib.sha256(combined.encode(), usedforsecurity=False).hexdigest()
|
|
347
|
+
|
|
348
|
+
def _validate_backup(
|
|
349
|
+
self, backup_metadata: BackupMetadata
|
|
350
|
+
) -> BackupValidationResult:
|
|
351
|
+
missing_files: list[Path] = []
|
|
352
|
+
corrupted_files: list[Path] = []
|
|
353
|
+
validation_errors: list[str] = []
|
|
354
|
+
total_validated = 0
|
|
355
|
+
|
|
356
|
+
backup_dir = backup_metadata.backup_directory
|
|
357
|
+
|
|
358
|
+
if not backup_dir.exists():
|
|
359
|
+
validation_errors.append(f"Backup directory missing: {backup_dir}")
|
|
360
|
+
return BackupValidationResult(
|
|
361
|
+
is_valid=False,
|
|
362
|
+
missing_files=missing_files,
|
|
363
|
+
corrupted_files=corrupted_files,
|
|
364
|
+
total_validated=0,
|
|
365
|
+
validation_errors=validation_errors,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
for (
|
|
369
|
+
relative_path_str,
|
|
370
|
+
expected_checksum,
|
|
371
|
+
) in backup_metadata.file_checksums.items():
|
|
372
|
+
backup_file_path = backup_dir / relative_path_str
|
|
373
|
+
|
|
374
|
+
if not backup_file_path.exists():
|
|
375
|
+
missing_files.append(backup_file_path)
|
|
376
|
+
validation_errors.append(f"Missing backup file: {relative_path_str}")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
content = backup_file_path.read_bytes()
|
|
381
|
+
actual_checksum = hashlib.sha256(
|
|
382
|
+
content, usedforsecurity=False
|
|
383
|
+
).hexdigest()
|
|
384
|
+
|
|
385
|
+
if actual_checksum != expected_checksum:
|
|
386
|
+
corrupted_files.append(backup_file_path)
|
|
387
|
+
validation_errors.append(
|
|
388
|
+
f"Corrupted backup file: {relative_path_str} "
|
|
389
|
+
f"(expected: {expected_checksum}, actual: {actual_checksum})"
|
|
390
|
+
)
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
total_validated += 1
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
validation_errors.append(f"Error validating {relative_path_str}: {e}")
|
|
397
|
+
|
|
398
|
+
if not validation_errors:
|
|
399
|
+
recalculated_checksum = self._calculate_backup_checksum(
|
|
400
|
+
backup_metadata.file_checksums
|
|
401
|
+
)
|
|
402
|
+
if recalculated_checksum != backup_metadata.checksum:
|
|
403
|
+
validation_errors.append("Overall backup checksum mismatch")
|
|
404
|
+
|
|
405
|
+
is_valid = len(validation_errors) == 0
|
|
406
|
+
|
|
407
|
+
if is_valid:
|
|
408
|
+
self.logger.debug(f"Backup validation passed: {backup_metadata.backup_id}")
|
|
409
|
+
else:
|
|
410
|
+
self.logger.warning(
|
|
411
|
+
f"Backup validation failed: {backup_metadata.backup_id}, "
|
|
412
|
+
f"errors: {len(validation_errors)}"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return BackupValidationResult(
|
|
416
|
+
is_valid=is_valid,
|
|
417
|
+
missing_files=missing_files,
|
|
418
|
+
corrupted_files=corrupted_files,
|
|
419
|
+
total_validated=total_validated,
|
|
420
|
+
validation_errors=validation_errors,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _stage_backup_files(
|
|
424
|
+
self,
|
|
425
|
+
backup_metadata: BackupMetadata,
|
|
426
|
+
temp_restore_dir: Path,
|
|
427
|
+
) -> None:
|
|
428
|
+
backup_dir = backup_metadata.backup_directory
|
|
429
|
+
|
|
430
|
+
for relative_path_str in backup_metadata.file_checksums.keys():
|
|
431
|
+
backup_file_path = backup_dir / relative_path_str
|
|
432
|
+
staging_file_path = temp_restore_dir / relative_path_str
|
|
433
|
+
|
|
434
|
+
staging_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
435
|
+
|
|
436
|
+
shutil.copy2(backup_file_path, staging_file_path)
|
|
437
|
+
|
|
438
|
+
def _commit_restoration(
|
|
439
|
+
self,
|
|
440
|
+
backup_metadata: BackupMetadata,
|
|
441
|
+
temp_restore_dir: Path,
|
|
442
|
+
base_directory: Path | None,
|
|
443
|
+
) -> None:
|
|
444
|
+
package_dir = backup_metadata.package_directory
|
|
445
|
+
|
|
446
|
+
for relative_path_str in backup_metadata.file_checksums.keys():
|
|
447
|
+
staging_file_path = temp_restore_dir / relative_path_str
|
|
448
|
+
final_file_path = package_dir / relative_path_str
|
|
449
|
+
|
|
450
|
+
final_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
451
|
+
|
|
452
|
+
content = staging_file_path.read_bytes()
|
|
453
|
+
|
|
454
|
+
AtomicFileOperations.atomic_write(
|
|
455
|
+
final_file_path,
|
|
456
|
+
content,
|
|
457
|
+
base_directory,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
self.logger.debug(f"Restored file: {relative_path_str}")
|
|
461
|
+
|
|
462
|
+
def _cleanup_backup_directory(self, directory: Path) -> None:
|
|
463
|
+
if directory.exists() and directory.is_dir():
|
|
464
|
+
try:
|
|
465
|
+
shutil.rmtree(directory)
|
|
466
|
+
except Exception as e:
|
|
467
|
+
self.logger.warning(f"Failed to cleanup directory {directory}: {e}")
|