10.6. Panonun oluşturulması

Kameranın JSON API’si curl için iyidir, ancak sahibi bir telefonu bir URL’ye yöneltmek ve bir şeyler görmek ister. Bu, kameradan API ile birlikte HTML, JS ve CSS sunmak anlamına gelir.

10.6.1. Dosya sisteminden statik dosyalar

microdot.Response.send_file(), kameranın dosya sisteminden bir dosya okur ve uzantıdan çıkarılan doğru Content-Type ile geri yazar. Yönlendirme tablosunun altındaki bir yakala-tümü rotası, bir API rotasıyla eşleşmeyen her şeyi gönderir:

@app.get('/<path:filename>')
async def static(request, filename):
    if '..' in filename or filename.startswith('/'):
        abort(403)
    return Response.send_file('/sdcard/static/' + filename)

Önceki sayfadan gelen <path:filename> dönüştürücüsü, eğik çizgi içeren segmentlerle eşleşir; app.js ve css/site.css ikisi de gelir. İki satırlık sağlamlık kontrolü, /sdcard/static/ dizininden .. veya mutlak bir yolla kaçmaya çalışan herhangi bir yolu reddeder; o olmadan meraklı bir istemci ../../boot.py veya /flash/secrets.txt dosyasını okuyabilir. send_file() kendisi hiçbir temizleme yapmaz: kendisine verdiğiniz hangi yolsa onu açar.

10.6.2. Pano dosyaları

Kamerada /sdcard/static/ altında üç dosya:

<!-- /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 akışını doğrudan bir <img> içine yükler; tarayıcı çok parçalı yanıtı şeffaf bir şekilde işler. Kaydırıcı, sayfa yüklenirken /config adresinden eşiği okur ve her değişiklikte yeni değeri geri gönderir.

10.6.3. MIME türleri ve sıkıştırma

send_file(), dosyanın uzantısını okur ve microdot.Response.types_map içinden bir Content-Type seçer. .html, text/html olur, .js, text/javascript olur, .css, text/css olur, .jpg, image/jpeg olur. Panonuza eklediğiniz uzantılar (.svg, .ico) zaten kayıtlıdır. Bilinmeyen herhangi bir şey varsayılan olarak application/octet-stream olur; yeni bir eşleme eklemek için başlangıçta Response.types_map öğesini bir kez genişletin.

Önceden sıkıştırdığınız varlıklar (style.css.gz, app.js.gz) için compressed=True geçirin; microdot .gz dosyasını Content-Encoding: gzip ile sunar. Tarayıcı şeffaf bir şekilde açar ve her sayfa yüklemesinde birkaç KB tasarruf edersiniz.

Sahibi artık http://yard-cam.local/index.html adresini açar ve canlı yayını, mevcut eşiği ve boş bir olay günlüğünü görür.