## Implementation Plan ### Goal Prevent container sync failures when a binary file’s blob is missing from disk. Two changes in `devplacepy/project_files.py` plus a supporting change in `delete_node` to reduce future orphans. --- ### Files to Modify **1. `devplacepy/project_files.py`** #### 1a. `_export_node()` – guard `shutil.copyfile` (≈line 581) ```python def _export_node(row: dict, dest: Path) -> None: target = (dest / row["path"]).resolve() target.parent.mkdir(parents=True, exist_ok=True) # already present if row.get("is_binary") and row.get("stored_name") and row.get("directory"): source = PROJECT_FILES_DIR / row["directory"] / row["stored_name"] try: shutil.copyfile(source, target) except (FileNotFoundError, OSError) as exc: logger.warning( "Missing blob for node %s (%s): %s", row.get("uid"), row.get("path"), exc, ) return # skip this file, continue sync else: target.write_text(row.get("content") or "", encoding="utf-8") ``` #### 1b. `export_to_dir()` – guard the same copy (≈line 711) ```python # Inside the loop over rows, where shutil.copyfile is called: source = PROJECT_FILES_DIR / row["directory"] / row["stored_name"] try: shutil.copyfile(source, target) except (FileNotFoundError, OSError) as exc: logger.warning( "Missing blob during full export for node %s (%s): %s", row.get("uid"), row.get("path"), exc, ) continue # skip this file, proceed with other files ``` #### 1c. `delete_node()` – remove blob on soft delete (≈line 459-470) Add a call to `_unlink_blob(row)` after the `update` inside the loop: ```python for row in _descendants(project_uid, path): _table().update( {"uid": row["uid"], "deleted_at": stamp, "deleted_by": deleted_by}, ["uid"], ) _unlink_blob(row) # NEW – delete blob if present (safe, already guarded) ``` --- **2. Test files** (create/update to cover the missing-blob scenario) #### 2a. `tests/unit/project_files.py` Add a test function that: - Creates a project file record with `is_binary=1`, `stored_name`, `directory`, but **no actual blob on disk**. - Calls `_export_node` (or `export_to_dir`) and verifies it completes without raising `FileNotFoundError`. - Asserts the warning is logged (or the function returns gracefully). #### 2b. `tests/unit/services/containers.py` Add a test that: - Sets up a project with one binary file with a missing blob. - Runs the sync (or its internal helper) and asserts the sync succeeds (non-zero `files_exported` count for the text files, zero failure). --- ### Verification Steps (Definition of Done) - [ ] The two guard blocks (1a, 1b) and the `_unlink_blob` call (1c) are applied exactly as described in `devplacepy/project_files.py`. - [ ] The two new test functions (2a, 2b) are added to their respective test files. - [ ] **Lint command passes** Run `pip install -q ruff && ruff check .` – no new violations. - [ ] **Unit tests pass** Run `pip install -e '.[dev]' -q && make test-unit` – all tests pass (including the new ones). - [ ] The fix is manually verified by inspecting the log output: a missing blob now produces only a warning, and the sync continues for other files.