pumaguard 20.post174__py3-none-any.whl → 20.post190__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pumaguard/main.py +7 -1
- pumaguard/presets.py +14 -1
- pumaguard/pumaguard-ui/flutter_bootstrap.js +1 -1
- pumaguard/pumaguard-ui/flutter_service_worker.js +1 -1
- pumaguard/server.py +18 -0
- pumaguard/web_routes/__init__.py +0 -0
- pumaguard/web_routes/artifacts.py +114 -0
- pumaguard/web_routes/diagnostics.py +89 -0
- pumaguard/web_routes/directories.py +67 -0
- pumaguard/web_routes/folders.py +121 -0
- pumaguard/web_routes/photos.py +112 -0
- pumaguard/web_routes/settings.py +116 -0
- pumaguard/web_routes/sync.py +130 -0
- pumaguard/web_ui.py +48 -519
- {pumaguard-20.post174.dist-info → pumaguard-20.post190.dist-info}/METADATA +1 -1
- {pumaguard-20.post174.dist-info → pumaguard-20.post190.dist-info}/RECORD +20 -12
- {pumaguard-20.post174.dist-info → pumaguard-20.post190.dist-info}/WHEEL +0 -0
- {pumaguard-20.post174.dist-info → pumaguard-20.post190.dist-info}/entry_points.txt +0 -0
- {pumaguard-20.post174.dist-info → pumaguard-20.post190.dist-info}/licenses/LICENSE +0 -0
- {pumaguard-20.post174.dist-info → pumaguard-20.post190.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Settings routes for loading, saving, and updating presets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import (
|
|
4
|
+
annotations,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
from flask import (
|
|
14
|
+
jsonify,
|
|
15
|
+
request,
|
|
16
|
+
)
|
|
17
|
+
from yaml.representer import (
|
|
18
|
+
YAMLError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from flask import (
|
|
23
|
+
Flask,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from pumaguard.web_ui import (
|
|
27
|
+
WebUI,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_settings_routes(app: "Flask", webui: "WebUI") -> None:
|
|
34
|
+
"""Register settings endpoints for GET, PUT, save, and load."""
|
|
35
|
+
|
|
36
|
+
@app.route("/api/settings", methods=["GET"])
|
|
37
|
+
def get_settings():
|
|
38
|
+
return jsonify(dict(webui.presets))
|
|
39
|
+
|
|
40
|
+
@app.route("/api/settings", methods=["PUT"])
|
|
41
|
+
def update_settings():
|
|
42
|
+
try:
|
|
43
|
+
data = request.json
|
|
44
|
+
if not data:
|
|
45
|
+
return jsonify({"error": "No data provided"}), 400
|
|
46
|
+
|
|
47
|
+
allowed_settings = [
|
|
48
|
+
"YOLO-min-size",
|
|
49
|
+
"YOLO-conf-thresh",
|
|
50
|
+
"YOLO-max-dets",
|
|
51
|
+
"YOLO-model-filename",
|
|
52
|
+
"classifier-model-filename",
|
|
53
|
+
"deterrent-sound-file",
|
|
54
|
+
"file-stabilization-extra-wait",
|
|
55
|
+
"play-sound",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if len(data) == 0:
|
|
59
|
+
raise ValueError("Did not receive any settings")
|
|
60
|
+
|
|
61
|
+
for key, value in data.items():
|
|
62
|
+
if key in allowed_settings:
|
|
63
|
+
logger.debug("Updating %s with %s", key, value)
|
|
64
|
+
attr_name = key.replace("-", "_").replace("YOLO_", "yolo_")
|
|
65
|
+
setattr(webui.presets, attr_name, value)
|
|
66
|
+
else:
|
|
67
|
+
logger.debug("Skipping unknown/read-only setting: %s", key)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
filepath = webui.presets.settings_file
|
|
71
|
+
settings_dict = dict(webui.presets)
|
|
72
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
73
|
+
yaml.dump(settings_dict, f, default_flow_style=False)
|
|
74
|
+
logger.info("Settings updated and saved to %s", filepath)
|
|
75
|
+
except YAMLError:
|
|
76
|
+
logger.exception("Error saving settings")
|
|
77
|
+
return (
|
|
78
|
+
jsonify(
|
|
79
|
+
{
|
|
80
|
+
"error": (
|
|
81
|
+
"Settings updated but failed to save due "
|
|
82
|
+
"to an internal error"
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
500,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return jsonify(
|
|
90
|
+
{"success": True, "message": "Settings updated and saved"}
|
|
91
|
+
)
|
|
92
|
+
except ValueError as e: # pragma: no cover (unexpected)
|
|
93
|
+
logger.error("Error updating settings: %s", e)
|
|
94
|
+
return jsonify({"error": str(e)}), 500
|
|
95
|
+
|
|
96
|
+
@app.route("/api/settings/save", methods=["POST"])
|
|
97
|
+
def save_settings():
|
|
98
|
+
data = request.json
|
|
99
|
+
filepath = data.get("filepath") if data else None
|
|
100
|
+
if not filepath:
|
|
101
|
+
filepath = webui.presets.settings_file
|
|
102
|
+
settings_dict = dict(webui.presets)
|
|
103
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
104
|
+
yaml.dump(settings_dict, f, default_flow_style=False)
|
|
105
|
+
logger.info("Settings saved to %s", filepath)
|
|
106
|
+
return jsonify({"success": True, "filepath": filepath})
|
|
107
|
+
|
|
108
|
+
@app.route("/api/settings/load", methods=["POST"])
|
|
109
|
+
def load_settings():
|
|
110
|
+
data = request.json
|
|
111
|
+
filepath = data.get("filepath") if data else None
|
|
112
|
+
if not filepath:
|
|
113
|
+
return jsonify({"error": "No filepath provided"}), 400
|
|
114
|
+
webui.presets.load(filepath)
|
|
115
|
+
logger.info("Settings loaded from %s", filepath)
|
|
116
|
+
return jsonify({"success": True, "message": "Settings loaded"})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Sync routes for checksums and batch downloading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import (
|
|
4
|
+
annotations,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
import zipfile
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from flask import (
|
|
16
|
+
jsonify,
|
|
17
|
+
request,
|
|
18
|
+
send_file,
|
|
19
|
+
send_from_directory,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from flask import (
|
|
24
|
+
Flask,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from pumaguard.web_ui import (
|
|
28
|
+
WebUI,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _calculate_file_checksum(filepath: str) -> str:
|
|
33
|
+
"""Calculate SHA256 checksum of file."""
|
|
34
|
+
sha256_hash = hashlib.sha256()
|
|
35
|
+
with open(filepath, "rb") as f:
|
|
36
|
+
for byte_block in iter(lambda: f.read(4096), b""):
|
|
37
|
+
sha256_hash.update(byte_block)
|
|
38
|
+
return sha256_hash.hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register_sync_routes(app: "Flask", webui: "WebUI") -> None:
|
|
42
|
+
"""Register sync endpoints for file checksums and downloads."""
|
|
43
|
+
|
|
44
|
+
@app.route("/api/sync/checksums", methods=["POST"])
|
|
45
|
+
def calculate_checksums():
|
|
46
|
+
data = request.json
|
|
47
|
+
if not data or "files" not in data:
|
|
48
|
+
return jsonify({"error": "No files provided"}), 400
|
|
49
|
+
client_files = data["files"]
|
|
50
|
+
files_to_download = []
|
|
51
|
+
for filepath, client_checksum in client_files.items():
|
|
52
|
+
# Resolve and validate within allowed directories
|
|
53
|
+
abs_filepath = os.path.realpath(os.path.normpath(filepath))
|
|
54
|
+
allowed = False
|
|
55
|
+
for directory in webui.image_directories:
|
|
56
|
+
abs_directory = os.path.realpath(os.path.normpath(directory))
|
|
57
|
+
try:
|
|
58
|
+
common = os.path.commonpath([abs_filepath, abs_directory])
|
|
59
|
+
if common == abs_directory:
|
|
60
|
+
allowed = True
|
|
61
|
+
break
|
|
62
|
+
except ValueError:
|
|
63
|
+
# Different drives on Windows
|
|
64
|
+
continue
|
|
65
|
+
if not allowed:
|
|
66
|
+
continue
|
|
67
|
+
if not os.path.exists(abs_filepath):
|
|
68
|
+
continue
|
|
69
|
+
server_checksum = _calculate_file_checksum(abs_filepath)
|
|
70
|
+
if server_checksum != client_checksum:
|
|
71
|
+
stat = os.stat(abs_filepath)
|
|
72
|
+
files_to_download.append(
|
|
73
|
+
{
|
|
74
|
+
"path": filepath,
|
|
75
|
+
"checksum": server_checksum,
|
|
76
|
+
"size": stat.st_size,
|
|
77
|
+
"modified": stat.st_mtime,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
return jsonify(
|
|
81
|
+
{
|
|
82
|
+
"files_to_download": files_to_download,
|
|
83
|
+
"total": len(files_to_download),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@app.route("/api/sync/download", methods=["POST"])
|
|
88
|
+
def download_files():
|
|
89
|
+
data = request.json
|
|
90
|
+
if not data or "files" not in data:
|
|
91
|
+
return jsonify({"error": "No files provided"}), 400
|
|
92
|
+
file_paths = data["files"]
|
|
93
|
+
validated_files = []
|
|
94
|
+
for filepath in file_paths:
|
|
95
|
+
# Resolve and validate within allowed directories
|
|
96
|
+
abs_filepath = os.path.realpath(os.path.normpath(filepath))
|
|
97
|
+
allowed = False
|
|
98
|
+
for directory in webui.image_directories:
|
|
99
|
+
# Ensure allowed directories are normalized and real paths
|
|
100
|
+
abs_directory = os.path.realpath(os.path.normpath(directory))
|
|
101
|
+
try:
|
|
102
|
+
common = os.path.commonpath([abs_filepath, abs_directory])
|
|
103
|
+
if common == abs_directory and os.path.isfile(
|
|
104
|
+
abs_filepath
|
|
105
|
+
):
|
|
106
|
+
allowed = True
|
|
107
|
+
break
|
|
108
|
+
except ValueError:
|
|
109
|
+
# Different drives on Windows
|
|
110
|
+
continue
|
|
111
|
+
if allowed and os.path.exists(abs_filepath):
|
|
112
|
+
validated_files.append(abs_filepath)
|
|
113
|
+
if not validated_files:
|
|
114
|
+
return jsonify({"error": "No valid files to download"}), 400
|
|
115
|
+
if len(validated_files) == 1:
|
|
116
|
+
directory = os.path.dirname(validated_files[0])
|
|
117
|
+
filename = os.path.basename(validated_files[0])
|
|
118
|
+
return send_from_directory(directory, filename, as_attachment=True)
|
|
119
|
+
memory_file = io.BytesIO()
|
|
120
|
+
with zipfile.ZipFile(memory_file, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
121
|
+
for filepath in validated_files:
|
|
122
|
+
arcname = os.path.basename(filepath)
|
|
123
|
+
zf.write(filepath, arcname)
|
|
124
|
+
memory_file.seek(0)
|
|
125
|
+
return send_file(
|
|
126
|
+
memory_file,
|
|
127
|
+
mimetype="application/zip",
|
|
128
|
+
as_attachment=True,
|
|
129
|
+
download_name="pumaguard_images.zip",
|
|
130
|
+
)
|