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,252 @@
|
|
|
1
|
+
"""ModelDownloadManager - Handle model downloads from various sources."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from ..logging.logging_config import get_logger
|
|
9
|
+
from ..models.exceptions import ComfyDockError
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..repositories.model_repository import ModelRepository
|
|
14
|
+
from ..models.shared import ModelWithLocation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModelDownloadManager:
|
|
21
|
+
"""Handle model downloads from various sources."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, model_repository: ModelRepository, cache_dir: Path):
|
|
24
|
+
"""Initialize ModelDownloadManager.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
model_manager: ModelManager instance for indexing
|
|
28
|
+
cache_dir: Directory to store downloaded models (defaults to workspace/models)
|
|
29
|
+
"""
|
|
30
|
+
self.model_repository = model_repository
|
|
31
|
+
self.cache_dir = cache_dir / "models"
|
|
32
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
def download_from_url(self, url: str, filename: str | None = None) -> ModelWithLocation:
|
|
35
|
+
"""Download model from URL and add to index.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
url: Source URL to download from
|
|
39
|
+
filename: Override filename (extracted from URL if not provided)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
ModelWithLocation entry for the downloaded model
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ComfyDockError: If download or indexing fails
|
|
46
|
+
"""
|
|
47
|
+
# Check if we already have this URL
|
|
48
|
+
existing = self.model_repository.find_by_source_url(url)
|
|
49
|
+
if existing:
|
|
50
|
+
logger.info(f"Model already indexed from {url}")
|
|
51
|
+
return existing
|
|
52
|
+
|
|
53
|
+
# Parse URL for metadata
|
|
54
|
+
source_type, metadata = self._parse_source_url(url)
|
|
55
|
+
|
|
56
|
+
# Determine filename
|
|
57
|
+
if not filename:
|
|
58
|
+
filename = self._extract_filename_from_url(url)
|
|
59
|
+
|
|
60
|
+
target_path = self.cache_dir / filename
|
|
61
|
+
|
|
62
|
+
# Download if not exists
|
|
63
|
+
if not target_path.exists():
|
|
64
|
+
logger.info(f"Downloading model from {url}")
|
|
65
|
+
self._download_file(url, target_path)
|
|
66
|
+
else:
|
|
67
|
+
logger.info(f"Model file already exists: {target_path}")
|
|
68
|
+
|
|
69
|
+
# Create model info and add to index
|
|
70
|
+
model_info = self.model_repository._create_model_info(target_path)
|
|
71
|
+
|
|
72
|
+
# Add to index with source tracking
|
|
73
|
+
self.model_repository.add_model(model_info, target_path, "downloads")
|
|
74
|
+
self.model_repository.add_source(
|
|
75
|
+
model_info.short_hash, source_type, url, metadata
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Return the indexed model
|
|
79
|
+
indexed_models = self.model_repository.find_model_by_hash(model_info.short_hash)
|
|
80
|
+
if not indexed_models:
|
|
81
|
+
raise ComfyDockError(f"Failed to index downloaded model: {filename}")
|
|
82
|
+
|
|
83
|
+
logger.info(f"Successfully downloaded and indexed: {filename}")
|
|
84
|
+
return indexed_models[0]
|
|
85
|
+
|
|
86
|
+
def _download_file(self, url: str, target_path: Path) -> None:
|
|
87
|
+
"""Download file from URL to target path.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
url: URL to download
|
|
91
|
+
target_path: Path to save file
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ComfyDockError: If download fails
|
|
95
|
+
"""
|
|
96
|
+
temp_path = None
|
|
97
|
+
try:
|
|
98
|
+
from ..utils.download import download_file
|
|
99
|
+
|
|
100
|
+
# Download to temporary file
|
|
101
|
+
temp_path = download_file(url)
|
|
102
|
+
|
|
103
|
+
# Move to target location
|
|
104
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
shutil.move(temp_path, target_path)
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
# Clean up temp file if it exists
|
|
109
|
+
if temp_path and temp_path.exists():
|
|
110
|
+
temp_path.unlink()
|
|
111
|
+
raise ComfyDockError(f"Failed to download {url}: {e}") from e
|
|
112
|
+
|
|
113
|
+
def _parse_source_url(self, url: str) -> tuple[str, dict]:
|
|
114
|
+
"""Parse URL to determine source type and extract metadata.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
url: Source URL
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Tuple of (source_type, metadata_dict)
|
|
121
|
+
"""
|
|
122
|
+
parsed = urlparse(url.lower())
|
|
123
|
+
metadata = {'original_url': url}
|
|
124
|
+
|
|
125
|
+
if 'civitai.com' in parsed.netloc:
|
|
126
|
+
source_type = 'civitai'
|
|
127
|
+
# Extract model ID from URL if possible
|
|
128
|
+
if '/models/' in url:
|
|
129
|
+
parts = url.split('/models/')
|
|
130
|
+
if len(parts) > 1:
|
|
131
|
+
model_id = parts[1].split('/')[0]
|
|
132
|
+
metadata['model_id'] = model_id
|
|
133
|
+
elif 'huggingface.co' in parsed.netloc:
|
|
134
|
+
source_type = 'huggingface'
|
|
135
|
+
# Extract repository path
|
|
136
|
+
path_parts = parsed.path.strip('/').split('/')
|
|
137
|
+
if len(path_parts) >= 2:
|
|
138
|
+
metadata['repository'] = f"{path_parts[0]}/{path_parts[1]}"
|
|
139
|
+
else:
|
|
140
|
+
source_type = 'url'
|
|
141
|
+
metadata['domain'] = parsed.netloc
|
|
142
|
+
|
|
143
|
+
return source_type, metadata
|
|
144
|
+
|
|
145
|
+
def _extract_filename_from_url(self, url: str) -> str:
|
|
146
|
+
"""Extract filename from URL.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
url: Source URL
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Extracted filename
|
|
153
|
+
"""
|
|
154
|
+
parsed = urlparse(url)
|
|
155
|
+
|
|
156
|
+
# Try to get filename from path
|
|
157
|
+
path = parsed.path
|
|
158
|
+
if path:
|
|
159
|
+
filename = Path(path).name
|
|
160
|
+
if filename and '.' in filename:
|
|
161
|
+
return filename
|
|
162
|
+
|
|
163
|
+
# Try to get from query parameters (common for download APIs)
|
|
164
|
+
if parsed.query:
|
|
165
|
+
for param in parsed.query.split('&'):
|
|
166
|
+
if '=' in param:
|
|
167
|
+
key, value = param.split('=', 1)
|
|
168
|
+
if key.lower() in ['filename', 'name', 'file']:
|
|
169
|
+
return value
|
|
170
|
+
|
|
171
|
+
# Fall back to generic name with extension guessing
|
|
172
|
+
if 'civitai' in url.lower():
|
|
173
|
+
return "civitai_model.safetensors"
|
|
174
|
+
elif 'huggingface' in url.lower():
|
|
175
|
+
return "huggingface_model.safetensors"
|
|
176
|
+
else:
|
|
177
|
+
return "downloaded_model.safetensors"
|
|
178
|
+
|
|
179
|
+
def get_download_info(self, url: str) -> dict:
|
|
180
|
+
"""Get information about what would be downloaded without downloading.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
url: Source URL
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dictionary with download information
|
|
187
|
+
"""
|
|
188
|
+
source_type, metadata = self._parse_source_url(url)
|
|
189
|
+
filename = self._extract_filename_from_url(url)
|
|
190
|
+
target_path = self.cache_dir / filename
|
|
191
|
+
|
|
192
|
+
# Check if already exists
|
|
193
|
+
existing = self.model_repository.find_by_source_url(url)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
'url': url,
|
|
197
|
+
'source_type': source_type,
|
|
198
|
+
'filename': filename,
|
|
199
|
+
'target_path': str(target_path),
|
|
200
|
+
'already_downloaded': target_path.exists(),
|
|
201
|
+
'already_indexed': existing is not None,
|
|
202
|
+
'existing_model': existing,
|
|
203
|
+
'metadata': metadata
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
def bulk_download(self, urls: list[str]) -> dict[str, ModelWithLocation | Exception]:
|
|
207
|
+
"""Download multiple models from URLs.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
urls: List of URLs to download
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dictionary mapping URLs to ModelIndex or Exception
|
|
214
|
+
"""
|
|
215
|
+
results = {}
|
|
216
|
+
|
|
217
|
+
for url in urls:
|
|
218
|
+
try:
|
|
219
|
+
model = self.download_from_url(url)
|
|
220
|
+
results[url] = model
|
|
221
|
+
logger.info(f"✓ Downloaded: {url}")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
results[url] = e
|
|
224
|
+
logger.error(f"✗ Failed to download {url}: {e}")
|
|
225
|
+
|
|
226
|
+
return results
|
|
227
|
+
|
|
228
|
+
def redownload_from_sources(self, model_hash: str) -> ModelWithLocation | None:
|
|
229
|
+
"""Attempt to redownload a model from its known sources.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
model_hash: Hash of model to redownload
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
ModelIndex if successful, None if no sources or download failed
|
|
236
|
+
"""
|
|
237
|
+
sources = self.model_repository.get_sources(model_hash)
|
|
238
|
+
|
|
239
|
+
if not sources:
|
|
240
|
+
logger.warning(f"No sources found for model {model_hash[:8]}...")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
for source in sources:
|
|
244
|
+
try:
|
|
245
|
+
logger.info(f"Attempting redownload from {source['type']}: {source['url']}")
|
|
246
|
+
return self.download_from_url(source['url'])
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.warning(f"Failed to redownload from {source['url']}: {e}")
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
logger.error(f"All redownload attempts failed for {model_hash[:8]}...")
|
|
252
|
+
return None
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""ModelSymlinkManager - Creates and manages symlink from ComfyUI/models to global models directory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..logging.logging_config import get_logger
|
|
8
|
+
from ..models.exceptions import CDEnvironmentError
|
|
9
|
+
from ..utils.symlink_utils import (
|
|
10
|
+
is_link,
|
|
11
|
+
create_platform_link,
|
|
12
|
+
is_safe_to_delete,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModelSymlinkManager:
|
|
19
|
+
"""Manages symlink/junction from ComfyUI/models to global models directory."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, comfyui_path: Path, global_models_path: Path):
|
|
22
|
+
"""Initialize ModelSymlinkManager.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
comfyui_path: Path to ComfyUI directory
|
|
26
|
+
global_models_path: Path to global models directory
|
|
27
|
+
"""
|
|
28
|
+
self.comfyui_path = comfyui_path
|
|
29
|
+
self.global_models_path = global_models_path
|
|
30
|
+
self.models_link_path = comfyui_path / "models"
|
|
31
|
+
|
|
32
|
+
def create_symlink(self) -> None:
|
|
33
|
+
"""Create symlink/junction from ComfyUI/models to global models.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
CDEnvironmentError: If global models directory doesn't exist, or if models/
|
|
37
|
+
exists with actual content
|
|
38
|
+
"""
|
|
39
|
+
# Check global models directory exists
|
|
40
|
+
if not self.global_models_path.exists():
|
|
41
|
+
raise CDEnvironmentError(
|
|
42
|
+
f"Global models directory does not exist: {self.global_models_path}\n"
|
|
43
|
+
f"Create it first: mkdir -p {self.global_models_path}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Handle existing models/ path
|
|
47
|
+
if self.models_link_path.exists():
|
|
48
|
+
if is_link(self.models_link_path):
|
|
49
|
+
# Already a link - check target
|
|
50
|
+
if self._resolve_link() == self.global_models_path.resolve():
|
|
51
|
+
logger.debug("Link already points to correct target")
|
|
52
|
+
return
|
|
53
|
+
else:
|
|
54
|
+
# Wrong target - recreate
|
|
55
|
+
logger.info(
|
|
56
|
+
f"Updating link target: {self._resolve_link()} → {self.global_models_path}"
|
|
57
|
+
)
|
|
58
|
+
self.models_link_path.unlink()
|
|
59
|
+
else:
|
|
60
|
+
# Real directory - check if safe to delete
|
|
61
|
+
safe_files = {".gitkeep", ".gitignore", "Put models here.txt"}
|
|
62
|
+
if is_safe_to_delete(self.models_link_path, safe_files):
|
|
63
|
+
logger.info(
|
|
64
|
+
"Removing ComfyUI default models/ directory (empty or placeholder files only)"
|
|
65
|
+
)
|
|
66
|
+
shutil.rmtree(self.models_link_path)
|
|
67
|
+
else:
|
|
68
|
+
raise CDEnvironmentError(
|
|
69
|
+
f"models/ directory exists with content: {self.models_link_path}\n"
|
|
70
|
+
f"Manual action required:\n"
|
|
71
|
+
f" 1. Backup if needed: mv {self.models_link_path} {self.models_link_path}.backup\n"
|
|
72
|
+
f" 2. Remove: rm -rf {self.models_link_path}\n"
|
|
73
|
+
f" 3. Retry: comfygit sync"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Ensure parent directory (ComfyUI/) exists
|
|
77
|
+
self.comfyui_path.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
# Create platform-appropriate link
|
|
80
|
+
create_platform_link(self.models_link_path, self.global_models_path, "models")
|
|
81
|
+
logger.info(
|
|
82
|
+
f"Created model link: {self.models_link_path} → {self.global_models_path}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def validate_symlink(self) -> bool:
|
|
86
|
+
"""Check if link exists and points to correct target.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if link is valid, False otherwise
|
|
90
|
+
"""
|
|
91
|
+
if not self.models_link_path.exists():
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
if not is_link(self.models_link_path):
|
|
95
|
+
logger.warning(f"models/ is not a link: {self.models_link_path}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
target = self._resolve_link()
|
|
99
|
+
if target != self.global_models_path.resolve():
|
|
100
|
+
logger.warning(
|
|
101
|
+
f"Link points to wrong target:\n"
|
|
102
|
+
f" Expected: {self.global_models_path}\n"
|
|
103
|
+
f" Actual: {target}"
|
|
104
|
+
)
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def remove_symlink(self) -> None:
|
|
110
|
+
"""Remove symlink/junction safely.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
CDEnvironmentError: If models/ is not a link (prevents accidental data loss)
|
|
114
|
+
"""
|
|
115
|
+
if not self.models_link_path.exists():
|
|
116
|
+
return # Nothing to remove
|
|
117
|
+
|
|
118
|
+
if not is_link(self.models_link_path):
|
|
119
|
+
raise CDEnvironmentError(
|
|
120
|
+
f"Cannot remove models/: not a link\n"
|
|
121
|
+
f"Manual deletion required: {self.models_link_path}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
self.models_link_path.unlink()
|
|
126
|
+
logger.info(f"Removed model link: {self.models_link_path}")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise CDEnvironmentError(f"Failed to remove link: {e}") from e
|
|
129
|
+
|
|
130
|
+
def get_status(self) -> dict:
|
|
131
|
+
"""Get current link status for debugging.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dictionary with status information
|
|
135
|
+
"""
|
|
136
|
+
if not self.models_link_path.exists():
|
|
137
|
+
return {
|
|
138
|
+
"exists": False,
|
|
139
|
+
"is_symlink": False,
|
|
140
|
+
"is_valid": False,
|
|
141
|
+
"target": None,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
is_symlink_or_junction = is_link(self.models_link_path)
|
|
145
|
+
target = self._resolve_link() if is_symlink_or_junction else None
|
|
146
|
+
is_valid = (
|
|
147
|
+
is_symlink_or_junction and target == self.global_models_path.resolve()
|
|
148
|
+
if target
|
|
149
|
+
else False
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"exists": True,
|
|
154
|
+
"is_symlink": is_symlink_or_junction,
|
|
155
|
+
"is_valid": is_valid,
|
|
156
|
+
"target": str(target) if target else None,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def _resolve_link(self) -> Path:
|
|
160
|
+
"""Get symlink target path.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Resolved path of symlink target
|
|
164
|
+
"""
|
|
165
|
+
# Use resolve() which works for both symlinks and junctions
|
|
166
|
+
return self.models_link_path.resolve()
|