- server/sql/init.sql: devices/tracking_points/dashcam_files schema - server/modules/db.py: psycopg3 pool + query helpers - server/modules/auth.py: X-Device-Name/X-Api-Key header auth - server/app.py: /health, /api/v1/tracking/batch, /api/v1/dashcam/upload - server/Dockerfile + docker-compose.yml: api (gunicorn) + postgres services - server/scripts/create_device.py: one-time device/API-key registration - server/README.md: unraid deployment + Cloudflare Tunnel exposure docs - PLAN.md: Phase 9 marked in progress with full build notes Co-authored-by: Copilot <[email protected]>
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
"""Postgres connection pool + small query helpers for the yaris-obd remote server."""
|
|
import os
|
|
from contextlib import contextmanager
|
|
|
|
from psycopg_pool import ConnectionPool
|
|
|
|
_POOL = None
|
|
|
|
|
|
def _dsn():
|
|
return (
|
|
f"host={os.environ.get('POSTGRES_HOST', 'db')} "
|
|
f"port={os.environ.get('POSTGRES_PORT', '5432')} "
|
|
f"dbname={os.environ.get('POSTGRES_DB', 'yarisobd')} "
|
|
f"user={os.environ.get('POSTGRES_USER', 'yarisobd')} "
|
|
f"password={os.environ.get('POSTGRES_PASSWORD', '')}"
|
|
)
|
|
|
|
|
|
def get_pool():
|
|
global _POOL
|
|
if _POOL is None:
|
|
_POOL = ConnectionPool(conninfo=_dsn(), min_size=1, max_size=5, open=True)
|
|
return _POOL
|
|
|
|
|
|
@contextmanager
|
|
def get_cursor(commit=False):
|
|
"""Yield a cursor from the pool, optionally committing on clean exit."""
|
|
pool = get_pool()
|
|
with pool.connection() as conn:
|
|
with conn.cursor() as cur:
|
|
yield cur
|
|
if commit:
|
|
conn.commit()
|
|
|
|
|
|
def get_device_by_name(name):
|
|
with get_cursor() as cur:
|
|
cur.execute(
|
|
"SELECT id, name, api_key_hash FROM devices WHERE name = %s",
|
|
(name,),
|
|
)
|
|
row = cur.fetchone()
|
|
if row is None:
|
|
return None
|
|
return {"id": row[0], "name": row[1], "api_key_hash": row[2]}
|
|
|
|
|
|
def touch_device_last_seen(device_id):
|
|
with get_cursor(commit=True) as cur:
|
|
cur.execute(
|
|
"UPDATE devices SET last_seen_at = now() WHERE id = %s",
|
|
(device_id,),
|
|
)
|
|
|
|
|
|
def insert_tracking_points(device_id, points):
|
|
"""Bulk-insert tracking points, skipping any that already exist
|
|
(device_id, recorded_at) so a client can safely re-send a batch after a
|
|
partial-failure retry without creating duplicates."""
|
|
if not points:
|
|
return 0
|
|
with get_cursor(commit=True) as cur:
|
|
cur.executemany(
|
|
"""
|
|
INSERT INTO tracking_points
|
|
(device_id, vehicle_name, driver_name, recorded_at, gps_lat, gps_lon, speed, odometer)
|
|
VALUES (%(device_id)s, %(vehicle_name)s, %(driver_name)s, %(recorded_at)s,
|
|
%(gps_lat)s, %(gps_lon)s, %(speed)s, %(odometer)s)
|
|
ON CONFLICT (device_id, recorded_at) DO NOTHING
|
|
""",
|
|
[
|
|
{
|
|
"device_id": device_id,
|
|
"vehicle_name": p.get("vehicle_name"),
|
|
"driver_name": p.get("driver_name"),
|
|
"recorded_at": p["recorded_at"],
|
|
"gps_lat": p.get("gps_lat"),
|
|
"gps_lon": p.get("gps_lon"),
|
|
"speed": p.get("speed"),
|
|
"odometer": p.get("odometer"),
|
|
}
|
|
for p in points
|
|
],
|
|
)
|
|
return len(points)
|
|
|
|
|
|
def dashcam_file_exists(device_id, relative_path, sha256):
|
|
with get_cursor() as cur:
|
|
cur.execute(
|
|
"SELECT 1 FROM dashcam_files WHERE device_id = %s AND relative_path = %s AND sha256 = %s",
|
|
(device_id, relative_path, sha256),
|
|
)
|
|
return cur.fetchone() is not None
|
|
|
|
|
|
def insert_dashcam_file(device_id, relative_path, size_bytes, sha256, storage_path):
|
|
with get_cursor(commit=True) as cur:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO dashcam_files (device_id, relative_path, size_bytes, sha256, storage_path)
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
ON CONFLICT (device_id, relative_path, sha256) DO NOTHING
|
|
""",
|
|
(device_id, relative_path, size_bytes, sha256, storage_path),
|
|
)
|