10.6. ダッシュボードの構築

カメラのJSON APIは curl には十分ですが、所有者は携帯電話をURLに向けて何かを見たいと考えています。これは、APIと並んでカメラからHTML、JS、CSSを提供することを意味します。

10.6.1. ファイルシステムからの静的ファイル

microdot.Response.send_file() は、カメラのファイルシステムからファイルを読み込み、拡張子から推測した正しい Content-Type を付けて書き戻します。ルーティングテーブルの最下部にある1つのキャッチオールルートが、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.jscss/site.css はどちらも通過します。2行の健全性チェックは、.. や絶対パスで /sdcard/static/ から抜け出そうとするパスをすべて拒否します。これがなければ、好奇心旺盛なクライアントが ../../boot.py/flash/secrets.txt を読み取れてしまいます。send_file() 自体はサニタイズを一切行いません。渡されたパスをそのまま開きます。

10.6.2. ダッシュボードのファイル

/sdcard/static/ の下にあるカメラ上の3つのファイルです。

<!-- /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> に直接読み込みます。ブラウザはマルチパートレスポンスを透過的に処理します。スライダーはページ読み込み時に /config からしきい値を読み取り、変更のたびに新しい値を投稿し直します。

10.6.3. MIMEタイプと圧縮

send_file() はファイルの拡張子を読み取り、microdot.Response.types_map から Content-Type を選びます。.htmltext/html に、.jstext/javascript に、.csstext/css に、.jpgimage/jpeg になります。ダッシュボードに追加した拡張子(.svg.ico)はすでに登録されています。不明なものはすべて application/octet-stream にデフォルト設定されます。新しいマッピングを追加するには、起動時に一度 Response.types_map を拡張してください。

事前圧縮済みのアセット(style.css.gzapp.js.gz)の場合は compressed=True を渡すと、microdotは .gz ファイルを Content-Encoding: gzip で提供します。ブラウザは透過的に展開し、ページ読み込みごとに数KBを節約できます。

所有者はこれで http://yard-cam.local/index.html を開き、ライブフィード、現在のしきい値、そして空のイベントログを見ることができます。