griptape-nodes 0.60.3__py3-none-any.whl → 0.61.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.
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
- griptape_nodes/common/macro_parser/__init__.py +16 -1
- griptape_nodes/common/macro_parser/core.py +15 -3
- griptape_nodes/common/macro_parser/exceptions.py +99 -0
- griptape_nodes/common/macro_parser/formats.py +13 -4
- griptape_nodes/common/macro_parser/matching.py +5 -2
- griptape_nodes/common/macro_parser/parsing.py +48 -8
- griptape_nodes/common/macro_parser/resolution.py +23 -5
- griptape_nodes/common/project_templates/__init__.py +49 -0
- griptape_nodes/common/project_templates/default_project_template.py +92 -0
- griptape_nodes/common/project_templates/defaults/README.md +36 -0
- griptape_nodes/common/project_templates/defaults/project_template.yml +89 -0
- griptape_nodes/common/project_templates/directory.py +67 -0
- griptape_nodes/common/project_templates/loader.py +341 -0
- griptape_nodes/common/project_templates/project.py +252 -0
- griptape_nodes/common/project_templates/situation.py +155 -0
- griptape_nodes/common/project_templates/validation.py +140 -0
- griptape_nodes/exe_types/core_types.py +36 -3
- griptape_nodes/exe_types/node_types.py +4 -2
- griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
- griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
- griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
- griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
- griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/execution_events.py +41 -0
- griptape_nodes/retained_mode/events/node_events.py +90 -1
- griptape_nodes/retained_mode/events/os_events.py +108 -0
- griptape_nodes/retained_mode/events/parameter_events.py +1 -1
- griptape_nodes/retained_mode/events/project_events.py +413 -0
- griptape_nodes/retained_mode/events/workflow_events.py +19 -1
- griptape_nodes/retained_mode/griptape_nodes.py +9 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
- griptape_nodes/retained_mode/managers/event_manager.py +6 -9
- griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
- griptape_nodes/retained_mode/managers/library_manager.py +55 -42
- griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
- griptape_nodes/retained_mode/managers/node_manager.py +232 -0
- griptape_nodes/retained_mode/managers/os_manager.py +346 -1
- griptape_nodes/retained_mode/managers/project_manager.py +617 -0
- griptape_nodes/retained_mode/managers/settings.py +6 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +17 -71
- griptape_nodes/traits/button.py +18 -0
- {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
- {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
- {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/entry_points.txt +0 -0
|
@@ -15,6 +15,12 @@ from rich.console import Console
|
|
|
15
15
|
from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
|
|
16
16
|
from griptape_nodes.retained_mode.events.base_events import ResultDetails, ResultPayload
|
|
17
17
|
from griptape_nodes.retained_mode.events.os_events import (
|
|
18
|
+
CopyFileRequest,
|
|
19
|
+
CopyFileResultFailure,
|
|
20
|
+
CopyFileResultSuccess,
|
|
21
|
+
CopyTreeRequest,
|
|
22
|
+
CopyTreeResultFailure,
|
|
23
|
+
CopyTreeResultSuccess,
|
|
18
24
|
CreateFileRequest,
|
|
19
25
|
CreateFileResultFailure,
|
|
20
26
|
CreateFileResultSuccess,
|
|
@@ -73,6 +79,24 @@ class FileContentResult(NamedTuple):
|
|
|
73
79
|
file_size: int
|
|
74
80
|
|
|
75
81
|
|
|
82
|
+
@dataclass
|
|
83
|
+
class CopyTreeValidationResult:
|
|
84
|
+
"""Result from validating copy tree paths."""
|
|
85
|
+
|
|
86
|
+
source_normalized: str
|
|
87
|
+
dest_normalized: str
|
|
88
|
+
source_path: Path
|
|
89
|
+
destination_path: Path
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class CopyTreeStats:
|
|
94
|
+
"""Statistics from copying a directory tree."""
|
|
95
|
+
|
|
96
|
+
files_copied: int
|
|
97
|
+
total_bytes_copied: int
|
|
98
|
+
|
|
99
|
+
|
|
76
100
|
class OSManager:
|
|
77
101
|
"""A class to manage OS-level scenarios.
|
|
78
102
|
|
|
@@ -105,6 +129,14 @@ class OSManager:
|
|
|
105
129
|
request_type=WriteFileRequest, callback=self.on_write_file_request
|
|
106
130
|
)
|
|
107
131
|
|
|
132
|
+
event_manager.assign_manager_to_request_type(
|
|
133
|
+
request_type=CopyTreeRequest, callback=self.on_copy_tree_request
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
event_manager.assign_manager_to_request_type(
|
|
137
|
+
request_type=CopyFileRequest, callback=self.on_copy_file_request
|
|
138
|
+
)
|
|
139
|
+
|
|
108
140
|
# Register for app initialization event to setup system resources
|
|
109
141
|
event_manager.add_listener_to_app_event(AppInitializationComplete, self.on_app_initialization_complete)
|
|
110
142
|
|
|
@@ -194,7 +226,7 @@ class OSManager:
|
|
|
194
226
|
path_str = str(path.resolve())
|
|
195
227
|
|
|
196
228
|
# Windows long path handling (paths > WINDOWS_MAX_PATH chars need \\?\ prefix)
|
|
197
|
-
if self.is_windows() and len(path_str)
|
|
229
|
+
if self.is_windows() and len(path_str) >= WINDOWS_MAX_PATH and not path_str.startswith("\\\\?\\"):
|
|
198
230
|
# UNC paths (\\server\share) need \\?\UNC\ prefix
|
|
199
231
|
if path_str.startswith("\\\\"):
|
|
200
232
|
return f"\\\\?\\UNC\\{path_str[2:]}"
|
|
@@ -757,6 +789,30 @@ class OSManager:
|
|
|
757
789
|
result_details=f"File written successfully: {file_path}",
|
|
758
790
|
)
|
|
759
791
|
|
|
792
|
+
def _copy_file(self, src_path: Path, dest_path: Path) -> int:
|
|
793
|
+
"""Copy a single file from source to destination with platform path normalization.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
src_path: Source file path (Path object)
|
|
797
|
+
dest_path: Destination file path (Path object)
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
Number of bytes copied
|
|
801
|
+
|
|
802
|
+
Raises:
|
|
803
|
+
OSError: If copy operation fails
|
|
804
|
+
PermissionError: If permission denied
|
|
805
|
+
"""
|
|
806
|
+
# Normalize both paths for platform (handles Windows long paths)
|
|
807
|
+
src_normalized = self.normalize_path_for_platform(src_path)
|
|
808
|
+
dest_normalized = self.normalize_path_for_platform(dest_path)
|
|
809
|
+
|
|
810
|
+
# Copy file preserving metadata
|
|
811
|
+
shutil.copy2(src_normalized, dest_normalized)
|
|
812
|
+
|
|
813
|
+
# Return size of copied file
|
|
814
|
+
return os.path.getsize(src_normalized) # noqa: PTH202
|
|
815
|
+
|
|
760
816
|
def _write_file_content(self, normalized_path: str, content: str | bytes, encoding: str, *, append: bool) -> int:
|
|
761
817
|
"""Write content to a file and return bytes written.
|
|
762
818
|
|
|
@@ -1135,6 +1191,295 @@ class OSManager:
|
|
|
1135
1191
|
result_details=ResultDetails(message=details, level=logging.INFO),
|
|
1136
1192
|
)
|
|
1137
1193
|
|
|
1194
|
+
def on_copy_file_request(self, request: CopyFileRequest) -> ResultPayload: # noqa: PLR0911, C901
|
|
1195
|
+
"""Handle a request to copy a single file."""
|
|
1196
|
+
# Resolve source path
|
|
1197
|
+
try:
|
|
1198
|
+
source_path = self._resolve_file_path(request.source_path, workspace_only=False)
|
|
1199
|
+
source_normalized = self.normalize_path_for_platform(source_path)
|
|
1200
|
+
except (ValueError, RuntimeError) as e:
|
|
1201
|
+
msg = f"Invalid source path: {e}"
|
|
1202
|
+
logger.error(msg)
|
|
1203
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1204
|
+
|
|
1205
|
+
# Check if source exists
|
|
1206
|
+
if not Path(source_normalized).exists():
|
|
1207
|
+
msg = f"Source file does not exist: {source_path}"
|
|
1208
|
+
logger.error(msg)
|
|
1209
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.FILE_NOT_FOUND, result_details=msg)
|
|
1210
|
+
|
|
1211
|
+
# Check if source is a file (not a directory)
|
|
1212
|
+
if not Path(source_normalized).is_file():
|
|
1213
|
+
msg = f"Source path is not a file: {source_path}"
|
|
1214
|
+
logger.error(msg)
|
|
1215
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1216
|
+
|
|
1217
|
+
# Resolve destination path
|
|
1218
|
+
try:
|
|
1219
|
+
destination_path = self._resolve_file_path(request.destination_path, workspace_only=False)
|
|
1220
|
+
dest_normalized = self.normalize_path_for_platform(destination_path)
|
|
1221
|
+
except (ValueError, RuntimeError) as e:
|
|
1222
|
+
msg = f"Invalid destination path: {e}"
|
|
1223
|
+
logger.error(msg)
|
|
1224
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1225
|
+
|
|
1226
|
+
# Check if destination already exists (unless overwrite is True)
|
|
1227
|
+
if Path(dest_normalized).exists() and not request.overwrite:
|
|
1228
|
+
msg = f"Destination file already exists: {destination_path}"
|
|
1229
|
+
logger.error(msg)
|
|
1230
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1231
|
+
|
|
1232
|
+
# Create parent directory if it doesn't exist
|
|
1233
|
+
dest_parent = Path(dest_normalized).parent
|
|
1234
|
+
if not dest_parent.exists():
|
|
1235
|
+
try:
|
|
1236
|
+
dest_parent.mkdir(parents=True)
|
|
1237
|
+
except PermissionError as e:
|
|
1238
|
+
msg = f"Permission denied creating parent directory {dest_parent}: {e}"
|
|
1239
|
+
logger.error(msg)
|
|
1240
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.PERMISSION_DENIED, result_details=msg)
|
|
1241
|
+
except OSError as e:
|
|
1242
|
+
msg = f"I/O error creating parent directory {dest_parent}: {e}"
|
|
1243
|
+
logger.error(msg)
|
|
1244
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.IO_ERROR, result_details=msg)
|
|
1245
|
+
|
|
1246
|
+
# Copy the file
|
|
1247
|
+
try:
|
|
1248
|
+
bytes_copied = self._copy_file(source_path, destination_path)
|
|
1249
|
+
except PermissionError as e:
|
|
1250
|
+
msg = f"Permission denied copying {source_path} to {destination_path}: {e}"
|
|
1251
|
+
logger.error(msg)
|
|
1252
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.PERMISSION_DENIED, result_details=msg)
|
|
1253
|
+
except OSError as e:
|
|
1254
|
+
if "No space left" in str(e) or "Disk full" in str(e):
|
|
1255
|
+
msg = f"Disk full copying {source_path} to {destination_path}: {e}"
|
|
1256
|
+
logger.error(msg)
|
|
1257
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.DISK_FULL, result_details=msg)
|
|
1258
|
+
|
|
1259
|
+
msg = f"I/O error copying {source_path} to {destination_path}: {e}"
|
|
1260
|
+
logger.error(msg)
|
|
1261
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.IO_ERROR, result_details=msg)
|
|
1262
|
+
except Exception as e:
|
|
1263
|
+
msg = f"Unexpected error copying {source_path} to {destination_path}: {type(e).__name__}: {e}"
|
|
1264
|
+
logger.error(msg)
|
|
1265
|
+
return CopyFileResultFailure(failure_reason=FileIOFailureReason.UNKNOWN, result_details=msg)
|
|
1266
|
+
|
|
1267
|
+
# SUCCESS PATH
|
|
1268
|
+
return CopyFileResultSuccess(
|
|
1269
|
+
source_path=str(source_path),
|
|
1270
|
+
destination_path=str(destination_path),
|
|
1271
|
+
bytes_copied=bytes_copied,
|
|
1272
|
+
result_details=f"File copied successfully: {source_path} -> {destination_path}",
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
def _validate_copy_tree_paths(
|
|
1276
|
+
self, source_str: str, dest_str: str, *, dirs_exist_ok: bool
|
|
1277
|
+
) -> CopyTreeValidationResult | CopyTreeResultFailure:
|
|
1278
|
+
"""Validate and normalize source and destination paths for copy tree operation.
|
|
1279
|
+
|
|
1280
|
+
Returns:
|
|
1281
|
+
CopyTreeValidationResult on success, CopyTreeResultFailure on validation failure
|
|
1282
|
+
"""
|
|
1283
|
+
# Resolve and normalize source path
|
|
1284
|
+
try:
|
|
1285
|
+
source_path = self._resolve_file_path(source_str, workspace_only=False)
|
|
1286
|
+
source_normalized = self.normalize_path_for_platform(source_path)
|
|
1287
|
+
except (ValueError, RuntimeError) as e:
|
|
1288
|
+
msg = f"Invalid source path: {e}"
|
|
1289
|
+
logger.error(msg)
|
|
1290
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1291
|
+
|
|
1292
|
+
# Check if source exists
|
|
1293
|
+
if not Path(source_normalized).exists():
|
|
1294
|
+
msg = f"Source path does not exist: {source_path}"
|
|
1295
|
+
logger.error(msg)
|
|
1296
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.FILE_NOT_FOUND, result_details=msg)
|
|
1297
|
+
|
|
1298
|
+
# Check if source is a directory
|
|
1299
|
+
if not Path(source_normalized).is_dir():
|
|
1300
|
+
msg = f"Source path is not a directory: {source_path}"
|
|
1301
|
+
logger.error(msg)
|
|
1302
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1303
|
+
|
|
1304
|
+
# Resolve and normalize destination path
|
|
1305
|
+
try:
|
|
1306
|
+
destination_path = self._resolve_file_path(dest_str, workspace_only=False)
|
|
1307
|
+
dest_normalized = self.normalize_path_for_platform(destination_path)
|
|
1308
|
+
except (ValueError, RuntimeError) as e:
|
|
1309
|
+
msg = f"Invalid destination path: {e}"
|
|
1310
|
+
logger.error(msg)
|
|
1311
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1312
|
+
|
|
1313
|
+
# Check if destination already exists (unless dirs_exist_ok is True)
|
|
1314
|
+
if Path(dest_normalized).exists() and not dirs_exist_ok:
|
|
1315
|
+
msg = f"Destination path already exists: {destination_path}"
|
|
1316
|
+
logger.error(msg)
|
|
1317
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.INVALID_PATH, result_details=msg)
|
|
1318
|
+
|
|
1319
|
+
return CopyTreeValidationResult(
|
|
1320
|
+
source_normalized=source_normalized,
|
|
1321
|
+
dest_normalized=dest_normalized,
|
|
1322
|
+
source_path=source_path,
|
|
1323
|
+
destination_path=destination_path,
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
def _copy_directory_tree( # noqa: PLR0912, C901
|
|
1327
|
+
self,
|
|
1328
|
+
source_normalized: str,
|
|
1329
|
+
dest_normalized: str,
|
|
1330
|
+
*,
|
|
1331
|
+
symlinks: bool,
|
|
1332
|
+
ignore_dangling_symlinks: bool,
|
|
1333
|
+
ignore_patterns: list[str] | None = None,
|
|
1334
|
+
) -> CopyTreeStats:
|
|
1335
|
+
"""Copy directory tree from source to destination.
|
|
1336
|
+
|
|
1337
|
+
Args:
|
|
1338
|
+
source_normalized: Normalized source path
|
|
1339
|
+
dest_normalized: Normalized destination path
|
|
1340
|
+
symlinks: If True, copy symbolic links as links
|
|
1341
|
+
ignore_dangling_symlinks: If True, ignore dangling symlinks
|
|
1342
|
+
ignore_patterns: List of glob patterns to ignore (e.g., ["__pycache__", "*.pyc"])
|
|
1343
|
+
|
|
1344
|
+
Returns:
|
|
1345
|
+
CopyTreeStats with files copied and bytes copied
|
|
1346
|
+
|
|
1347
|
+
Raises:
|
|
1348
|
+
OSError: If copy operation fails
|
|
1349
|
+
PermissionError: If permission denied
|
|
1350
|
+
"""
|
|
1351
|
+
from fnmatch import fnmatch
|
|
1352
|
+
|
|
1353
|
+
files_copied = 0
|
|
1354
|
+
total_bytes_copied = 0
|
|
1355
|
+
ignore_patterns = ignore_patterns or []
|
|
1356
|
+
|
|
1357
|
+
def should_ignore(name: str) -> bool:
|
|
1358
|
+
"""Check if a file/directory name matches any ignore pattern."""
|
|
1359
|
+
return any(fnmatch(name, pattern) for pattern in ignore_patterns)
|
|
1360
|
+
|
|
1361
|
+
# Create destination directory if it doesn't exist
|
|
1362
|
+
dest_path_obj = Path(dest_normalized)
|
|
1363
|
+
if not dest_path_obj.exists():
|
|
1364
|
+
dest_path_obj.mkdir(parents=True)
|
|
1365
|
+
|
|
1366
|
+
# Walk through source directory and copy files/directories
|
|
1367
|
+
for root, dirs, files in os.walk(source_normalized):
|
|
1368
|
+
# Calculate relative path from source
|
|
1369
|
+
root_path = Path(root)
|
|
1370
|
+
source_path_obj = Path(source_normalized)
|
|
1371
|
+
rel_path = root_path.relative_to(source_path_obj)
|
|
1372
|
+
|
|
1373
|
+
# Create corresponding directory in destination
|
|
1374
|
+
if str(rel_path) != ".":
|
|
1375
|
+
dest_dir = dest_path_obj / rel_path
|
|
1376
|
+
else:
|
|
1377
|
+
dest_dir = dest_path_obj
|
|
1378
|
+
|
|
1379
|
+
# Filter out ignored directories and create remaining ones
|
|
1380
|
+
dirs_to_remove = []
|
|
1381
|
+
for dir_name in dirs:
|
|
1382
|
+
if should_ignore(dir_name):
|
|
1383
|
+
dirs_to_remove.append(dir_name)
|
|
1384
|
+
continue
|
|
1385
|
+
|
|
1386
|
+
src_dir = root_path / dir_name
|
|
1387
|
+
dst_dir = dest_dir / dir_name
|
|
1388
|
+
|
|
1389
|
+
# Handle symlinks if requested
|
|
1390
|
+
if src_dir.is_symlink():
|
|
1391
|
+
if symlinks:
|
|
1392
|
+
link_target = src_dir.readlink()
|
|
1393
|
+
dst_dir.symlink_to(link_target)
|
|
1394
|
+
continue
|
|
1395
|
+
|
|
1396
|
+
if not dst_dir.exists():
|
|
1397
|
+
dst_dir.mkdir(parents=True)
|
|
1398
|
+
|
|
1399
|
+
# Remove ignored directories from dirs list to prevent os.walk from descending into them
|
|
1400
|
+
for dir_name in dirs_to_remove:
|
|
1401
|
+
dirs.remove(dir_name)
|
|
1402
|
+
|
|
1403
|
+
# Copy files
|
|
1404
|
+
for file_name in files:
|
|
1405
|
+
# Skip ignored files
|
|
1406
|
+
if should_ignore(file_name):
|
|
1407
|
+
continue
|
|
1408
|
+
|
|
1409
|
+
src_file = root_path / file_name
|
|
1410
|
+
dst_file = dest_dir / file_name
|
|
1411
|
+
|
|
1412
|
+
# Handle symlinks if requested
|
|
1413
|
+
if src_file.is_symlink():
|
|
1414
|
+
if symlinks:
|
|
1415
|
+
try:
|
|
1416
|
+
link_target = src_file.readlink()
|
|
1417
|
+
dst_file.symlink_to(link_target)
|
|
1418
|
+
except OSError:
|
|
1419
|
+
if not ignore_dangling_symlinks:
|
|
1420
|
+
raise
|
|
1421
|
+
continue
|
|
1422
|
+
|
|
1423
|
+
# Copy file
|
|
1424
|
+
bytes_copied = self._copy_file(src_file, dst_file)
|
|
1425
|
+
files_copied += 1
|
|
1426
|
+
total_bytes_copied += bytes_copied
|
|
1427
|
+
|
|
1428
|
+
return CopyTreeStats(files_copied=files_copied, total_bytes_copied=total_bytes_copied)
|
|
1429
|
+
|
|
1430
|
+
def on_copy_tree_request(self, request: CopyTreeRequest) -> ResultPayload:
|
|
1431
|
+
"""Handle a request to copy a directory tree."""
|
|
1432
|
+
# Validate paths
|
|
1433
|
+
validation_result = self._validate_copy_tree_paths(
|
|
1434
|
+
request.source_path,
|
|
1435
|
+
request.destination_path,
|
|
1436
|
+
dirs_exist_ok=request.dirs_exist_ok,
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
if isinstance(validation_result, CopyTreeResultFailure):
|
|
1440
|
+
return validation_result
|
|
1441
|
+
|
|
1442
|
+
source_normalized = validation_result.source_normalized
|
|
1443
|
+
dest_normalized = validation_result.dest_normalized
|
|
1444
|
+
source_path = validation_result.source_path
|
|
1445
|
+
destination_path = validation_result.destination_path
|
|
1446
|
+
|
|
1447
|
+
# Copy directory tree
|
|
1448
|
+
try:
|
|
1449
|
+
stats = self._copy_directory_tree(
|
|
1450
|
+
source_normalized,
|
|
1451
|
+
dest_normalized,
|
|
1452
|
+
symlinks=request.symlinks,
|
|
1453
|
+
ignore_dangling_symlinks=request.ignore_dangling_symlinks,
|
|
1454
|
+
ignore_patterns=request.ignore_patterns,
|
|
1455
|
+
)
|
|
1456
|
+
except PermissionError as e:
|
|
1457
|
+
msg = f"Permission denied copying {source_path} to {destination_path}: {e}"
|
|
1458
|
+
logger.error(msg)
|
|
1459
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.PERMISSION_DENIED, result_details=msg)
|
|
1460
|
+
except OSError as e:
|
|
1461
|
+
if "No space left" in str(e) or "Disk full" in str(e):
|
|
1462
|
+
msg = f"Disk full copying {source_path} to {destination_path}: {e}"
|
|
1463
|
+
logger.error(msg)
|
|
1464
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.DISK_FULL, result_details=msg)
|
|
1465
|
+
|
|
1466
|
+
msg = f"I/O error copying {source_path} to {destination_path}: {e}"
|
|
1467
|
+
logger.error(msg)
|
|
1468
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.IO_ERROR, result_details=msg)
|
|
1469
|
+
except Exception as e:
|
|
1470
|
+
msg = f"Unexpected error copying {source_path} to {destination_path}: {type(e).__name__}: {e}"
|
|
1471
|
+
logger.error(msg)
|
|
1472
|
+
return CopyTreeResultFailure(failure_reason=FileIOFailureReason.UNKNOWN, result_details=msg)
|
|
1473
|
+
|
|
1474
|
+
# SUCCESS PATH
|
|
1475
|
+
return CopyTreeResultSuccess(
|
|
1476
|
+
source_path=str(source_path),
|
|
1477
|
+
destination_path=str(destination_path),
|
|
1478
|
+
files_copied=stats.files_copied,
|
|
1479
|
+
total_bytes_copied=stats.total_bytes_copied,
|
|
1480
|
+
result_details=f"Directory tree copied successfully: {source_path} -> {destination_path}",
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1138
1483
|
def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
|
|
1139
1484
|
"""Handle app initialization complete event by registering system resources."""
|
|
1140
1485
|
self._register_system_resources()
|