comfygit-core 0.2.0__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.
- comfygit_core/analyzers/custom_node_scanner.py +109 -0
- comfygit_core/analyzers/git_change_parser.py +156 -0
- comfygit_core/analyzers/model_scanner.py +318 -0
- comfygit_core/analyzers/node_classifier.py +58 -0
- comfygit_core/analyzers/node_git_analyzer.py +77 -0
- comfygit_core/analyzers/status_scanner.py +362 -0
- comfygit_core/analyzers/workflow_dependency_parser.py +143 -0
- comfygit_core/caching/__init__.py +16 -0
- comfygit_core/caching/api_cache.py +210 -0
- comfygit_core/caching/base.py +212 -0
- comfygit_core/caching/comfyui_cache.py +100 -0
- comfygit_core/caching/custom_node_cache.py +320 -0
- comfygit_core/caching/workflow_cache.py +797 -0
- comfygit_core/clients/__init__.py +4 -0
- comfygit_core/clients/civitai_client.py +412 -0
- comfygit_core/clients/github_client.py +349 -0
- comfygit_core/clients/registry_client.py +230 -0
- comfygit_core/configs/comfyui_builtin_nodes.py +1614 -0
- comfygit_core/configs/comfyui_models.py +62 -0
- comfygit_core/configs/model_config.py +151 -0
- comfygit_core/constants.py +82 -0
- comfygit_core/core/environment.py +1635 -0
- comfygit_core/core/workspace.py +898 -0
- comfygit_core/factories/environment_factory.py +419 -0
- comfygit_core/factories/uv_factory.py +61 -0
- comfygit_core/factories/workspace_factory.py +109 -0
- comfygit_core/infrastructure/sqlite_manager.py +156 -0
- comfygit_core/integrations/__init__.py +7 -0
- comfygit_core/integrations/uv_command.py +318 -0
- comfygit_core/logging/logging_config.py +15 -0
- comfygit_core/managers/environment_git_orchestrator.py +316 -0
- comfygit_core/managers/environment_model_manager.py +296 -0
- comfygit_core/managers/export_import_manager.py +116 -0
- comfygit_core/managers/git_manager.py +667 -0
- comfygit_core/managers/model_download_manager.py +252 -0
- comfygit_core/managers/model_symlink_manager.py +166 -0
- comfygit_core/managers/node_manager.py +1378 -0
- comfygit_core/managers/pyproject_manager.py +1321 -0
- comfygit_core/managers/user_content_symlink_manager.py +436 -0
- comfygit_core/managers/uv_project_manager.py +569 -0
- comfygit_core/managers/workflow_manager.py +1944 -0
- comfygit_core/models/civitai.py +432 -0
- comfygit_core/models/commit.py +18 -0
- comfygit_core/models/environment.py +293 -0
- comfygit_core/models/exceptions.py +378 -0
- comfygit_core/models/manifest.py +132 -0
- comfygit_core/models/node_mapping.py +201 -0
- comfygit_core/models/protocols.py +248 -0
- comfygit_core/models/registry.py +63 -0
- comfygit_core/models/shared.py +356 -0
- comfygit_core/models/sync.py +42 -0
- comfygit_core/models/system.py +204 -0
- comfygit_core/models/workflow.py +914 -0
- comfygit_core/models/workspace_config.py +71 -0
- comfygit_core/py.typed +0 -0
- comfygit_core/repositories/migrate_paths.py +49 -0
- comfygit_core/repositories/model_repository.py +958 -0
- comfygit_core/repositories/node_mappings_repository.py +246 -0
- comfygit_core/repositories/workflow_repository.py +57 -0
- comfygit_core/repositories/workspace_config_repository.py +121 -0
- comfygit_core/resolvers/global_node_resolver.py +459 -0
- comfygit_core/resolvers/model_resolver.py +250 -0
- comfygit_core/services/import_analyzer.py +218 -0
- comfygit_core/services/model_downloader.py +422 -0
- comfygit_core/services/node_lookup_service.py +251 -0
- comfygit_core/services/registry_data_manager.py +161 -0
- comfygit_core/strategies/__init__.py +4 -0
- comfygit_core/strategies/auto.py +72 -0
- comfygit_core/strategies/confirmation.py +69 -0
- comfygit_core/utils/comfyui_ops.py +125 -0
- comfygit_core/utils/common.py +164 -0
- comfygit_core/utils/conflict_parser.py +232 -0
- comfygit_core/utils/dependency_parser.py +231 -0
- comfygit_core/utils/download.py +216 -0
- comfygit_core/utils/environment_cleanup.py +111 -0
- comfygit_core/utils/filesystem.py +178 -0
- comfygit_core/utils/git.py +1184 -0
- comfygit_core/utils/input_signature.py +145 -0
- comfygit_core/utils/model_categories.py +52 -0
- comfygit_core/utils/pytorch.py +71 -0
- comfygit_core/utils/requirements.py +211 -0
- comfygit_core/utils/retry.py +242 -0
- comfygit_core/utils/symlink_utils.py +119 -0
- comfygit_core/utils/system_detector.py +258 -0
- comfygit_core/utils/uuid.py +28 -0
- comfygit_core/utils/uv_error_handler.py +158 -0
- comfygit_core/utils/version.py +73 -0
- comfygit_core/utils/workflow_hash.py +90 -0
- comfygit_core/validation/resolution_tester.py +297 -0
- comfygit_core-0.2.0.dist-info/METADATA +939 -0
- comfygit_core-0.2.0.dist-info/RECORD +93 -0
- comfygit_core-0.2.0.dist-info/WHEEL +4 -0
- comfygit_core-0.2.0.dist-info/licenses/LICENSE.txt +661 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""UserContentSymlinkManager - Manages per-environment input/output directories."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
from ..logging.logging_config import get_logger
|
|
9
|
+
from ..models.exceptions import CDEnvironmentError
|
|
10
|
+
from ..utils.symlink_utils import (
|
|
11
|
+
is_link,
|
|
12
|
+
create_platform_link,
|
|
13
|
+
is_safe_to_delete,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UserContentSymlinkManager:
|
|
20
|
+
"""Manages per-environment user content directories (input/output).
|
|
21
|
+
|
|
22
|
+
Creates symlinks from ComfyUI/input and ComfyUI/output to workspace-level
|
|
23
|
+
directories that persist when environments are deleted.
|
|
24
|
+
|
|
25
|
+
Architecture:
|
|
26
|
+
- Input: ComfyUI/input/ → workspace/input/{env_name}/
|
|
27
|
+
- Output: ComfyUI/output/ → workspace/output/{env_name}/
|
|
28
|
+
|
|
29
|
+
Unlike models (which are shared across environments), input/output are
|
|
30
|
+
isolated per-environment to prevent cross-contamination of user data.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
comfyui_path: Path,
|
|
36
|
+
env_name: str,
|
|
37
|
+
workspace_input_base: Path,
|
|
38
|
+
workspace_output_base: Path,
|
|
39
|
+
):
|
|
40
|
+
"""Initialize UserContentSymlinkManager.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
comfyui_path: Path to ComfyUI directory
|
|
44
|
+
env_name: Environment name (for subdirectory creation)
|
|
45
|
+
workspace_input_base: Base workspace input directory (workspace/input/)
|
|
46
|
+
workspace_output_base: Base workspace output directory (workspace/output/)
|
|
47
|
+
"""
|
|
48
|
+
self.comfyui_path = comfyui_path
|
|
49
|
+
self.env_name = env_name
|
|
50
|
+
|
|
51
|
+
# Workspace targets (per-environment subdirectories)
|
|
52
|
+
self.input_target = workspace_input_base / env_name
|
|
53
|
+
self.output_target = workspace_output_base / env_name
|
|
54
|
+
|
|
55
|
+
# ComfyUI symlinks
|
|
56
|
+
self.input_link = comfyui_path / "input"
|
|
57
|
+
self.output_link = comfyui_path / "output"
|
|
58
|
+
|
|
59
|
+
# Safe files that can be deleted without warning
|
|
60
|
+
self.safe_files = {".gitkeep", ".gitignore", "Put files here.txt"}
|
|
61
|
+
|
|
62
|
+
def create_directories(self) -> None:
|
|
63
|
+
"""Create workspace subdirectories for this environment.
|
|
64
|
+
|
|
65
|
+
Creates:
|
|
66
|
+
- workspace/input/{env_name}/
|
|
67
|
+
- workspace/output/{env_name}/
|
|
68
|
+
|
|
69
|
+
Safe to call multiple times (idempotent).
|
|
70
|
+
"""
|
|
71
|
+
self.input_target.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
self.output_target.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
logger.debug(
|
|
74
|
+
f"Created workspace directories for '{self.env_name}': "
|
|
75
|
+
f"input={self.input_target}, output={self.output_target}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def create_symlinks(self) -> None:
|
|
79
|
+
"""Create input and output symlinks.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
CDEnvironmentError: If workspace directories don't exist or if
|
|
83
|
+
ComfyUI directories exist with actual content
|
|
84
|
+
"""
|
|
85
|
+
# Ensure workspace directories exist
|
|
86
|
+
if not self.input_target.exists():
|
|
87
|
+
raise CDEnvironmentError(
|
|
88
|
+
f"Workspace input directory for '{self.env_name}' does not exist: {self.input_target}\n"
|
|
89
|
+
f"Call create_directories() first"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if not self.output_target.exists():
|
|
93
|
+
raise CDEnvironmentError(
|
|
94
|
+
f"Workspace output directory for '{self.env_name}' does not exist: {self.output_target}\n"
|
|
95
|
+
f"Call create_directories() first"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Create both symlinks
|
|
99
|
+
self._create_single_link(
|
|
100
|
+
self.input_link,
|
|
101
|
+
self.input_target,
|
|
102
|
+
"input"
|
|
103
|
+
)
|
|
104
|
+
self._create_single_link(
|
|
105
|
+
self.output_link,
|
|
106
|
+
self.output_target,
|
|
107
|
+
"output"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _create_single_link(
|
|
111
|
+
self,
|
|
112
|
+
link_path: Path,
|
|
113
|
+
target_path: Path,
|
|
114
|
+
name: str
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Create a single symlink with safety checks.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
link_path: Where to create symlink (e.g., ComfyUI/input/)
|
|
120
|
+
target_path: What symlink should point to (e.g., workspace/input/env1/)
|
|
121
|
+
name: Directory name for logging/errors ("input" or "output")
|
|
122
|
+
"""
|
|
123
|
+
# Handle existing path
|
|
124
|
+
if link_path.exists():
|
|
125
|
+
if is_link(link_path):
|
|
126
|
+
# Already a link - check target
|
|
127
|
+
if link_path.resolve() == target_path.resolve():
|
|
128
|
+
logger.debug(f"{name} link already points to correct target")
|
|
129
|
+
return
|
|
130
|
+
else:
|
|
131
|
+
# Wrong target - recreate
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Updating {name} link target: "
|
|
134
|
+
f"{link_path.resolve()} → {target_path}"
|
|
135
|
+
)
|
|
136
|
+
link_path.unlink()
|
|
137
|
+
else:
|
|
138
|
+
# Real directory - check if safe to delete
|
|
139
|
+
if is_safe_to_delete(link_path, self.safe_files):
|
|
140
|
+
logger.info(
|
|
141
|
+
f"Removing ComfyUI default {name}/ directory "
|
|
142
|
+
f"(empty or placeholder files only)"
|
|
143
|
+
)
|
|
144
|
+
shutil.rmtree(link_path)
|
|
145
|
+
else:
|
|
146
|
+
# Has content - needs migration
|
|
147
|
+
raise CDEnvironmentError(
|
|
148
|
+
f"{name}/ directory exists with content: {link_path}\n"
|
|
149
|
+
f"Use migrate_existing_data() to migrate to workspace-level storage"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Ensure parent directory (ComfyUI/) exists
|
|
153
|
+
self.comfyui_path.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
|
|
155
|
+
# Create link
|
|
156
|
+
create_platform_link(link_path, target_path, name)
|
|
157
|
+
logger.info(f"Created {name} link: {link_path} → {target_path}")
|
|
158
|
+
|
|
159
|
+
def migrate_existing_data(self) -> dict[str, int]:
|
|
160
|
+
"""Migrate existing input/output directories to workspace-level storage.
|
|
161
|
+
|
|
162
|
+
This handles upgrading environments created before the symlink feature.
|
|
163
|
+
If ComfyUI/input or ComfyUI/output exist as real directories with content,
|
|
164
|
+
moves their contents to workspace-level and creates symlinks.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dict with migration statistics:
|
|
168
|
+
{
|
|
169
|
+
"input_files_moved": int,
|
|
170
|
+
"output_files_moved": int,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
CDEnvironmentError: If migration fails
|
|
175
|
+
"""
|
|
176
|
+
stats = {
|
|
177
|
+
"input_files_moved": 0,
|
|
178
|
+
"output_files_moved": 0,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Ensure workspace directories exist
|
|
182
|
+
self.create_directories()
|
|
183
|
+
|
|
184
|
+
# Migrate input
|
|
185
|
+
if self.input_link.exists() and not is_link(self.input_link):
|
|
186
|
+
if not is_safe_to_delete(self.input_link, self.safe_files):
|
|
187
|
+
logger.info(f"Migrating existing input/ directory to workspace...")
|
|
188
|
+
stats["input_files_moved"] = self._migrate_directory(
|
|
189
|
+
self.input_link,
|
|
190
|
+
self.input_target,
|
|
191
|
+
"input"
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
# Just remove empty/placeholder directory
|
|
195
|
+
shutil.rmtree(self.input_link)
|
|
196
|
+
|
|
197
|
+
# Migrate output
|
|
198
|
+
if self.output_link.exists() and not is_link(self.output_link):
|
|
199
|
+
if not is_safe_to_delete(self.output_link, self.safe_files):
|
|
200
|
+
logger.info(f"Migrating existing output/ directory to workspace...")
|
|
201
|
+
stats["output_files_moved"] = self._migrate_directory(
|
|
202
|
+
self.output_link,
|
|
203
|
+
self.output_target,
|
|
204
|
+
"output"
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
# Just remove empty/placeholder directory
|
|
208
|
+
shutil.rmtree(self.output_link)
|
|
209
|
+
|
|
210
|
+
# Create symlinks after migration
|
|
211
|
+
self.create_symlinks()
|
|
212
|
+
|
|
213
|
+
return stats
|
|
214
|
+
|
|
215
|
+
def _migrate_directory(
|
|
216
|
+
self,
|
|
217
|
+
source_dir: Path,
|
|
218
|
+
target_dir: Path,
|
|
219
|
+
name: str
|
|
220
|
+
) -> int:
|
|
221
|
+
"""Move contents from source to target and create symlink.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
source_dir: Directory with existing content (e.g., ComfyUI/input/)
|
|
225
|
+
target_dir: Workspace directory to move to (e.g., workspace/input/env1/)
|
|
226
|
+
name: Directory name for logging
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Number of items moved
|
|
230
|
+
"""
|
|
231
|
+
# Count items before migration
|
|
232
|
+
items = list(source_dir.iterdir())
|
|
233
|
+
item_count = len(items)
|
|
234
|
+
|
|
235
|
+
if item_count == 0:
|
|
236
|
+
logger.debug(f"No files to migrate in {name}/")
|
|
237
|
+
shutil.rmtree(source_dir)
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
logger.info(f"Migrating {item_count} items from {name}/ to workspace...")
|
|
241
|
+
|
|
242
|
+
# Move each item
|
|
243
|
+
for item in items:
|
|
244
|
+
target_path = target_dir / item.name
|
|
245
|
+
if target_path.exists():
|
|
246
|
+
logger.warning(
|
|
247
|
+
f"Target already exists, skipping: {item.name}"
|
|
248
|
+
)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
shutil.move(str(item), str(target_path))
|
|
252
|
+
logger.debug(f"Moved: {item.name}")
|
|
253
|
+
|
|
254
|
+
# Remove now-empty source directory
|
|
255
|
+
shutil.rmtree(source_dir)
|
|
256
|
+
logger.info(f"Migration complete: {item_count} items moved to {target_dir}")
|
|
257
|
+
|
|
258
|
+
return item_count
|
|
259
|
+
|
|
260
|
+
def validate_symlinks(self) -> dict[str, bool]:
|
|
261
|
+
"""Check if symlinks exist and point to correct targets.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Dict with validation results:
|
|
265
|
+
{
|
|
266
|
+
"input": bool, # True if valid
|
|
267
|
+
"output": bool, # True if valid
|
|
268
|
+
}
|
|
269
|
+
"""
|
|
270
|
+
return {
|
|
271
|
+
"input": self._validate_single_link(self.input_link, self.input_target, "input"),
|
|
272
|
+
"output": self._validate_single_link(self.output_link, self.output_target, "output"),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
def _validate_single_link(
|
|
276
|
+
self,
|
|
277
|
+
link_path: Path,
|
|
278
|
+
target_path: Path,
|
|
279
|
+
name: str
|
|
280
|
+
) -> bool:
|
|
281
|
+
"""Validate a single symlink."""
|
|
282
|
+
if not link_path.exists():
|
|
283
|
+
logger.warning(f"{name} link does not exist: {link_path}")
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
if not is_link(link_path):
|
|
287
|
+
logger.warning(f"{name}/ is not a link: {link_path}")
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
actual_target = link_path.resolve()
|
|
291
|
+
expected_target = target_path.resolve()
|
|
292
|
+
|
|
293
|
+
if actual_target != expected_target:
|
|
294
|
+
logger.warning(
|
|
295
|
+
f"{name} link points to wrong target:\n"
|
|
296
|
+
f" Expected: {expected_target}\n"
|
|
297
|
+
f" Actual: {actual_target}"
|
|
298
|
+
)
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
def remove_symlinks(self) -> None:
|
|
304
|
+
"""Remove input and output symlinks safely.
|
|
305
|
+
|
|
306
|
+
Note: This only removes symlinks, not the workspace data they point to.
|
|
307
|
+
Workspace data (workspace/input/{env_name}/ and workspace/output/{env_name}/)
|
|
308
|
+
is preserved for manual cleanup or deletion via delete_user_data().
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
CDEnvironmentError: If paths exist but are not symlinks
|
|
312
|
+
"""
|
|
313
|
+
self._remove_single_link(self.input_link, "input")
|
|
314
|
+
self._remove_single_link(self.output_link, "output")
|
|
315
|
+
|
|
316
|
+
def _remove_single_link(self, link_path: Path, name: str) -> None:
|
|
317
|
+
"""Remove a single symlink safely."""
|
|
318
|
+
if not link_path.exists():
|
|
319
|
+
return # Nothing to remove
|
|
320
|
+
|
|
321
|
+
if not is_link(link_path):
|
|
322
|
+
raise CDEnvironmentError(
|
|
323
|
+
f"Cannot remove {name}/: not a link\n"
|
|
324
|
+
f"Manual deletion required: {link_path}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
link_path.unlink()
|
|
328
|
+
logger.info(f"Removed {name} link: {link_path}")
|
|
329
|
+
|
|
330
|
+
def get_user_data_size(self) -> dict[str, Tuple[int, int]]:
|
|
331
|
+
"""Get size of user content for deletion warnings.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Dict with file counts and sizes in bytes:
|
|
335
|
+
{
|
|
336
|
+
"input": (file_count, total_bytes),
|
|
337
|
+
"output": (file_count, total_bytes),
|
|
338
|
+
}
|
|
339
|
+
"""
|
|
340
|
+
return {
|
|
341
|
+
"input": self._get_directory_size(self.input_target),
|
|
342
|
+
"output": self._get_directory_size(self.output_target),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def _get_directory_size(self, path: Path) -> Tuple[int, int]:
|
|
346
|
+
"""Get file count and total size for directory."""
|
|
347
|
+
if not path.exists():
|
|
348
|
+
return (0, 0)
|
|
349
|
+
|
|
350
|
+
total_size = 0
|
|
351
|
+
file_count = 0
|
|
352
|
+
|
|
353
|
+
for item in path.rglob("*"):
|
|
354
|
+
if item.is_file():
|
|
355
|
+
file_count += 1
|
|
356
|
+
total_size += item.stat().st_size
|
|
357
|
+
|
|
358
|
+
return (file_count, total_size)
|
|
359
|
+
|
|
360
|
+
def delete_user_data(self) -> dict[str, int]:
|
|
361
|
+
"""Delete workspace user data for this environment.
|
|
362
|
+
|
|
363
|
+
WARNING: This permanently deletes user content (input files and generated outputs).
|
|
364
|
+
Should only be called when user explicitly confirms deletion.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dict with deletion statistics:
|
|
368
|
+
{
|
|
369
|
+
"input_files_deleted": int,
|
|
370
|
+
"output_files_deleted": int,
|
|
371
|
+
}
|
|
372
|
+
"""
|
|
373
|
+
stats = {
|
|
374
|
+
"input_files_deleted": 0,
|
|
375
|
+
"output_files_deleted": 0,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Delete input directory
|
|
379
|
+
if self.input_target.exists():
|
|
380
|
+
file_count, _ = self._get_directory_size(self.input_target)
|
|
381
|
+
shutil.rmtree(self.input_target)
|
|
382
|
+
stats["input_files_deleted"] = file_count
|
|
383
|
+
logger.info(f"Deleted {file_count} input files from {self.input_target}")
|
|
384
|
+
|
|
385
|
+
# Delete output directory
|
|
386
|
+
if self.output_target.exists():
|
|
387
|
+
file_count, _ = self._get_directory_size(self.output_target)
|
|
388
|
+
shutil.rmtree(self.output_target)
|
|
389
|
+
stats["output_files_deleted"] = file_count
|
|
390
|
+
logger.info(f"Deleted {file_count} output files from {self.output_target}")
|
|
391
|
+
|
|
392
|
+
return stats
|
|
393
|
+
|
|
394
|
+
def get_status(self) -> dict:
|
|
395
|
+
"""Get current symlink status for debugging.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dictionary with detailed status for both input and output
|
|
399
|
+
"""
|
|
400
|
+
return {
|
|
401
|
+
"input": self._get_single_status(self.input_link, self.input_target, "input"),
|
|
402
|
+
"output": self._get_single_status(self.output_link, self.output_target, "output"),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
def _get_single_status(
|
|
406
|
+
self,
|
|
407
|
+
link_path: Path,
|
|
408
|
+
target_path: Path,
|
|
409
|
+
name: str
|
|
410
|
+
) -> dict:
|
|
411
|
+
"""Get status for a single symlink."""
|
|
412
|
+
if not link_path.exists():
|
|
413
|
+
return {
|
|
414
|
+
"exists": False,
|
|
415
|
+
"is_symlink": False,
|
|
416
|
+
"is_valid": False,
|
|
417
|
+
"target": None,
|
|
418
|
+
"expected_target": str(target_path),
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
is_symlink_or_junction = is_link(link_path)
|
|
422
|
+
actual_target = link_path.resolve() if is_symlink_or_junction else None
|
|
423
|
+
is_valid = (
|
|
424
|
+
is_symlink_or_junction
|
|
425
|
+
and actual_target == target_path.resolve()
|
|
426
|
+
if actual_target
|
|
427
|
+
else False
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
"exists": True,
|
|
432
|
+
"is_symlink": is_symlink_or_junction,
|
|
433
|
+
"is_valid": is_valid,
|
|
434
|
+
"target": str(actual_target) if actual_target else None,
|
|
435
|
+
"expected_target": str(target_path),
|
|
436
|
+
}
|