10.6. Создание панели управления¶
JSON API камеры хорош для curl, но владелец хочет навести телефон на URL и увидеть что-то. Это означает раздачу HTML, JS и CSS с камеры наряду с API.
10.6.1. Статические файлы из файловой системы¶
microdot.Response.send_file() читает файл из файловой системы камеры и отдаёт его обратно с правильным Content-Type, выведенным из расширения. Один универсальный маршрут в самом низу таблицы маршрутизации отдаёт всё, что не совпало с маршрутом API:
@app.get('/<path:filename>')
async def static(request, filename):
if '..' in filename or filename.startswith('/'):
abort(403)
return Response.send_file('/sdcard/static/' + filename)
Преобразователь <path:filename> с предыдущей страницы совпадает с сегментами, содержащими слеши – так что и app.js, и css/site.css проходят. Двухстрочная проверка отклоняет любой путь, пытающийся выйти за пределы /sdcard/static/ через .. или абсолютный путь – без неё любопытный клиент мог бы прочитать ../../boot.py или /flash/secrets.txt. Сам send_file() не выполняет никакой санитизации: он открывает любой путь, который вы ему передадите.
10.6.2. Файлы панели управления¶
Три файла на камере в каталоге /sdcard/static/:
<!-- /sdcard/static/index.html -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Backyard cam</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Backyard cam</h1>
<img id="stream" src="/stream.jpg">
<label>Threshold:
<input id="threshold" type="range" min="0" max="100">
<span id="threshold-value"></span>
</label>
<h2>Recent events</h2>
<ul id="events"></ul>
<script src="app.js"></script>
</body>
</html>
// /sdcard/static/app.js
const slider = document.getElementById('threshold');
const sliderValue = document.getElementById('threshold-value');
async function loadConfig() {
const r = await fetch('/config');
const cfg = await r.json();
slider.value = cfg.threshold;
sliderValue.textContent = cfg.threshold;
}
loadConfig();
slider.addEventListener('change', async () => {
sliderValue.textContent = slider.value;
await fetch('/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({threshold: parseInt(slider.value)}),
});
});
/* /sdcard/static/style.css */
body { font-family: sans-serif; max-width: 720px; margin: 1em auto; }
img#stream { width: 100%; }
HTML загружает MJPEG-поток прямо в <img> – браузер прозрачно обрабатывает multipart-ответ. Слайдер читает порог из /config при загрузке страницы и отправляет новое значение обратно при каждом изменении.
10.6.3. MIME-типы и сжатие¶
send_file() читает расширение файла и выбирает Content-Type из microdot.Response.types_map. .html становится text/html, .js становится text/javascript, .css становится text/css, .jpg становится image/jpeg. Расширения, которые вы добавили в свою панель управления (.svg, .ico), уже зарегистрированы. Всё неизвестное по умолчанию получает application/octet-stream – чтобы добавить новое сопоставление, один раз при запуске расширьте Response.types_map.
Для ресурсов, которые вы предварительно сжали (style.css.gz, app.js.gz), передайте compressed=True, и microdot отдаст файл .gz с заголовком Content-Encoding: gzip. Браузер прозрачно его распакует, а вы сэкономите несколько КБ при каждой загрузке страницы.
Теперь владелец открывает http://yard-cam.local/index.html и видит живую трансляцию, текущий порог и пустой журнал событий.