--- phase: 05-package-export plan: "01" subsystem: api/packages tags: [fastapi, zip, intunewin, package-export, tdd] dependency_graph: requires: - imptune/generators/script_generator.py (render_install, render_uninstall, render_detect) - imptune/generators/intunewin_builder.py (build_intunewin) - imptune/api/scripts.py (_get_printer_and_driver pattern) - imptune/db/models.py (Printer, Driver ORM) - imptune/config.py (DRIVERS_DIR) provides: - GET /printers/{id}/packages/ninja (NinjaRMM ZIP download) - GET /printers/{id}/packages/intunewin (.intunewin download) affects: - imptune/main.py (router registration) tech_stack: added: [] patterns: - In-memory ZIP assembly with io.BytesIO + zipfile.ZipFile - TemporaryDirectory context manager for auto-cleanup of intunewin build artifacts - Driver ZIP existence validation before processing key_files: created: - imptune/api/packages.py - tests/test_packages.py modified: - imptune/main.py decisions: - _get_printer_and_driver() copied (not imported) from scripts.py for module self-containment - NinjaRMM ZIP uses DEFLATE compression with {printer_name}/install.ps1 + {printer_name}/drivers/* structure - intunewin endpoint uses TemporaryDirectory for auto-cleanup of tmp build files (no manual cleanup needed) - Driver ZIP file existence validated on disk before building package (422 if missing) metrics: duration: "~2 min" completed_date: "2026-04-10" tasks_completed: 1 files_modified: 3 requirements-completed: [PKG-01, PKG-02, PKG-03] --- # Phase 5 Plan 1: Package Export Endpoints Summary **One-liner:** NinjaRMM ZIP and .intunewin package export endpoints using in-memory ZIP assembly and Python-native intunewin build. ## What Was Built Two GET endpoints on `imptune/api/packages.py`: 1. **`GET /printers/{id}/packages/ninja`** — Returns a ZIP file (application/zip) with: - `{safe_name}/install.ps1` — rendered PowerShell install script - `{safe_name}/drivers/*` — all driver files extracted from the driver ZIP on disk - Built entirely in-memory with `io.BytesIO` + `zipfile.ZipFile(ZIP_DEFLATED)` 2. **`GET /printers/{id}/packages/intunewin`** — Returns a `.intunewin` file (application/octet-stream) with: - Writes install.ps1, uninstall.ps1, detect.ps1 into a `TemporaryDirectory` - Extracts driver ZIP into `tmpdir/drivers/` - Calls `build_intunewin(tmpdir, "install.ps1", output_path)` — no subprocess calls - Reads bytes and returns as binary Response Both endpoints share `_get_printer_and_driver()` helper (404 for missing printer, 422 for no/invalid driver) and validate the driver ZIP file exists on disk (422 if missing). Router registered in `imptune/main.py` after `scripts.router`. ## Tests 9 new tests in `tests/test_packages.py`: - `TestNinjaDownload`: 5 tests (zip response, install.ps1 in zip, driver files in zip, 404, 422) - `TestIntunewinDownload`: 4 tests (intunewin response, valid outer ZIP structure, 404, 422) Full suite result: 84 passed (excluding pre-existing icon upload failures in test_icon_upload.py which existed before this plan). ## Deviations from Plan None — plan executed exactly as written. ## Pre-existing Issues (Out of Scope) `tests/test_icon_upload.py` has 5 failing tests (`/printers/{id}/icon` returns 404). These failures existed before this plan was executed and are unrelated to package export. Logged for future attention. ## Commits | Hash | Type | Description | | ------- | ------ | ------------------------------------------------------------- | | a31c71e | test | add failing tests for NinjaRMM ZIP and intunewin endpoints | | dd6cedf | feat | implement NinjaRMM ZIP and intunewin package export endpoints | ## Self-Check: PASSED - FOUND: imptune/api/packages.py - FOUND: tests/test_packages.py - FOUND: imptune/main.py (modified) - FOUND commit a31c71e (RED tests) - FOUND commit dd6cedf (GREEN implementation)