feat: add snapshot FTP-to-cloud forwarder service - #277
Conversation
Adds a new snapshot/ subproject that runs an FTP server to receive Hikvision ANPR snapshot uploads (full-frame image, plate crop, and anpr.xml metadata) and forwards the matching JPEG to the Plate Recognizer Snapshot Cloud API via /v1/plate-reader/. Includes pyftpdlib-based FTP server, environment-driven configuration, example env file, and uv-managed dependencies.
There was a problem hiding this comment.
Code Review
This pull request introduces a Python-based FTP server bridge that forwards Hikvision ANPR snapshots to the Plate Recognizer Snapshot Cloud API. Key feedback includes optimizing HTTP performance by utilizing a shared requests.Session for connection pooling, adding a defensive check to ignore empty files, reordering the shutdown sequence to close the FTP server before stopping the thread pool executor, and replacing the placeholder description in pyproject.toml.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new snapshot/ftp-to-snapshot-cloud subproject that runs a small FTP server (via pyftpdlib) to receive Hikvision ANPR snapshot uploads and forward matching JPEGs to the Plate Recognizer Snapshot /v1/plate-reader/ endpoint (Cloud by default, optional SDK mode).
Changes:
- Introduces a new standalone Python service (
main.py) with env-driven configuration for FTP ingest + background HTTP forwarding. - Adds documentation (
README.md) and example configuration (.env.example) describing setup, networking, and operational behavior. - Adds
uvproject metadata (pyproject.toml,uv.lock) and local ignores (.gitignore) for the subproject.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| snapshot/ftp-to-snapshot-cloud/main.py | Implements FTP receive + suffix-based filtering + async forwarding to Snapshot endpoint. |
| snapshot/ftp-to-snapshot-cloud/README.md | Documents deployment/configuration and camera/firewall setup. |
| snapshot/ftp-to-snapshot-cloud/.env.example | Provides sample environment configuration for Cloud/SDK modes and FTP settings. |
| snapshot/ftp-to-snapshot-cloud/pyproject.toml | Declares project metadata, dependencies, and Python requirement. |
| snapshot/ftp-to-snapshot-cloud/uv.lock | Locks Python dependencies for uv installs. |
| snapshot/ftp-to-snapshot-cloud/.gitignore | Adds subproject-local ignores (venv, build artifacts). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if regions: | ||
| data["regions"] = regions | ||
| if camera_id: |
| files = {"upload": (file_path.name, fp, "image/jpeg")} | ||
| response = session.post( | ||
| url, headers=headers, data=data, files=files, timeout=timeout | ||
| ) |
| except requests.RequestException as exc: | ||
| log.error("Snapshot API request failed for %s: %s", file_path.name, exc) | ||
| return |
| log.warning("Ignoring empty matching file: %s", name) | ||
| return | ||
| log.info("Forwarding %s to Snapshot Cloud", name) | ||
| except OSError as exc: |
| config_raw = _env_str("CONFIG_JSON", "") | ||
| config = json.loads(config_raw) if config_raw.strip() else None |
| port = _env_int("FTP_PORT", 2121) | ||
| user = _env_str("FTP_USER", "camera") | ||
| password = _env_str("FTP_PASSWORD", "camera") | ||
| root = Path(_env_str("FTP_ROOT", "./uploads")).resolve() |
| def forward_to_snapshot( | ||
| file_path: Path, | ||
| *, | ||
| url: str, | ||
| token: str, | ||
| regions: list[str] | None, | ||
| camera_id: str | None, | ||
| mmc: bool, | ||
| config: dict | None, | ||
| timeout: float, | ||
| ) -> None: | ||
| """Upload ``file_path`` to a Snapshot (Cloud or SDK) plate-reader endpoint.""" |
| # Python-generated files | ||
| __pycache__/ | ||
| *.py[oc] | ||
| build/ | ||
| dist/ | ||
| wheels/ | ||
| *.egg-info | ||
|
|
||
| # Virtual environments | ||
| .venv |
| [project] | ||
| name = "ftp-to-snapshot-cloud" | ||
| version = "0.1.0" | ||
| description = "FTP server that forwards Hikvision ANPR snapshots to the Plate Recognizer Snapshot Cloud API" | ||
| requires-python = ">=3.13" | ||
| dependencies = [ |
| ## Requirements | ||
|
|
||
| * **Python 3.13 or newer** (declared in `pyproject.toml`). | ||
| * Either **[uv](https://docs.astral.sh/uv/)** (recommended, a `uv.lock` is | ||
| shipped) or **pip** + a virtual environment. | ||
| * Network access to your target Snapshot endpoint: | ||
| * **Cloud** — outbound HTTPS to `api.platerecognizer.com`. | ||
| * **SDK** — outbound HTTP to the host where the SDK is running (default | ||
| `localhost:8080`; override with `SNAPSHOT_URL`). |
|
Let's focus on the http option instead. It's simpler to manage for the end user. |
Adds a new snapshot/ subproject that runs an FTP server to receive Hikvision ANPR snapshot uploads (full-frame image, plate crop, and anpr.xml metadata) and forwards the matching JPEG to the Plate Recognizer Snapshot Cloud API via /v1/plate-reader/.
Includes pyftpdlib-based FTP server, environment-driven configuration, example env file, and uv-managed dependencies.