Private
Public Access
addet test set
This commit is contained in:
1 parent
d370f992ce
commit
b0f35bf3cc
21 files changed
+1796
-183
No files matched your search
+98
-10
@@ -3,6 +3,12 @@
|
||||
from contextlib import closing
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
from PIL import Image
|
||||
import math
|
||||
import os
|
||||
import sqlite3
|
||||
@@ -36,6 +42,7 @@ class EventStore:
|
||||
def __init__(self, state_dir: Path):
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.path = state_dir / "events.sqlite3"
|
||||
self.image_dir = state_dir / "event-images"
|
||||
with closing(self._connect()) as connection:
|
||||
with connection:
|
||||
connection.execute("""
|
||||
@@ -75,6 +82,8 @@ class EventStore:
|
||||
"ALTER TABLE events ADD COLUMN profile_id INTEGER "
|
||||
"REFERENCES profiles(id) ON DELETE SET NULL"
|
||||
)
|
||||
if "image_filename" not in columns:
|
||||
connection.execute("ALTER TABLE events ADD COLUMN image_filename TEXT")
|
||||
connection.execute("""
|
||||
CREATE TABLE IF NOT EXISTS profile_templates (
|
||||
profile_id INTEGER PRIMARY KEY
|
||||
@@ -98,18 +107,87 @@ class EventStore:
|
||||
def record_test_trigger(self, mode: str) -> int:
|
||||
return self._record_trigger(mode, "test_trigger")
|
||||
|
||||
def record_detection_trigger(self, mode: str) -> int:
|
||||
return self._record_trigger(mode, "person_line_crossing")
|
||||
def record_detection_trigger(self, mode: str, *, jpeg=None, region=None) -> int:
|
||||
return self._record_trigger(mode, "person_line_crossing", jpeg, region)
|
||||
|
||||
def _record_trigger(self, mode: str, kind: str) -> int:
|
||||
def record_motion_trigger(self, mode: str, *, jpeg=None, region=None) -> int:
|
||||
return self._record_trigger(mode, "motion_direction_test", jpeg, region)
|
||||
|
||||
def _save_image(self, jpeg: bytes, region) -> str:
|
||||
if (region is None or len(region) != 4 or
|
||||
any(not math.isfinite(v) or not 0 <= v <= 1 for v in region) or
|
||||
region[0] >= region[2] or region[1] >= region[3]):
|
||||
raise ValueError("Ungültiger Bildbereich")
|
||||
with Image.open(BytesIO(jpeg)) as image:
|
||||
width, height = image.size
|
||||
# Round inward: no pixel outside the chosen private region is saved.
|
||||
box = (math.ceil(region[0]*width), math.ceil(region[1]*height),
|
||||
math.floor(region[2]*width), math.floor(region[3]*height))
|
||||
if box[2] <= box[0] or box[3] <= box[1]:
|
||||
raise ValueError("Bildbereich ist zu klein")
|
||||
cropped = image.crop(box).convert('RGB')
|
||||
self.image_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
os.chmod(self.image_dir, 0o700)
|
||||
filename = uuid.uuid4().hex + '.jpg'
|
||||
target = self.image_dir / filename
|
||||
with tempfile.NamedTemporaryFile(dir=self.image_dir, suffix='.tmp', delete=False) as stream:
|
||||
temporary = Path(stream.name)
|
||||
try:
|
||||
cropped.save(stream, format='JPEG', quality=90)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, target)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return filename
|
||||
|
||||
def _record_trigger(self, mode: str, kind: str, jpeg=None, region=None) -> int:
|
||||
occurred_at = _timestamp(_utc_now())
|
||||
filename = self._save_image(jpeg, region) if jpeg is not None else None
|
||||
try:
|
||||
with closing(self._connect()) as connection:
|
||||
with connection:
|
||||
cursor = connection.execute(
|
||||
"INSERT INTO events (occurred_at, kind, mode, image_filename) VALUES (?, ?, ?, ?)",
|
||||
(occurred_at, kind, mode, filename),
|
||||
)
|
||||
event_id = cursor.lastrowid
|
||||
return event_id
|
||||
except Exception:
|
||||
if filename:
|
||||
(self.image_dir / filename).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
def _image_path(self, filename):
|
||||
if not isinstance(filename, str) or not re.fullmatch(r'[0-9a-f]{32}\.jpg', filename):
|
||||
raise KeyError("Kein Bild für dieses Ereignis")
|
||||
path = self.image_dir / filename
|
||||
if path.is_symlink():
|
||||
raise KeyError("Kein Bild für dieses Ereignis")
|
||||
return path
|
||||
|
||||
def image_path(self, event_id: int) -> Path:
|
||||
with closing(self._connect()) as connection:
|
||||
row = connection.execute("SELECT image_filename FROM events WHERE id = ?", (event_id,)).fetchone()
|
||||
if row is None:
|
||||
raise KeyError("Ereignis nicht gefunden")
|
||||
path = self._image_path(row['image_filename'])
|
||||
if not path.is_file():
|
||||
raise KeyError("Bild nicht vorhanden")
|
||||
return path
|
||||
|
||||
def delete_image(self, event_id: int) -> bool:
|
||||
with closing(self._connect()) as connection:
|
||||
with connection:
|
||||
cursor = connection.execute(
|
||||
"INSERT INTO events (occurred_at, kind, mode) VALUES (?, ?, ?)",
|
||||
(occurred_at, kind, mode),
|
||||
)
|
||||
return cursor.lastrowid
|
||||
row = connection.execute("SELECT image_filename FROM events WHERE id = ?", (event_id,)).fetchone()
|
||||
if row is None:
|
||||
raise KeyError("Ereignis nicht gefunden")
|
||||
if row['image_filename'] is None:
|
||||
return False
|
||||
path = self._image_path(row['image_filename'])
|
||||
path.unlink(missing_ok=True)
|
||||
connection.execute("UPDATE events SET image_filename = NULL WHERE id = ?", (event_id,))
|
||||
return True
|
||||
|
||||
def recent(self, limit: int = 20) -> list[dict]:
|
||||
if not 1 <= limit <= 100:
|
||||
@@ -118,11 +196,21 @@ class EventStore:
|
||||
with closing(self._connect()) as connection:
|
||||
rows = connection.execute(
|
||||
"SELECT e.id, e.occurred_at, e.kind, e.mode, "
|
||||
"e.profile_id, p.label AS profile_label "
|
||||
"e.profile_id, e.image_filename, p.label AS profile_label "
|
||||
"FROM events e LEFT JOIN profiles p ON p.id = e.profile_id "
|
||||
"ORDER BY e.id DESC LIMIT ?", (limit,),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
result = []
|
||||
for row in rows:
|
||||
event = dict(row)
|
||||
filename = event.pop('image_filename')
|
||||
try:
|
||||
present = self._image_path(filename).is_file()
|
||||
except KeyError:
|
||||
present = False
|
||||
event['image_url'] = f"/api/events/{event['id']}/image" if present else None
|
||||
result.append(event)
|
||||
return result
|
||||
|
||||
def create_profile(self, label: str) -> dict:
|
||||
label = label.strip()
|
||||
|
||||
Reference in new issue
Block a user