Coverage for cli/storage.py: 94.97%

700 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-30 21:22 +0000

1"""Discover GCO S3 buckets and safely sync data with local storage. 

2 

3The physical names of GCO buckets contain deployment-generated account and 

4region components. This module keeps those names out of the user interface by 

5resolving stable aliases from the SSM and CloudFormation contracts published by 

6the stacks. 

7""" 

8 

9from __future__ import annotations 

10 

11import base64 

12import hashlib 

13import os 

14import secrets 

15import stat 

16import sys 

17import unicodedata 

18from contextlib import suppress 

19from dataclasses import dataclass 

20from datetime import datetime 

21from pathlib import Path 

22from types import TracebackType 

23from typing import Any, Self 

24 

25import boto3 

26from botocore.exceptions import ClientError 

27 

28from .config import GCOConfig, get_config 

29 

30type _FileSignature = tuple[int, int, int, int, int] 

31 

32 

33class StorageBucketNotFoundError(RuntimeError): 

34 """Raised when a friendly alias has no deployed backing bucket.""" 

35 

36 

37@dataclass(frozen=True) 

38class _ConfinementContract: 

39 """Identity-bound MCP local-root contract propagated to the CLI.""" 

40 

41 root: Path 

42 device: int 

43 inode: int 

44 

45 

46class _PinnedRoot: 

47 """Descriptor-pinned root for race-resistant confined filesystem access.""" 

48 

49 def __init__(self, contract: _ConfinementContract): 

50 if os.name != "posix" or not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"): 

51 raise RuntimeError( 

52 "Confined storage sync requires descriptor-relative no-follow filesystem support" 

53 ) 

54 if not contract.root.is_absolute(): 

55 raise ValueError("The internal storage confinement root must be absolute") 

56 

57 self.root = contract.root 

58 flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0) 

59 try: 

60 self._fd = os.open(self.root, flags) 

61 except OSError as exc: 

62 raise ValueError( 

63 f"Cannot open the configured storage confinement root securely: {self.root}" 

64 ) from exc 

65 

66 root_stat = os.fstat(self._fd) 

67 if not stat.S_ISDIR(root_stat.st_mode): 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true

68 os.close(self._fd) 

69 self._fd = -1 

70 raise NotADirectoryError( 

71 f"The configured storage confinement root is not a directory: {self.root}" 

72 ) 

73 if (root_stat.st_dev, root_stat.st_ino) != (contract.device, contract.inode): 

74 os.close(self._fd) 

75 self._fd = -1 

76 raise RuntimeError( 

77 "GCO_STORAGE_LOCAL_ROOT changed after the MCP request was validated; retry the call" 

78 ) 

79 

80 def __enter__(self) -> Self: 

81 return self 

82 

83 def __exit__( 

84 self, 

85 exc_type: type[BaseException] | None, 

86 exc_value: BaseException | None, 

87 traceback: TracebackType | None, 

88 ) -> None: 

89 if self._fd >= 0: 89 ↛ exitline 89 didn't return from function '__exit__' because the condition on line 89 was always true

90 os.close(self._fd) 

91 self._fd = -1 

92 

93 @staticmethod 

94 def _directory_flags() -> int: 

95 return os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0) 

96 

97 @staticmethod 

98 def _file_flags() -> int: 

99 # Avoid blocking if a raced source replacement is a FIFO; fstat below 

100 # still rejects every opened object that is not a regular file. 

101 return ( 

102 os.O_RDONLY 

103 | os.O_NOFOLLOW 

104 | getattr(os, "O_NONBLOCK", 0) 

105 | getattr(os, "O_BINARY", 0) 

106 | getattr(os, "O_CLOEXEC", 0) 

107 ) 

108 

109 def relative_parts(self, local_path: str) -> tuple[str, ...]: 

110 """Return a lexical root-relative path; traversal itself stays descriptor-relative.""" 

111 supplied = Path(local_path).expanduser() 

112 candidate = supplied if supplied.is_absolute() else self.root / supplied 

113 lexical = Path(os.path.abspath(candidate)) 

114 try: 

115 relative = lexical.relative_to(self.root) 

116 except ValueError as exc: 

117 raise ValueError( 

118 f"Local sync path must stay within GCO_STORAGE_LOCAL_ROOT: {local_path}" 

119 ) from exc 

120 

121 parts = tuple(relative.parts) 

122 if any(part in ("", ".", "..") for part in parts): 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true

123 raise ValueError( 

124 f"Local sync path must stay within GCO_STORAGE_LOCAL_ROOT: {local_path}" 

125 ) 

126 return parts 

127 

128 def display_path(self, parts: tuple[str, ...]) -> Path: 

129 """Return a human-readable path without using it for confined access.""" 

130 return self.root.joinpath(*parts) 

131 

132 def open_directory( 

133 self, 

134 parts: tuple[str, ...], 

135 *, 

136 create: bool = False, 

137 allow_missing: bool = False, 

138 ) -> int | None: 

139 """Open a directory by walking from the pinned root without following links.""" 

140 current_fd = os.dup(self._fd) 

141 try: 

142 walked: tuple[str, ...] = () 

143 for part in parts: 

144 walked += (part,) 

145 try: 

146 child_fd = os.open(part, self._directory_flags(), dir_fd=current_fd) 

147 except FileNotFoundError: 

148 if not create: 

149 if allow_missing: 

150 os.close(current_fd) 

151 return None 

152 raise FileNotFoundError( 

153 f"Confined local directory does not exist: {self.display_path(walked)}" 

154 ) from None 

155 with suppress(FileExistsError): 

156 os.mkdir(part, dir_fd=current_fd) 

157 try: 

158 child_fd = os.open(part, self._directory_flags(), dir_fd=current_fd) 

159 except OSError as exc: 

160 raise ValueError( 

161 "Confined local path changed while creating a directory: " 

162 f"{self.display_path(walked)}" 

163 ) from exc 

164 except OSError as exc: 

165 raise ValueError( 

166 "Confined local path contains a symbolic link or non-directory: " 

167 f"{self.display_path(walked)}" 

168 ) from exc 

169 os.close(current_fd) 

170 current_fd = child_fd 

171 return current_fd 

172 except BaseException: 

173 os.close(current_fd) 

174 raise 

175 

176 def inspect_directory( 

177 self, 

178 parts: tuple[str, ...], 

179 *, 

180 create: bool, 

181 ) -> bool: 

182 """Securely inspect or create a confined directory.""" 

183 directory_fd = self.open_directory(parts, create=create, allow_missing=not create) 

184 if directory_fd is None: 

185 return False 

186 os.close(directory_fd) 

187 return True 

188 

189 def lstat(self, parts: tuple[str, ...]) -> os.stat_result | None: 

190 """Stat a confined path without following its final component.""" 

191 if not parts: 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true

192 return os.fstat(self._fd) 

193 parent_fd = self.open_directory(parts[:-1], allow_missing=True) 

194 if parent_fd is None: 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true

195 return None 

196 try: 

197 try: 

198 return os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False) 

199 except FileNotFoundError: 

200 return None 

201 finally: 

202 os.close(parent_fd) 

203 

204 def open_regular_file(self, parts: tuple[str, ...]) -> int: 

205 """Open a confined regular file without following any path component.""" 

206 if not parts: 

207 raise IsADirectoryError(f"Upload source is a directory: {self.root}") 

208 parent_fd = self.open_directory(parts[:-1]) 

209 if parent_fd is None: # pragma: no cover - allow_missing is false 

210 raise FileNotFoundError(self.display_path(parts)) 

211 try: 

212 try: 

213 file_fd = os.open(parts[-1], self._file_flags(), dir_fd=parent_fd) 

214 except OSError as exc: 

215 raise ValueError( 

216 "Confined upload source is missing, linked, or not a regular file: " 

217 f"{self.display_path(parts)}" 

218 ) from exc 

219 finally: 

220 os.close(parent_fd) 

221 

222 file_stat = os.fstat(file_fd) 

223 if not stat.S_ISREG(file_stat.st_mode): 

224 os.close(file_fd) 

225 raise ValueError(f"Upload source is not a regular file: {self.display_path(parts)}") 

226 return file_fd 

227 

228 def open_child_directory(self, parent_fd: int, name: str, display: Path) -> int: 

229 """Open an enumerated child directory without following a raced replacement.""" 

230 try: 

231 child_fd = os.open(name, self._directory_flags(), dir_fd=parent_fd) 

232 except OSError as exc: 

233 raise ValueError(f"Upload source directory changed or is linked: {display}") from exc 

234 if not stat.S_ISDIR(os.fstat(child_fd).st_mode): 

235 os.close(child_fd) 

236 raise ValueError(f"Upload source is not a directory: {display}") 

237 return child_fd 

238 

239 def open_child_regular_file(self, parent_fd: int, name: str, display: Path) -> int: 

240 """Open an enumerated child file without following a raced replacement.""" 

241 try: 

242 file_fd = os.open(name, self._file_flags(), dir_fd=parent_fd) 

243 except OSError as exc: 

244 raise ValueError(f"Upload source file changed or is linked: {display}") from exc 

245 if not stat.S_ISREG(os.fstat(file_fd).st_mode): 

246 os.close(file_fd) 

247 raise ValueError(f"Upload source is not a regular file: {display}") 

248 return file_fd 

249 

250 def download_target_is_current( 

251 self, 

252 parts: tuple[str, ...], 

253 size: int, 

254 modified: datetime | None, 

255 *, 

256 evaluate_current: bool, 

257 ) -> bool: 

258 """Securely inspect a prospective destination and optionally test freshness.""" 

259 parent_fd = self.open_directory(parts[:-1], allow_missing=True) 

260 if parent_fd is None: 

261 return False 

262 try: 

263 try: 

264 target_stat = os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False) 

265 except FileNotFoundError: 

266 return False 

267 finally: 

268 os.close(parent_fd) 

269 

270 display = self.display_path(parts) 

271 if stat.S_ISLNK(target_stat.st_mode): 

272 raise ValueError(f"Download destination must not be a symbolic link: {display}") 

273 if stat.S_ISDIR(target_stat.st_mode): 

274 raise IsADirectoryError(f"S3 object maps to an existing directory: {display}") 

275 if not stat.S_ISREG(target_stat.st_mode): 275 ↛ 277line 275 didn't jump to line 277 because the condition on line 275 was always true

276 raise ValueError(f"Download destination is not a regular file: {display}") 

277 return bool( 

278 evaluate_current 

279 and modified is not None 

280 and target_stat.st_size == size 

281 and int(target_stat.st_mtime) >= int(modified.timestamp()) 

282 ) 

283 

284 def download_object(self, s3: Any, bucket: str, obj: _SyncObject) -> None: 

285 """Download to a secure sibling temporary file and atomically install it.""" 

286 if obj.destination_parts is None: # pragma: no cover - internal invariant 

287 raise RuntimeError("Missing confined destination components") 

288 parent_fd = self.open_directory(obj.destination_parts[:-1], create=True) 

289 if parent_fd is None: # pragma: no cover - create is true 

290 raise RuntimeError("Failed to create confined destination directory") 

291 

292 temporary_name = "" 

293 temporary_created = False 

294 try: 

295 temporary_flags = ( 

296 os.O_RDWR 

297 | os.O_CREAT 

298 | os.O_EXCL 

299 | os.O_NOFOLLOW 

300 | getattr(os, "O_BINARY", 0) 

301 | getattr(os, "O_CLOEXEC", 0) 

302 ) 

303 for _ in range(128): 

304 temporary_name = f".gco-sync-{os.getpid()}-{secrets.token_hex(12)}.tmp" 

305 try: 

306 temporary_fd = os.open( 

307 temporary_name, 

308 temporary_flags, 

309 0o600, 

310 dir_fd=parent_fd, 

311 ) 

312 temporary_created = True 

313 break 

314 except FileExistsError: 

315 continue 

316 else: # pragma: no cover - cryptographically improbable 

317 raise RuntimeError("Could not allocate a unique download temporary file") 

318 

319 with os.fdopen(temporary_fd, "w+b") as temporary_file: 

320 s3.download_fileobj(bucket, obj.key, temporary_file) 

321 temporary_file.flush() 

322 downloaded_stat = os.fstat(temporary_file.fileno()) 

323 if downloaded_stat.st_size != obj.size: 

324 raise RuntimeError( 

325 f"Downloaded size changed for s3://{bucket}/{obj.key}: " 

326 f"expected {obj.size}, received {downloaded_stat.st_size}" 

327 ) 

328 if obj.last_modified is not None: 328 ↛ 332line 328 didn't jump to line 332

329 timestamp = obj.last_modified.timestamp() 

330 os.utime(temporary_file.fileno(), (timestamp, timestamp)) 

331 

332 os.replace( 

333 temporary_name, 

334 obj.destination_parts[-1], 

335 src_dir_fd=parent_fd, 

336 dst_dir_fd=parent_fd, 

337 ) 

338 temporary_created = False 

339 finally: 

340 if temporary_created: 

341 with suppress(FileNotFoundError): 

342 os.unlink(temporary_name, dir_fd=parent_fd) 

343 os.close(parent_fd) 

344 

345 

346@dataclass(frozen=True) 

347class _SyncObject: 

348 """One validated object in a bucket-to-local sync plan.""" 

349 

350 key: str 

351 destination: Path 

352 destination_parts: tuple[str, ...] | None 

353 size: int 

354 last_modified: datetime | None 

355 current: bool 

356 

357 

358@dataclass(frozen=True) 

359class _PreparedUpload: 

360 """One securely opened and hashed local upload source.""" 

361 

362 source: Path 

363 relative: str 

364 source_parts: tuple[str, ...] | None 

365 size: int 

366 sha256: str 

367 signature: _FileSignature 

368 

369 

370@dataclass(frozen=True) 

371class _UploadObject: 

372 """One validated local file in a local-to-bucket sync plan.""" 

373 

374 source: Path 

375 source_parts: tuple[str, ...] | None 

376 key: str 

377 size: int 

378 sha256: str 

379 signature: _FileSignature 

380 current: bool 

381 

382 

383class StorageManager: 

384 """Resolve friendly GCO bucket aliases and transfer their contents.""" 

385 

386 _UPLOAD_DIGEST_METADATA = "gco-sync-sha256" 

387 

388 _PURPOSES = { 

389 "cluster-shared": "Cross-region cluster job artifacts and shared data", 

390 "model-weights": "Central model weights used by inference endpoints", 

391 "regional-shared": "General-purpose data for workloads in one region", 

392 "analytics-studio": "SageMaker Studio private scratch data and outputs", 

393 } 

394 

395 def __init__(self, config: GCOConfig | None = None): 

396 self.config = config or get_config() 

397 

398 def list_buckets(self, region: str | None = None) -> list[dict[str, str]]: 

399 """Return deployed user-facing buckets under their stable aliases. 

400 

401 Global and analytics buckets are always considered. ``region`` limits 

402 regional-bucket discovery to one region; otherwise every regional 

403 deployment configured in ``cdk.json`` is considered. A bucket whose 

404 stack is not deployed is omitted, while permission and transport 

405 errors still surface to the caller. 

406 """ 

407 buckets: list[dict[str, str]] = [] 

408 

409 for alias in ("cluster-shared", "model-weights"): 

410 with suppress(StorageBucketNotFoundError): 

411 buckets.append(self.resolve_bucket(alias)) 

412 

413 regional_regions = [region] if region else self._configured_regional_regions() 

414 for regional_region in regional_regions: 

415 with suppress(StorageBucketNotFoundError): 

416 buckets.append(self.resolve_bucket("regional-shared", region=regional_region)) 

417 

418 with suppress(StorageBucketNotFoundError): 

419 buckets.append(self.resolve_bucket("analytics-studio")) 

420 

421 return buckets 

422 

423 def resolve_bucket(self, alias: str, region: str | None = None) -> dict[str, str]: 

424 """Resolve a stable alias to a physical bucket and home region. 

425 

426 Supported aliases are ``cluster-shared``, ``model-weights``, 

427 ``analytics-studio``, and either ``regional-shared:<region>`` or 

428 ``regional-shared`` with ``region`` supplied. The unqualified regional 

429 alias is inferred only when exactly one deployment region is configured. 

430 """ 

431 normalized = alias.strip().lower() 

432 embedded_region: str | None = None 

433 

434 if normalized.startswith("regional-shared:"): 

435 normalized, embedded_region = normalized.split(":", 1) 

436 embedded_region = embedded_region.strip() 

437 if not embedded_region: 

438 raise ValueError("Regional bucket alias must include a region after ':'") 

439 if region and region != embedded_region: 

440 raise ValueError( 

441 f"Alias region '{embedded_region}' conflicts with --region '{region}'" 

442 ) 

443 region = embedded_region 

444 

445 if normalized == "regional-shared": 

446 target_region = region or self._infer_single_regional_region() 

447 return self._resolve_regional_shared(target_region) 

448 

449 if region: 

450 raise ValueError("--region is only valid with the 'regional-shared' alias") 

451 

452 if normalized == "cluster-shared": 

453 return self._resolve_cluster_shared() 

454 if normalized == "model-weights": 

455 return self._resolve_model_weights() 

456 if normalized == "analytics-studio": 

457 return self._resolve_analytics_studio() 

458 

459 raise ValueError( 

460 f"Unknown bucket alias '{alias}'. Use one of: cluster-shared, " 

461 "model-weights, regional-shared:<region>, analytics-studio" 

462 ) 

463 

464 def sync( 

465 self, 

466 alias: str, 

467 local_dir: str, 

468 *, 

469 region: str | None = None, 

470 prefix: str = "", 

471 direction: str = "download", 

472 dry_run: bool = False, 

473 force: bool = False, 

474 confinement_root: str | None = None, 

475 confinement_device: int | None = None, 

476 confinement_inode: int | None = None, 

477 ) -> dict[str, Any]: 

478 """Incrementally transfer files in one explicit direction. 

479 

480 ``download`` preserves the original S3-to-local behavior. ``upload`` 

481 transfers a local file or directory into S3. Neither direction deletes 

482 destination-only files or objects. The confinement values form an 

483 internal MCP-to-CLI contract and are not public CLI options. 

484 """ 

485 normalized_direction = direction.strip().lower() 

486 if normalized_direction not in {"download", "upload"}: 

487 raise ValueError("Sync direction must be either 'download' or 'upload'") 

488 

489 contract = self._make_confinement_contract( 

490 confinement_root, 

491 confinement_device, 

492 confinement_inode, 

493 ) 

494 bucket = self.resolve_bucket(alias, region=region) 

495 normalized_prefix = self._normalize_prefix(prefix) 

496 s3 = boto3.client("s3", region_name=bucket["region"]) 

497 

498 if contract is not None: 

499 with _PinnedRoot(contract) as confinement: 

500 local_parts = confinement.relative_parts(local_dir) 

501 local_path = confinement.display_path(local_parts) 

502 if normalized_direction == "upload": 

503 return self._sync_upload( 

504 bucket, 

505 s3, 

506 local_path, 

507 normalized_prefix, 

508 dry_run=dry_run, 

509 force=force, 

510 confinement=confinement, 

511 source_parts=local_parts, 

512 ) 

513 return self._sync_download( 

514 bucket, 

515 s3, 

516 local_path, 

517 normalized_prefix, 

518 dry_run=dry_run, 

519 force=force, 

520 confinement=confinement, 

521 destination_parts=local_parts, 

522 ) 

523 

524 local_path = Path(local_dir).expanduser() 

525 if normalized_direction == "upload": 

526 return self._sync_upload( 

527 bucket, 

528 s3, 

529 local_path, 

530 normalized_prefix, 

531 dry_run=dry_run, 

532 force=force, 

533 ) 

534 return self._sync_download( 

535 bucket, 

536 s3, 

537 local_path, 

538 normalized_prefix, 

539 dry_run=dry_run, 

540 force=force, 

541 ) 

542 

543 @staticmethod 

544 def _make_confinement_contract( 

545 root: str | None, 

546 device: int | None, 

547 inode: int | None, 

548 ) -> _ConfinementContract | None: 

549 supplied = (root is not None, device is not None, inode is not None) 

550 if not any(supplied): 

551 return None 

552 if not all(supplied) or not root: 

553 raise ValueError("The internal storage confinement contract is incomplete") 

554 if device is None or inode is None: # pragma: no cover - narrowed by all() 

555 raise ValueError("The internal storage confinement identity is incomplete") 

556 if device < 0 or inode < 0: 

557 raise ValueError("The internal storage confinement identity is invalid") 

558 root_path = Path(root).expanduser() 

559 if not root_path.is_absolute() or Path(os.path.abspath(root_path)) != root_path: 

560 raise ValueError( 

561 "The internal storage confinement root must be normalized and absolute" 

562 ) 

563 return _ConfinementContract(root=root_path, device=device, inode=inode) 

564 

565 def _sync_download( 

566 self, 

567 bucket: dict[str, str], 

568 s3: Any, 

569 destination: Path, 

570 prefix: str, 

571 *, 

572 dry_run: bool, 

573 force: bool, 

574 confinement: _PinnedRoot | None = None, 

575 destination_parts: tuple[str, ...] = (), 

576 ) -> dict[str, Any]: 

577 """Download the selected bucket prefix into a local directory.""" 

578 if confinement is None: 

579 if destination.exists() and not destination.is_dir(): 

580 raise NotADirectoryError(f"Sync destination is not a directory: {destination}") 

581 if not dry_run: 

582 destination.mkdir(parents=True, exist_ok=True) 

583 destination = destination.resolve() 

584 else: 

585 confinement.inspect_directory(destination_parts, create=not dry_run) 

586 

587 objects, directory_markers = self._build_sync_plan( 

588 s3, 

589 bucket["bucket"], 

590 prefix, 

591 destination, 

592 force=force, 

593 confinement=confinement, 

594 destination_parts=destination_parts, 

595 ) 

596 pending = [obj for obj in objects if not obj.current] 

597 current = [obj for obj in objects if obj.current] 

598 skipped = len(current) 

599 bytes_planned = sum(obj.size for obj in pending) 

600 downloaded = 0 

601 bytes_downloaded = 0 

602 

603 if not dry_run: 

604 for obj in pending: 

605 try: 

606 if confinement is not None: 

607 confinement.download_object(s3, bucket["bucket"], obj) 

608 else: 

609 obj.destination.parent.mkdir(parents=True, exist_ok=True) 

610 s3.download_file(bucket["bucket"], obj.key, str(obj.destination)) 

611 if obj.last_modified is not None: 611 ↛ 620line 611 didn't jump to line 620 because the condition on line 611 was always true

612 timestamp = obj.last_modified.timestamp() 

613 os.utime(obj.destination, (timestamp, timestamp)) 

614 except Exception as exc: 

615 raise RuntimeError( 

616 "Sync did not complete: failed to download " 

617 f"'s3://{bucket['bucket']}/{obj.key}' to " 

618 f"'{obj.destination}': {exc}" 

619 ) from exc 

620 downloaded += 1 

621 bytes_downloaded += obj.size 

622 

623 for obj in current: 

624 if confinement is not None: 

625 if obj.destination_parts is None: # pragma: no cover - internal invariant 

626 raise RuntimeError("Missing confined destination components") 

627 still_current = confinement.download_target_is_current( 

628 obj.destination_parts, 

629 obj.size, 

630 obj.last_modified, 

631 evaluate_current=True, 

632 ) 

633 else: 

634 still_current = self._is_current( 

635 obj.destination, 

636 obj.size, 

637 obj.last_modified, 

638 ) 

639 if not still_current: 

640 raise RuntimeError( 

641 "Sync did not complete: a skipped local file changed after planning: " 

642 f"{obj.destination}" 

643 ) 

644 

645 source = f"s3://{bucket['bucket']}/{prefix}" 

646 return { 

647 "alias": bucket["alias"], 

648 "bucket": bucket["bucket"], 

649 "region": bucket["region"], 

650 "direction": "download", 

651 "source": source, 

652 "destination": str(destination), 

653 "prefix": prefix, 

654 "dry_run": dry_run, 

655 "force": force, 

656 "objects_scanned": len(objects) + directory_markers, 

657 "directory_markers": directory_markers, 

658 "files_planned": len(pending), 

659 "files_downloaded": downloaded, 

660 "files_skipped": skipped, 

661 "bytes_planned": bytes_planned, 

662 "bytes_downloaded": bytes_downloaded, 

663 } 

664 

665 def _sync_upload( 

666 self, 

667 bucket: dict[str, str], 

668 s3: Any, 

669 source: Path, 

670 prefix: str, 

671 *, 

672 dry_run: bool, 

673 force: bool, 

674 confinement: _PinnedRoot | None = None, 

675 source_parts: tuple[str, ...] = (), 

676 ) -> dict[str, Any]: 

677 """Upload a local file or directory into the selected bucket prefix.""" 

678 if confinement is None: 

679 if source.is_symlink(): 

680 raise ValueError(f"Upload source must not be a symbolic link: {source}") 

681 if not source.exists(): 

682 raise FileNotFoundError(f"Upload source not found: {source}") 

683 if not source.is_file() and not source.is_dir(): 

684 raise ValueError(f"Upload source must be a regular file or directory: {source}") 

685 source = source.resolve() 

686 

687 objects, remote_objects_probed = self._build_upload_plan( 

688 s3, 

689 bucket["bucket"], 

690 prefix, 

691 source, 

692 force=force, 

693 confinement=confinement, 

694 source_parts=source_parts, 

695 ) 

696 pending = [obj for obj in objects if not obj.current] 

697 current = [obj for obj in objects if obj.current] 

698 skipped = len(current) 

699 bytes_planned = sum(obj.size for obj in pending) 

700 uploaded = 0 

701 bytes_uploaded = 0 

702 

703 if not dry_run: 

704 for obj in pending: 

705 try: 

706 source_fd = self._open_upload_source(obj, confinement) 

707 try: 

708 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 708 ↛ 709line 708 didn't jump to line 709 because the condition on line 708 was never true

709 raise RuntimeError(f"Local file changed after planning: {obj.source}") 

710 # Managed single-part uploads close their input stream. Keep the 

711 # securely opened descriptor alive for the post-transfer signature 

712 # check while allowing s3transfer to close its stream normally. 

713 with open(source_fd, "rb", closefd=False) as source_file: 

714 s3.upload_fileobj( 

715 source_file, 

716 bucket["bucket"], 

717 obj.key, 

718 ExtraArgs={ 

719 "Metadata": {self._UPLOAD_DIGEST_METADATA: obj.sha256}, 

720 "ChecksumSHA256": base64.b64encode( 

721 bytes.fromhex(obj.sha256) 

722 ).decode("ascii"), 

723 }, 

724 ) 

725 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 

726 raise RuntimeError(f"Local file changed during upload: {obj.source}") 

727 finally: 

728 os.close(source_fd) 

729 # A descriptor remains valid if its pathname is renamed away. Reopen 

730 # after upload so a raced path replacement cannot be reported current. 

731 self._verify_upload_source_signature( 

732 obj, 

733 confinement, 

734 skipped=False, 

735 ) 

736 except Exception as exc: 

737 raise RuntimeError( 

738 "Sync did not complete: failed to upload " 

739 f"'{obj.source}' to 's3://{bucket['bucket']}/{obj.key}': {exc}" 

740 ) from exc 

741 uploaded += 1 

742 bytes_uploaded += obj.size 

743 

744 for obj in current: 

745 source_fd = self._open_upload_source(obj, confinement) 

746 try: 

747 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 

748 raise RuntimeError( 

749 f"A skipped local file changed after planning: {obj.source}" 

750 ) 

751 remote_objects_probed += 1 

752 remote_current = self._remote_digest_matches( 

753 s3, 

754 bucket["bucket"], 

755 obj.key, 

756 obj.size, 

757 obj.sha256, 

758 ) 

759 if self._stat_signature(os.fstat(source_fd)) != obj.signature: 

760 raise RuntimeError( 

761 f"A skipped local file changed during revalidation: {obj.source}" 

762 ) 

763 finally: 

764 os.close(source_fd) 

765 

766 # Reopen after the remote probe so a renamed source path cannot 

767 # be reported current merely because its old descriptor is stable. 

768 self._verify_upload_source_signature( 

769 obj, 

770 confinement, 

771 skipped=True, 

772 ) 

773 if not remote_current: 

774 raise RuntimeError( 

775 "Sync did not complete: a skipped S3 object changed after planning: " 

776 f"s3://{bucket['bucket']}/{obj.key}" 

777 ) 

778 

779 destination = f"s3://{bucket['bucket']}/{prefix}" 

780 return { 

781 "alias": bucket["alias"], 

782 "bucket": bucket["bucket"], 

783 "region": bucket["region"], 

784 "direction": "upload", 

785 "source": str(source), 

786 "destination": destination, 

787 "prefix": prefix, 

788 "dry_run": dry_run, 

789 "force": force, 

790 "files_scanned": len(objects), 

791 "objects_scanned": len(objects), 

792 "objects_probed": remote_objects_probed, 

793 "files_planned": len(pending), 

794 "files_uploaded": uploaded, 

795 "files_skipped": skipped, 

796 "bytes_planned": bytes_planned, 

797 "bytes_uploaded": bytes_uploaded, 

798 } 

799 

800 def _resolve_cluster_shared(self) -> dict[str, str]: 

801 from gco.services.aws_ssm import get_ssm_parameter_optional 

802 from gco.stacks.constants import cluster_shared_ssm_parameter_prefix 

803 

804 prefix = cluster_shared_ssm_parameter_prefix(self.config.project_name) 

805 name = get_ssm_parameter_optional( 

806 f"{prefix}/name", 

807 region=self.config.global_region, 

808 ) 

809 if not name: 

810 raise StorageBucketNotFoundError( 

811 "Cluster shared bucket not found. Deploy the global stack first." 

812 ) 

813 bucket_region = get_ssm_parameter_optional( 

814 f"{prefix}/region", 

815 region=self.config.global_region, 

816 ) 

817 return self._bucket_record( 

818 alias="cluster-shared", 

819 name=name, 

820 region=bucket_region or self.config.global_region, 

821 scope="global", 

822 ) 

823 

824 def _resolve_model_weights(self) -> dict[str, str]: 

825 from gco.services.aws_ssm import get_ssm_parameter_optional 

826 

827 name = get_ssm_parameter_optional( 

828 f"/{self.config.project_name}/model-bucket-name", 

829 region=self.config.global_region, 

830 ) 

831 if not name: 

832 raise StorageBucketNotFoundError( 

833 "Model weights bucket not found. Deploy the global stack first." 

834 ) 

835 return self._bucket_record( 

836 alias="model-weights", 

837 name=name, 

838 region=self.config.global_region, 

839 scope="global", 

840 ) 

841 

842 def _resolve_regional_shared(self, region: str) -> dict[str, str]: 

843 from gco.services.aws_ssm import get_ssm_parameter_optional 

844 from gco.stacks.constants import regional_shared_ssm_parameter_prefix 

845 

846 prefix = regional_shared_ssm_parameter_prefix(self.config.project_name) 

847 name = get_ssm_parameter_optional(f"{prefix}/name", region=region) 

848 if not name: 

849 raise StorageBucketNotFoundError( 

850 f"Regional shared bucket not found in region '{region}'. " 

851 "Deploy that region's stack first." 

852 ) 

853 bucket_region = get_ssm_parameter_optional(f"{prefix}/region", region=region) 

854 return self._bucket_record( 

855 alias=f"regional-shared:{region}", 

856 name=name, 

857 region=bucket_region or region, 

858 scope="regional", 

859 ) 

860 

861 def _resolve_analytics_studio(self) -> dict[str, str]: 

862 region = self.config.api_gateway_region 

863 stack_name = f"{self.config.project_name}-analytics" 

864 cfn = boto3.client("cloudformation", region_name=region) 

865 token: str | None = None 

866 

867 try: 

868 while True: 

869 kwargs: dict[str, str] = {"StackName": stack_name} 

870 if token: 

871 kwargs["NextToken"] = token 

872 response = cfn.list_stack_resources(**kwargs) 

873 for resource in response.get("StackResourceSummaries", []): 

874 logical_id = str(resource.get("LogicalResourceId", "")) 

875 if resource.get("ResourceType") == "AWS::S3::Bucket" and logical_id.startswith( 

876 "StudioOnlyBucket" 

877 ): 

878 name = resource.get("PhysicalResourceId") 

879 if isinstance(name, str) and name: 

880 return self._bucket_record( 

881 alias="analytics-studio", 

882 name=name, 

883 region=region, 

884 scope="analytics", 

885 ) 

886 token_value = response.get("NextToken") 

887 token = token_value if isinstance(token_value, str) else None 

888 if not token: 

889 break 

890 except ClientError as exc: 

891 error = exc.response.get("Error", {}) 

892 if error.get("Code") == "ValidationError" and "does not exist" in str( 

893 error.get("Message", "") 

894 ): 

895 raise StorageBucketNotFoundError( 

896 "Analytics Studio bucket not found. Deploy the analytics stack first." 

897 ) from exc 

898 raise 

899 

900 raise StorageBucketNotFoundError( 

901 "Analytics Studio bucket not found in the deployed analytics stack." 

902 ) 

903 

904 def _configured_regional_regions(self) -> list[str]: 

905 from .config import _load_cdk_json 

906 

907 configured = _load_cdk_json().get("regional", []) 

908 candidates = configured if isinstance(configured, list) else [] 

909 regions: list[str] = [] 

910 seen: set[str] = set() 

911 for value in candidates: 

912 if isinstance(value, str) and value and value not in seen: 

913 regions.append(value) 

914 seen.add(value) 

915 return regions or [self.config.default_region] 

916 

917 def _infer_single_regional_region(self) -> str: 

918 regions = self._configured_regional_regions() 

919 if len(regions) == 1: 

920 return regions[0] 

921 choices = ", ".join(f"regional-shared:{item}" for item in regions) 

922 raise ValueError( 

923 "The 'regional-shared' alias is ambiguous across configured regions. " 

924 f"Use --region or one of: {choices}" 

925 ) 

926 

927 def _bucket_record(self, *, alias: str, name: str, region: str, scope: str) -> dict[str, str]: 

928 purpose_key = "regional-shared" if alias.startswith("regional-shared:") else alias 

929 return { 

930 "alias": alias, 

931 "bucket": name, 

932 "region": region, 

933 "scope": scope, 

934 "purpose": self._PURPOSES[purpose_key], 

935 "s3_uri": f"s3://{name}/", 

936 } 

937 

938 @staticmethod 

939 def _normalize_prefix(prefix: str) -> str: 

940 normalized = prefix.lstrip("/") 

941 if normalized and not normalized.endswith("/"): 

942 normalized += "/" 

943 return normalized 

944 

945 @staticmethod 

946 def _stat_signature(value: os.stat_result) -> _FileSignature: 

947 return ( 

948 value.st_dev, 

949 value.st_ino, 

950 value.st_size, 

951 value.st_mtime_ns, 

952 value.st_ctime_ns, 

953 ) 

954 

955 @classmethod 

956 def _hash_upload_fd(cls, file_fd: int, path: Path) -> tuple[str, _FileSignature]: 

957 with os.fdopen(file_fd, "rb") as source_file: 

958 before = os.fstat(source_file.fileno()) 

959 if not stat.S_ISREG(before.st_mode): 

960 raise ValueError(f"Upload source is not a regular file: {path}") 

961 before_signature = cls._stat_signature(before) 

962 digest = hashlib.sha256() 

963 while chunk := source_file.read(8 * 1024 * 1024): 

964 digest.update(chunk) 

965 after_signature = cls._stat_signature(os.fstat(source_file.fileno())) 

966 if before_signature != after_signature: 

967 raise RuntimeError(f"Local file changed while planning upload: {path}") 

968 return digest.hexdigest(), before_signature 

969 

970 @classmethod 

971 def _hash_upload_file(cls, path: Path) -> tuple[str, _FileSignature]: 

972 flags = ( 

973 os.O_RDONLY 

974 | getattr(os, "O_BINARY", 0) 

975 | getattr(os, "O_NOFOLLOW", 0) 

976 | getattr(os, "O_CLOEXEC", 0) 

977 ) 

978 return cls._hash_upload_fd(os.open(path, flags), path) 

979 

980 @staticmethod 

981 def _validate_upload_relative_path(relative: str, source: Path) -> None: 

982 parts = relative.split("/") 

983 if ( 

984 not relative 

985 or "\x00" in relative 

986 or "\\" in relative 

987 or any(part in ("", ".", "..") for part in parts) 

988 ): 

989 raise ValueError(f"Local path cannot be represented safely as an S3 key: {source}") 

990 

991 @classmethod 

992 def _collect_upload_files(cls, source: Path) -> list[_PreparedUpload]: 

993 paths: list[tuple[Path, str]] = [] 

994 if source.is_file(): 

995 relative = source.name 

996 cls._validate_upload_relative_path(relative, source) 

997 paths.append((source, relative)) 

998 else: 

999 

1000 def raise_walk_error(error: OSError) -> None: 

1001 raise error 

1002 

1003 for root, directories, names in os.walk( 

1004 source, 

1005 topdown=True, 

1006 onerror=raise_walk_error, 

1007 followlinks=False, 

1008 ): 

1009 root_path = Path(root) 

1010 for directory_name in directories: 

1011 directory = root_path / directory_name 

1012 if directory.is_symlink(): 1012 ↛ 1013line 1012 didn't jump to line 1013 because the condition on line 1012 was never true

1013 raise ValueError(f"Upload source contains a symbolic link: {directory}") 

1014 for name in names: 

1015 path = root_path / name 

1016 if path.is_symlink(): 

1017 raise ValueError(f"Upload source contains a symbolic link: {path}") 

1018 if not path.is_file(): 

1019 raise ValueError(f"Upload source contains a non-regular file: {path}") 

1020 relative = path.relative_to(source).as_posix() 

1021 cls._validate_upload_relative_path(relative, path) 

1022 paths.append((path, relative)) 

1023 

1024 prepared: list[_PreparedUpload] = [] 

1025 for path, relative in sorted(paths, key=lambda item: item[1]): 

1026 digest, signature = cls._hash_upload_file(path) 

1027 prepared.append( 

1028 _PreparedUpload( 

1029 source=path, 

1030 relative=relative, 

1031 source_parts=None, 

1032 size=signature[2], 

1033 sha256=digest, 

1034 signature=signature, 

1035 ) 

1036 ) 

1037 return prepared 

1038 

1039 @classmethod 

1040 def _collect_confined_upload_files( 

1041 cls, 

1042 confinement: _PinnedRoot, 

1043 source_parts: tuple[str, ...], 

1044 ) -> list[_PreparedUpload]: 

1045 source = confinement.display_path(source_parts) 

1046 source_stat = confinement.lstat(source_parts) 

1047 if source_stat is None: 

1048 raise FileNotFoundError(f"Upload source not found: {source}") 

1049 if stat.S_ISLNK(source_stat.st_mode): 

1050 raise ValueError(f"Upload source must not be a symbolic link: {source}") 

1051 if stat.S_ISREG(source_stat.st_mode): 

1052 relative = source.name 

1053 cls._validate_upload_relative_path(relative, source) 

1054 digest, signature = cls._hash_upload_fd( 

1055 confinement.open_regular_file(source_parts), 

1056 source, 

1057 ) 

1058 return [ 

1059 _PreparedUpload( 

1060 source=source, 

1061 relative=relative, 

1062 source_parts=source_parts, 

1063 size=signature[2], 

1064 sha256=digest, 

1065 signature=signature, 

1066 ) 

1067 ] 

1068 if not stat.S_ISDIR(source_stat.st_mode): 

1069 raise ValueError(f"Upload source must be a regular file or directory: {source}") 

1070 

1071 source_fd = confinement.open_directory(source_parts) 

1072 if source_fd is None: # pragma: no cover - allow_missing is false 

1073 raise FileNotFoundError(source) 

1074 prepared: list[_PreparedUpload] = [] 

1075 try: 

1076 cls._walk_confined_upload_directory( 

1077 confinement, 

1078 source_fd, 

1079 source_parts, 

1080 (), 

1081 prepared, 

1082 ) 

1083 finally: 

1084 os.close(source_fd) 

1085 return sorted(prepared, key=lambda item: item.relative) 

1086 

1087 @classmethod 

1088 def _walk_confined_upload_directory( 

1089 cls, 

1090 confinement: _PinnedRoot, 

1091 directory_fd: int, 

1092 directory_parts: tuple[str, ...], 

1093 relative_parts: tuple[str, ...], 

1094 prepared: list[_PreparedUpload], 

1095 ) -> None: 

1096 """Enumerate and open one source directory through already-pinned descriptors.""" 

1097 for name in sorted(os.listdir(directory_fd)): 

1098 child_parts = directory_parts + (name,) 

1099 child_relative_parts = relative_parts + (name,) 

1100 child = confinement.display_path(child_parts) 

1101 relative = "/".join(child_relative_parts) 

1102 cls._validate_upload_relative_path(relative, child) 

1103 try: 

1104 child_stat = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) 

1105 except FileNotFoundError as exc: 

1106 raise RuntimeError(f"Upload source changed during enumeration: {child}") from exc 

1107 

1108 if stat.S_ISLNK(child_stat.st_mode): 

1109 raise ValueError(f"Upload source contains a symbolic link: {child}") 

1110 if stat.S_ISDIR(child_stat.st_mode): 1110 ↛ 1111line 1110 didn't jump to line 1111 because the condition on line 1110 was never true

1111 child_fd = confinement.open_child_directory(directory_fd, name, child) 

1112 try: 

1113 cls._walk_confined_upload_directory( 

1114 confinement, 

1115 child_fd, 

1116 child_parts, 

1117 child_relative_parts, 

1118 prepared, 

1119 ) 

1120 finally: 

1121 os.close(child_fd) 

1122 continue 

1123 if not stat.S_ISREG(child_stat.st_mode): 

1124 raise ValueError(f"Upload source contains a non-regular file: {child}") 

1125 

1126 digest, signature = cls._hash_upload_fd( 

1127 confinement.open_child_regular_file(directory_fd, name, child), 

1128 child, 

1129 ) 

1130 prepared.append( 

1131 _PreparedUpload( 

1132 source=child, 

1133 relative=relative, 

1134 source_parts=child_parts, 

1135 size=signature[2], 

1136 sha256=digest, 

1137 signature=signature, 

1138 ) 

1139 ) 

1140 

1141 @staticmethod 

1142 def _open_upload_source(obj: _UploadObject, confinement: _PinnedRoot | None) -> int: 

1143 if confinement is not None: 

1144 if obj.source_parts is None: # pragma: no cover - internal invariant 

1145 raise RuntimeError("Missing confined upload source components") 

1146 return confinement.open_regular_file(obj.source_parts) 

1147 flags = ( 

1148 os.O_RDONLY 

1149 | getattr(os, "O_BINARY", 0) 

1150 | getattr(os, "O_NOFOLLOW", 0) 

1151 | getattr(os, "O_CLOEXEC", 0) 

1152 ) 

1153 file_fd = os.open(obj.source, flags) 

1154 if not stat.S_ISREG(os.fstat(file_fd).st_mode): 1154 ↛ 1155line 1154 didn't jump to line 1155 because the condition on line 1154 was never true

1155 os.close(file_fd) 

1156 raise ValueError(f"Upload source is not a regular file: {obj.source}") 

1157 return file_fd 

1158 

1159 @classmethod 

1160 def _verify_upload_source_signature( 

1161 cls, 

1162 obj: _UploadObject, 

1163 confinement: _PinnedRoot | None, 

1164 *, 

1165 skipped: bool, 

1166 ) -> None: 

1167 source_fd = cls._open_upload_source(obj, confinement) 

1168 try: 

1169 if cls._stat_signature(os.fstat(source_fd)) != obj.signature: 1169 ↛ 1170line 1169 didn't jump to line 1170 because the condition on line 1169 was never true

1170 description = "A skipped local file" if skipped else "Local file" 

1171 raise RuntimeError(f"{description} changed after planning: {obj.source}") 

1172 finally: 

1173 os.close(source_fd) 

1174 

1175 def _remote_digest_matches( 

1176 self, 

1177 s3: Any, 

1178 bucket: str, 

1179 key: str, 

1180 size: int, 

1181 digest: str, 

1182 ) -> bool: 

1183 try: 

1184 response = s3.head_object(Bucket=bucket, Key=key) 

1185 except ClientError as exc: 

1186 error = exc.response.get("Error", {}) 

1187 status = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode") 

1188 if error.get("Code") in { 

1189 "403", 

1190 "404", 

1191 "AccessDenied", 

1192 "NoSuchKey", 

1193 "NotFound", 

1194 } or status in {403, 404}: 

1195 # S3 returns 403 rather than 404 for a missing key when the 

1196 # caller intentionally lacks ListBucket. Treat it as not 

1197 # current and let PutObject enforce write authorization. 

1198 return False 

1199 raise 

1200 

1201 if int(response.get("ContentLength", -1)) != size: 

1202 return False 

1203 metadata = response.get("Metadata", {}) 

1204 if not isinstance(metadata, dict): 

1205 return False 

1206 remote_digest = next( 

1207 ( 

1208 str(value) 

1209 for name, value in metadata.items() 

1210 if str(name).lower() == self._UPLOAD_DIGEST_METADATA 

1211 ), 

1212 "", 

1213 ) 

1214 return remote_digest.strip().lower() == digest 

1215 

1216 def _build_upload_plan( 

1217 self, 

1218 s3: Any, 

1219 bucket: str, 

1220 prefix: str, 

1221 source: Path, 

1222 *, 

1223 force: bool, 

1224 confinement: _PinnedRoot | None, 

1225 source_parts: tuple[str, ...], 

1226 ) -> tuple[list[_UploadObject], int]: 

1227 if confinement is None: 

1228 files = self._collect_upload_files(source) 

1229 else: 

1230 files = self._collect_confined_upload_files(confinement, source_parts) 

1231 

1232 objects: list[_UploadObject] = [] 

1233 remote_objects_probed = 0 

1234 for prepared in files: 

1235 key = f"{prefix}{prepared.relative}" 

1236 current = False 

1237 if not force: 

1238 remote_objects_probed += 1 

1239 current = self._remote_digest_matches( 

1240 s3, 

1241 bucket, 

1242 key, 

1243 prepared.size, 

1244 prepared.sha256, 

1245 ) 

1246 objects.append( 

1247 _UploadObject( 

1248 source=prepared.source, 

1249 source_parts=prepared.source_parts, 

1250 key=key, 

1251 size=prepared.size, 

1252 sha256=prepared.sha256, 

1253 signature=prepared.signature, 

1254 current=current, 

1255 ) 

1256 ) 

1257 

1258 return objects, remote_objects_probed 

1259 

1260 def _build_sync_plan( 

1261 self, 

1262 s3: Any, 

1263 bucket: str, 

1264 prefix: str, 

1265 destination: Path, 

1266 *, 

1267 force: bool, 

1268 confinement: _PinnedRoot | None, 

1269 destination_parts: tuple[str, ...], 

1270 ) -> tuple[list[_SyncObject], int]: 

1271 objects: list[_SyncObject] = [] 

1272 directory_markers = 0 

1273 paginator = s3.get_paginator("list_objects_v2") 

1274 

1275 for page in paginator.paginate(Bucket=bucket, Prefix=prefix): 

1276 for item in page.get("Contents", []): 

1277 key = str(item.get("Key", "")) 

1278 if not key: 

1279 raise ValueError("S3 returned an object with an empty key") 

1280 size = int(item.get("Size", 0)) 

1281 if key.endswith("/"): 

1282 if size != 0: 

1283 raise ValueError( 

1284 "S3 object keys ending in '/' cannot be represented as local files: " 

1285 f"{key!r}" 

1286 ) 

1287 directory_markers += 1 

1288 continue 

1289 

1290 relative_key = key[len(prefix) :] if prefix else key 

1291 key_parts = self._download_relative_parts(relative_key, key) 

1292 modified_value = item.get("LastModified") 

1293 modified = modified_value if isinstance(modified_value, datetime) else None 

1294 if confinement is None: 

1295 local_path = self._safe_local_path(destination, relative_key, key) 

1296 local_parts: tuple[str, ...] | None = None 

1297 current = False if force else self._is_current(local_path, size, modified) 

1298 else: 

1299 local_parts = destination_parts + key_parts 

1300 local_path = confinement.display_path(local_parts) 

1301 current = confinement.download_target_is_current( 

1302 local_parts, 

1303 size, 

1304 modified, 

1305 evaluate_current=not force, 

1306 ) 

1307 objects.append( 

1308 _SyncObject( 

1309 key=key, 

1310 destination=local_path, 

1311 destination_parts=local_parts, 

1312 size=size, 

1313 last_modified=modified, 

1314 current=current, 

1315 ) 

1316 ) 

1317 

1318 self._validate_sync_plan(objects) 

1319 return objects, directory_markers 

1320 

1321 @staticmethod 

1322 def _validate_windows_download_part(part: str, source_key: str) -> None: 

1323 if not sys.platform.startswith("win"): 

1324 return 

1325 if part.endswith((".", " ")): 

1326 raise ValueError(f"Unsafe Windows S3 object key cannot be synced: {source_key!r}") 

1327 if any( 

1328 unicodedata.category(character) == "Cc" or character in '<>:"|?*' for character in part 

1329 ): 

1330 raise ValueError(f"Unsafe Windows S3 object key cannot be synced: {source_key!r}") 

1331 

1332 stem = part.split(".", 1)[0].rstrip(" .").casefold() 

1333 reserved = {"con", "prn", "aux", "nul", "clock$", "conin$", "conout$"} 

1334 numbered_suffixes = "123456789¹²³" 

1335 if stem in reserved or ( 

1336 len(stem) == 4 and stem[:3] in {"com", "lpt"} and stem[3] in numbered_suffixes 

1337 ): 

1338 raise ValueError(f"Reserved Windows path cannot be synced: {source_key!r}") 

1339 

1340 @classmethod 

1341 def _download_relative_parts(cls, relative_key: str, source_key: str) -> tuple[str, ...]: 

1342 if "\x00" in relative_key or "\\" in relative_key: 

1343 raise ValueError(f"Unsafe S3 object key cannot be synced: {source_key!r}") 

1344 parts = tuple(relative_key.split("/")) 

1345 if not relative_key or any(part in ("", ".", "..") for part in parts): 

1346 raise ValueError(f"Unsafe S3 object key cannot be synced: {source_key!r}") 

1347 for part in parts: 

1348 cls._validate_windows_download_part(part, source_key) 

1349 return parts 

1350 

1351 @staticmethod 

1352 def _local_collision_parts(path: Path) -> tuple[str, ...]: 

1353 """Return path components normalized conservatively for the host filesystem.""" 

1354 if sys.platform.startswith("win"): 

1355 return tuple(unicodedata.normalize("NFC", part).casefold() for part in path.parts) 

1356 parts = tuple(os.path.normcase(part) for part in path.parts) 

1357 if sys.platform == "darwin": 

1358 # Default macOS volumes compare names case-insensitively and apply 

1359 # Unicode normalization even though os.path.normcase is a no-op. 

1360 return tuple(unicodedata.normalize("NFD", part).casefold() for part in parts) 

1361 return parts 

1362 

1363 @classmethod 

1364 def _validate_sync_plan(cls, objects: list[_SyncObject]) -> None: 

1365 """Reject object sets that cannot be represented without collisions.""" 

1366 seen: dict[tuple[str, ...], _SyncObject] = {} 

1367 ordered = sorted(objects, key=lambda obj: len(obj.destination.parts)) 

1368 for obj in ordered: 

1369 collision_key = cls._local_collision_parts(obj.destination) 

1370 conflicting = seen.get(collision_key) 

1371 if conflicting is not None: 

1372 raise ValueError( 

1373 "S3 object keys map to the same local path: " 

1374 f"{conflicting.key!r} and {obj.key!r}" 

1375 ) 

1376 for length in range(1, len(collision_key)): 

1377 ancestor = seen.get(collision_key[:length]) 

1378 if ancestor is not None: 

1379 raise ValueError( 

1380 "S3 object keys have a local file/directory collision: " 

1381 f"{ancestor.key!r} and {obj.key!r}" 

1382 ) 

1383 seen[collision_key] = obj 

1384 

1385 @classmethod 

1386 def _safe_local_path(cls, destination: Path, relative_key: str, source_key: str) -> Path: 

1387 parts = cls._download_relative_parts(relative_key, source_key) 

1388 candidate = destination.joinpath(*parts).resolve() 

1389 if candidate == destination or not candidate.is_relative_to(destination): 

1390 raise ValueError(f"S3 object key escapes the sync destination: {source_key!r}") 

1391 for parent in candidate.parents: 1391 ↛ 1398line 1391 didn't jump to line 1398 because the loop on line 1391 didn't complete

1392 if parent == destination: 

1393 break 

1394 if parent.exists() and not parent.is_dir(): 

1395 raise NotADirectoryError( 

1396 f"S3 object '{source_key}' has a local parent that is not a directory: {parent}" 

1397 ) 

1398 if candidate.exists() and candidate.is_dir(): 

1399 raise IsADirectoryError( 

1400 f"S3 object '{source_key}' maps to an existing directory: {candidate}" 

1401 ) 

1402 return candidate 

1403 

1404 @staticmethod 

1405 def _is_current(path: Path, size: int, modified: datetime | None) -> bool: 

1406 if not path.is_file() or modified is None: 

1407 return False 

1408 path_stat = path.stat() 

1409 return path_stat.st_size == size and int(path_stat.st_mtime) >= int(modified.timestamp()) 

1410 

1411 

1412def get_storage_manager(config: GCOConfig | None = None) -> StorageManager: 

1413 """Return a storage manager using the merged CLI configuration.""" 

1414 return StorageManager(config)