devsync 0.5.5__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.
- aiconfigkit/__init__.py +0 -0
- aiconfigkit/__main__.py +6 -0
- aiconfigkit/ai_tools/__init__.py +0 -0
- aiconfigkit/ai_tools/base.py +236 -0
- aiconfigkit/ai_tools/capability_registry.py +262 -0
- aiconfigkit/ai_tools/claude.py +91 -0
- aiconfigkit/ai_tools/claude_desktop.py +97 -0
- aiconfigkit/ai_tools/cline.py +92 -0
- aiconfigkit/ai_tools/copilot.py +92 -0
- aiconfigkit/ai_tools/cursor.py +109 -0
- aiconfigkit/ai_tools/detector.py +169 -0
- aiconfigkit/ai_tools/kiro.py +85 -0
- aiconfigkit/ai_tools/mcp_syncer.py +291 -0
- aiconfigkit/ai_tools/roo.py +110 -0
- aiconfigkit/ai_tools/translator.py +390 -0
- aiconfigkit/ai_tools/winsurf.py +102 -0
- aiconfigkit/cli/__init__.py +0 -0
- aiconfigkit/cli/delete.py +118 -0
- aiconfigkit/cli/download.py +274 -0
- aiconfigkit/cli/install.py +237 -0
- aiconfigkit/cli/install_new.py +937 -0
- aiconfigkit/cli/list.py +275 -0
- aiconfigkit/cli/main.py +454 -0
- aiconfigkit/cli/mcp_configure.py +232 -0
- aiconfigkit/cli/mcp_install.py +166 -0
- aiconfigkit/cli/mcp_sync.py +165 -0
- aiconfigkit/cli/package.py +383 -0
- aiconfigkit/cli/package_create.py +323 -0
- aiconfigkit/cli/package_install.py +472 -0
- aiconfigkit/cli/template.py +19 -0
- aiconfigkit/cli/template_backup.py +261 -0
- aiconfigkit/cli/template_init.py +499 -0
- aiconfigkit/cli/template_install.py +261 -0
- aiconfigkit/cli/template_list.py +172 -0
- aiconfigkit/cli/template_uninstall.py +146 -0
- aiconfigkit/cli/template_update.py +225 -0
- aiconfigkit/cli/template_validate.py +234 -0
- aiconfigkit/cli/tools.py +47 -0
- aiconfigkit/cli/uninstall.py +125 -0
- aiconfigkit/cli/update.py +309 -0
- aiconfigkit/core/__init__.py +0 -0
- aiconfigkit/core/checksum.py +211 -0
- aiconfigkit/core/component_detector.py +905 -0
- aiconfigkit/core/conflict_resolution.py +329 -0
- aiconfigkit/core/git_operations.py +539 -0
- aiconfigkit/core/mcp/__init__.py +1 -0
- aiconfigkit/core/mcp/credentials.py +279 -0
- aiconfigkit/core/mcp/manager.py +308 -0
- aiconfigkit/core/mcp/set_manager.py +1 -0
- aiconfigkit/core/mcp/validator.py +1 -0
- aiconfigkit/core/models.py +1661 -0
- aiconfigkit/core/package_creator.py +743 -0
- aiconfigkit/core/package_manifest.py +248 -0
- aiconfigkit/core/repository.py +298 -0
- aiconfigkit/core/secret_detector.py +438 -0
- aiconfigkit/core/template_manifest.py +283 -0
- aiconfigkit/core/version.py +201 -0
- aiconfigkit/storage/__init__.py +0 -0
- aiconfigkit/storage/library.py +429 -0
- aiconfigkit/storage/mcp_tracker.py +1 -0
- aiconfigkit/storage/package_tracker.py +234 -0
- aiconfigkit/storage/template_library.py +229 -0
- aiconfigkit/storage/template_tracker.py +296 -0
- aiconfigkit/storage/tracker.py +416 -0
- aiconfigkit/tui/__init__.py +5 -0
- aiconfigkit/tui/installer.py +511 -0
- aiconfigkit/utils/__init__.py +0 -0
- aiconfigkit/utils/atomic_write.py +90 -0
- aiconfigkit/utils/backup.py +169 -0
- aiconfigkit/utils/dotenv.py +128 -0
- aiconfigkit/utils/git_helpers.py +187 -0
- aiconfigkit/utils/logging.py +60 -0
- aiconfigkit/utils/namespace.py +134 -0
- aiconfigkit/utils/paths.py +205 -0
- aiconfigkit/utils/project.py +109 -0
- aiconfigkit/utils/streaming.py +216 -0
- aiconfigkit/utils/ui.py +194 -0
- aiconfigkit/utils/validation.py +187 -0
- devsync-0.5.5.dist-info/LICENSE +21 -0
- devsync-0.5.5.dist-info/METADATA +477 -0
- devsync-0.5.5.dist-info/RECORD +84 -0
- devsync-0.5.5.dist-info/WHEEL +5 -0
- devsync-0.5.5.dist-info/entry_points.txt +2 -0
- devsync-0.5.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Cross-platform path utilities for AI coding tool directories."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_home_directory() -> Path:
|
|
9
|
+
"""Get user's home directory in a cross-platform way."""
|
|
10
|
+
return Path.home()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_cursor_config_dir() -> Path:
|
|
14
|
+
"""Get Cursor configuration directory based on platform."""
|
|
15
|
+
home = get_home_directory()
|
|
16
|
+
|
|
17
|
+
if os.name == "nt": # Windows
|
|
18
|
+
return home / "AppData" / "Roaming" / "Cursor" / "User" / "globalStorage"
|
|
19
|
+
elif os.name == "posix":
|
|
20
|
+
if "darwin" in os.uname().sysname.lower(): # macOS
|
|
21
|
+
return home / "Library" / "Application Support" / "Cursor" / "User" / "globalStorage"
|
|
22
|
+
else: # Linux
|
|
23
|
+
return home / ".config" / "Cursor" / "User" / "globalStorage"
|
|
24
|
+
|
|
25
|
+
raise OSError(f"Unsupported operating system: {os.name}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_copilot_config_dir() -> Path:
|
|
29
|
+
"""Get GitHub Copilot (VS Code) configuration directory based on platform."""
|
|
30
|
+
home = get_home_directory()
|
|
31
|
+
|
|
32
|
+
if os.name == "nt": # Windows
|
|
33
|
+
return home / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "github.copilot"
|
|
34
|
+
elif os.name == "posix":
|
|
35
|
+
if "darwin" in os.uname().sysname.lower(): # macOS
|
|
36
|
+
return home / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "github.copilot"
|
|
37
|
+
else: # Linux
|
|
38
|
+
return home / ".config" / "Code" / "User" / "globalStorage" / "github.copilot"
|
|
39
|
+
|
|
40
|
+
raise OSError(f"Unsupported operating system: {os.name}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_winsurf_config_dir() -> Path:
|
|
44
|
+
"""Get Windsurf configuration directory based on platform."""
|
|
45
|
+
home = get_home_directory()
|
|
46
|
+
|
|
47
|
+
if os.name == "nt": # Windows
|
|
48
|
+
return home / "AppData" / "Roaming" / "Windsurf" / "User" / "globalStorage"
|
|
49
|
+
elif os.name == "posix":
|
|
50
|
+
if "darwin" in os.uname().sysname.lower(): # macOS
|
|
51
|
+
return home / "Library" / "Application Support" / "Windsurf" / "User" / "globalStorage"
|
|
52
|
+
else: # Linux
|
|
53
|
+
return home / ".config" / "Windsurf" / "User" / "globalStorage"
|
|
54
|
+
|
|
55
|
+
raise OSError(f"Unsupported operating system: {os.name}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_kiro_config_dir() -> Path:
|
|
59
|
+
"""Get Kiro configuration directory based on platform."""
|
|
60
|
+
home = get_home_directory()
|
|
61
|
+
|
|
62
|
+
if os.name == "nt": # Windows
|
|
63
|
+
return home / "AppData" / "Roaming" / "Kiro" / "User" / "globalStorage"
|
|
64
|
+
elif os.name == "posix":
|
|
65
|
+
if "darwin" in os.uname().sysname.lower(): # macOS
|
|
66
|
+
return home / "Library" / "Application Support" / "Kiro" / "User" / "globalStorage"
|
|
67
|
+
else: # Linux
|
|
68
|
+
return home / ".config" / "Kiro" / "User" / "globalStorage"
|
|
69
|
+
|
|
70
|
+
raise OSError(f"Unsupported operating system: {os.name}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_cline_config_dir() -> Path:
|
|
74
|
+
"""Get Cline (VS Code extension) configuration directory based on platform."""
|
|
75
|
+
home = get_home_directory()
|
|
76
|
+
|
|
77
|
+
if os.name == "nt": # Windows
|
|
78
|
+
return home / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev"
|
|
79
|
+
elif os.name == "posix":
|
|
80
|
+
if "darwin" in os.uname().sysname.lower(): # macOS
|
|
81
|
+
return (
|
|
82
|
+
home / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev"
|
|
83
|
+
)
|
|
84
|
+
else: # Linux
|
|
85
|
+
return home / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev"
|
|
86
|
+
|
|
87
|
+
raise OSError(f"Unsupported operating system: {os.name}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_roo_config_dir() -> Path:
|
|
91
|
+
"""Get Roo Code (VS Code extension) configuration directory based on platform."""
|
|
92
|
+
home = get_home_directory()
|
|
93
|
+
|
|
94
|
+
if os.name == "nt": # Windows
|
|
95
|
+
return home / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline"
|
|
96
|
+
elif os.name == "posix":
|
|
97
|
+
if "darwin" in os.uname().sysname.lower(): # macOS
|
|
98
|
+
return (
|
|
99
|
+
home
|
|
100
|
+
/ "Library"
|
|
101
|
+
/ "Application Support"
|
|
102
|
+
/ "Code"
|
|
103
|
+
/ "User"
|
|
104
|
+
/ "globalStorage"
|
|
105
|
+
/ "rooveterinaryinc.roo-cline"
|
|
106
|
+
)
|
|
107
|
+
else: # Linux
|
|
108
|
+
return home / ".config" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline"
|
|
109
|
+
|
|
110
|
+
raise OSError(f"Unsupported operating system: {os.name}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_claude_config_dir() -> Path:
|
|
114
|
+
"""Get Claude Code configuration directory based on platform."""
|
|
115
|
+
home = get_home_directory()
|
|
116
|
+
|
|
117
|
+
# Claude Code uses ~/.claude/rules/ for global rules
|
|
118
|
+
# This is consistent across all platforms
|
|
119
|
+
return home / ".claude" / "rules"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_claude_desktop_config_path() -> Path:
|
|
123
|
+
"""Get Claude Desktop MCP configuration file path based on platform."""
|
|
124
|
+
home = get_home_directory()
|
|
125
|
+
|
|
126
|
+
if os.name == "nt": # Windows
|
|
127
|
+
return home / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
|
|
128
|
+
elif os.name == "posix":
|
|
129
|
+
if "darwin" in os.uname().sysname.lower(): # macOS
|
|
130
|
+
return home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
131
|
+
else: # Linux
|
|
132
|
+
return home / ".config" / "Claude" / "claude_desktop_config.json"
|
|
133
|
+
|
|
134
|
+
raise OSError(f"Unsupported operating system: {os.name}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_cursor_mcp_config_path() -> Path:
|
|
138
|
+
"""Get Cursor MCP configuration file path based on platform."""
|
|
139
|
+
# Cursor doesn't natively support MCP yet, but we'll prepare for it
|
|
140
|
+
# Expected location would be similar to their settings structure
|
|
141
|
+
config_dir = get_cursor_config_dir()
|
|
142
|
+
return config_dir / "mcp_config.json"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_windsurf_mcp_config_path() -> Path:
|
|
146
|
+
"""Get Windsurf MCP configuration file path based on platform."""
|
|
147
|
+
# Windsurf may have MCP support in future
|
|
148
|
+
# Expected location would be similar to their settings structure
|
|
149
|
+
config_dir = get_winsurf_config_dir()
|
|
150
|
+
return config_dir / "mcp_config.json"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_instructionkit_data_dir() -> Path:
|
|
154
|
+
"""Get InstructionKit data directory for tracking installations."""
|
|
155
|
+
home = get_home_directory()
|
|
156
|
+
data_dir = home / ".instructionkit"
|
|
157
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
return data_dir
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_library_dir() -> Path:
|
|
162
|
+
"""Get InstructionKit library directory for downloaded instructions."""
|
|
163
|
+
library_dir = get_instructionkit_data_dir() / "library"
|
|
164
|
+
library_dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
return library_dir
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_installation_tracker_path() -> Path:
|
|
169
|
+
"""Get path to installation tracking JSON file."""
|
|
170
|
+
return get_instructionkit_data_dir() / "installations.json"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def ensure_directory_exists(path: Path) -> None:
|
|
174
|
+
"""Ensure a directory exists, creating it if necessary."""
|
|
175
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def safe_file_name(name: str) -> str:
|
|
179
|
+
"""Sanitize a string to be safe as a filename."""
|
|
180
|
+
# Replace unsafe characters with underscores
|
|
181
|
+
unsafe_chars = '<>:"/\\|?*'
|
|
182
|
+
safe_name = name
|
|
183
|
+
for char in unsafe_chars:
|
|
184
|
+
safe_name = safe_name.replace(char, "_")
|
|
185
|
+
return safe_name
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def resolve_conflict_name(original_path: Path, suffix: Optional[str] = None) -> Path:
|
|
189
|
+
"""Generate a new path for conflict resolution with optional suffix."""
|
|
190
|
+
if suffix:
|
|
191
|
+
stem = original_path.stem
|
|
192
|
+
extension = original_path.suffix
|
|
193
|
+
new_name = f"{stem}-{suffix}{extension}"
|
|
194
|
+
return original_path.parent / new_name
|
|
195
|
+
|
|
196
|
+
# Auto-increment: file.md -> file-1.md -> file-2.md
|
|
197
|
+
counter = 1
|
|
198
|
+
while True:
|
|
199
|
+
stem = original_path.stem
|
|
200
|
+
extension = original_path.suffix
|
|
201
|
+
new_name = f"{stem}-{counter}{extension}"
|
|
202
|
+
new_path = original_path.parent / new_name
|
|
203
|
+
if not new_path.exists():
|
|
204
|
+
return new_path
|
|
205
|
+
counter += 1
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Project detection utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_project_root(start_path: Optional[Path] = None) -> Optional[Path]:
|
|
8
|
+
"""
|
|
9
|
+
Find the project root directory by looking for common project markers.
|
|
10
|
+
|
|
11
|
+
Searches upward from the start path for files/directories that indicate
|
|
12
|
+
a project root:
|
|
13
|
+
- .git directory (Git repository)
|
|
14
|
+
- pyproject.toml (Python project)
|
|
15
|
+
- package.json (Node.js project)
|
|
16
|
+
- Cargo.toml (Rust project)
|
|
17
|
+
- go.mod (Go project)
|
|
18
|
+
- pom.xml (Java/Maven project)
|
|
19
|
+
- build.gradle (Java/Gradle project)
|
|
20
|
+
- composer.json (PHP project)
|
|
21
|
+
- Gemfile (Ruby project)
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
start_path: Directory to start searching from. Defaults to current directory.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path to project root if found, None otherwise
|
|
28
|
+
"""
|
|
29
|
+
if start_path is None:
|
|
30
|
+
start_path = Path.cwd()
|
|
31
|
+
else:
|
|
32
|
+
start_path = Path(start_path).resolve()
|
|
33
|
+
|
|
34
|
+
# Common project markers
|
|
35
|
+
markers = [
|
|
36
|
+
".git",
|
|
37
|
+
"pyproject.toml",
|
|
38
|
+
"package.json",
|
|
39
|
+
"Cargo.toml",
|
|
40
|
+
"go.mod",
|
|
41
|
+
"pom.xml",
|
|
42
|
+
"build.gradle",
|
|
43
|
+
"composer.json",
|
|
44
|
+
"Gemfile",
|
|
45
|
+
".project", # Eclipse project
|
|
46
|
+
"Makefile",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
current = start_path
|
|
50
|
+
|
|
51
|
+
# Search upward through parent directories
|
|
52
|
+
while True:
|
|
53
|
+
for marker in markers:
|
|
54
|
+
marker_path = current / marker
|
|
55
|
+
if marker_path.exists():
|
|
56
|
+
return current
|
|
57
|
+
|
|
58
|
+
# Check if we've reached the filesystem root
|
|
59
|
+
parent = current.parent
|
|
60
|
+
if parent == current:
|
|
61
|
+
# No project root found
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
current = parent
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_in_project() -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if the current directory is within a project.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if a project root can be found
|
|
73
|
+
"""
|
|
74
|
+
return find_project_root() is not None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_project_instructions_dir(project_root: Path, create: bool = True) -> Path:
|
|
78
|
+
"""
|
|
79
|
+
Get the directory for project-specific instructions.
|
|
80
|
+
|
|
81
|
+
Creates a .instructionkit directory in the project root for storing
|
|
82
|
+
project-specific instructions and metadata.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
project_root: Path to the project root directory
|
|
86
|
+
create: Whether to create the directory if it doesn't exist
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Path to project instructions directory
|
|
90
|
+
"""
|
|
91
|
+
instructions_dir = project_root / ".instructionkit"
|
|
92
|
+
|
|
93
|
+
if create:
|
|
94
|
+
instructions_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
|
|
96
|
+
return instructions_dir
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_project_installation_tracker_path(project_root: Path) -> Path:
|
|
100
|
+
"""
|
|
101
|
+
Get path to project-specific installation tracking file.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
project_root: Path to the project root directory
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Path to project installation tracking JSON file
|
|
108
|
+
"""
|
|
109
|
+
return get_project_instructions_dir(project_root) / "installations.json"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Streaming utilities for large file operations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StreamingError(Exception):
|
|
8
|
+
"""Raised when streaming operations fail."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def stream_copy_file(
|
|
14
|
+
source: Path,
|
|
15
|
+
destination: Path,
|
|
16
|
+
chunk_size: int = 8192,
|
|
17
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Copy a file in streaming chunks for memory-efficient large file handling.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
source: Source file path
|
|
24
|
+
destination: Destination file path
|
|
25
|
+
chunk_size: Size of chunks to read/write (default 8KB)
|
|
26
|
+
progress_callback: Optional callback for progress updates (bytes_copied, total_size)
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
FileNotFoundError: If source file doesn't exist
|
|
30
|
+
StreamingError: If copy operation fails
|
|
31
|
+
"""
|
|
32
|
+
if not source.exists():
|
|
33
|
+
raise FileNotFoundError(f"Source file not found: {source}")
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Get total file size for progress tracking
|
|
37
|
+
total_size = source.stat().st_size
|
|
38
|
+
bytes_copied = 0
|
|
39
|
+
|
|
40
|
+
# Ensure destination directory exists
|
|
41
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
# Open both files and copy in chunks
|
|
44
|
+
with open(source, "rb") as src_file:
|
|
45
|
+
with open(destination, "wb") as dst_file:
|
|
46
|
+
while True:
|
|
47
|
+
chunk = src_file.read(chunk_size)
|
|
48
|
+
if not chunk:
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
dst_file.write(chunk)
|
|
52
|
+
bytes_copied += len(chunk)
|
|
53
|
+
|
|
54
|
+
# Call progress callback if provided
|
|
55
|
+
if progress_callback:
|
|
56
|
+
progress_callback(bytes_copied, total_size)
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
# Clean up partial destination file on error
|
|
60
|
+
if destination.exists():
|
|
61
|
+
destination.unlink()
|
|
62
|
+
raise StreamingError(f"Failed to copy file: {e}") from e
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def stream_copy_with_verification(
|
|
66
|
+
source: Path,
|
|
67
|
+
destination: Path,
|
|
68
|
+
checksum: Optional[str] = None,
|
|
69
|
+
chunk_size: int = 8192,
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Copy a file with optional checksum verification.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
source: Source file path
|
|
76
|
+
destination: Destination file path
|
|
77
|
+
checksum: Expected SHA-256 checksum (optional)
|
|
78
|
+
chunk_size: Size of chunks to read/write
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if copy and verification succeeded
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
FileNotFoundError: If source file doesn't exist
|
|
85
|
+
StreamingError: If copy or verification fails
|
|
86
|
+
"""
|
|
87
|
+
# First, copy the file
|
|
88
|
+
stream_copy_file(source, destination, chunk_size)
|
|
89
|
+
|
|
90
|
+
# Verify checksum if provided
|
|
91
|
+
if checksum:
|
|
92
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
93
|
+
|
|
94
|
+
actual_checksum = calculate_file_checksum(str(destination), "sha256")
|
|
95
|
+
|
|
96
|
+
if actual_checksum.lower() != checksum.lower():
|
|
97
|
+
# Delete corrupted file
|
|
98
|
+
destination.unlink()
|
|
99
|
+
raise StreamingError(f"Checksum verification failed! " f"Expected: {checksum}, Actual: {actual_checksum}")
|
|
100
|
+
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_file_size(file_path: Path) -> int:
|
|
105
|
+
"""
|
|
106
|
+
Get file size in bytes.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_path: Path to file
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
File size in bytes
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
FileNotFoundError: If file doesn't exist
|
|
116
|
+
"""
|
|
117
|
+
if not file_path.exists():
|
|
118
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
119
|
+
|
|
120
|
+
return file_path.stat().st_size
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def format_file_size(size_bytes: int) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Format file size in human-readable format.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
size_bytes: Size in bytes
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Formatted string (e.g., "1.5 MB", "100 KB")
|
|
132
|
+
"""
|
|
133
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
134
|
+
size = float(size_bytes)
|
|
135
|
+
unit_index = 0
|
|
136
|
+
|
|
137
|
+
while size >= 1024 and unit_index < len(units) - 1:
|
|
138
|
+
size /= 1024
|
|
139
|
+
unit_index += 1
|
|
140
|
+
|
|
141
|
+
if unit_index == 0:
|
|
142
|
+
# Bytes - no decimal places
|
|
143
|
+
return f"{int(size)} {units[unit_index]}"
|
|
144
|
+
else:
|
|
145
|
+
# Other units - 1 decimal place
|
|
146
|
+
return f"{size:.1f} {units[unit_index]}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def is_large_file(file_path: Path, threshold_mb: int = 10) -> bool:
|
|
150
|
+
"""
|
|
151
|
+
Check if file is considered "large" based on size threshold.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
file_path: Path to file
|
|
155
|
+
threshold_mb: Size threshold in megabytes (default 10 MB)
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if file size exceeds threshold
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
FileNotFoundError: If file doesn't exist
|
|
162
|
+
"""
|
|
163
|
+
size_bytes = get_file_size(file_path)
|
|
164
|
+
threshold_bytes = threshold_mb * 1024 * 1024
|
|
165
|
+
return size_bytes > threshold_bytes
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def copy_directory_tree(
|
|
169
|
+
source: Path,
|
|
170
|
+
destination: Path,
|
|
171
|
+
progress_callback: Optional[Callable[[str, int, int], None]] = None,
|
|
172
|
+
) -> int:
|
|
173
|
+
"""
|
|
174
|
+
Copy an entire directory tree with progress tracking.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
source: Source directory path
|
|
178
|
+
destination: Destination directory path
|
|
179
|
+
progress_callback: Optional callback(file_path, current, total)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Number of files copied
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
StreamingError: If copy operation fails
|
|
186
|
+
"""
|
|
187
|
+
if not source.is_dir():
|
|
188
|
+
raise StreamingError(f"Source is not a directory: {source}")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Count total files for progress tracking
|
|
192
|
+
all_files = list(source.rglob("*"))
|
|
193
|
+
total_files = len([f for f in all_files if f.is_file()])
|
|
194
|
+
files_copied = 0
|
|
195
|
+
|
|
196
|
+
# Recreate directory structure
|
|
197
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
|
|
199
|
+
for item in all_files:
|
|
200
|
+
if item.is_file():
|
|
201
|
+
# Calculate relative path
|
|
202
|
+
rel_path = item.relative_to(source)
|
|
203
|
+
dest_file = destination / rel_path
|
|
204
|
+
|
|
205
|
+
# Copy file
|
|
206
|
+
stream_copy_file(item, dest_file)
|
|
207
|
+
files_copied += 1
|
|
208
|
+
|
|
209
|
+
# Call progress callback
|
|
210
|
+
if progress_callback:
|
|
211
|
+
progress_callback(str(rel_path), files_copied, total_files)
|
|
212
|
+
|
|
213
|
+
return files_copied
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
raise StreamingError(f"Failed to copy directory tree: {e}") from e
|