10.6. 대시보드 만들기#

카메라의 JSON API는 curl에는 좋지만, 소유자는 휴대폰을 URL로 향하게 하여 무언가를 보고 싶어 합니다. 즉, API와 함께 카메라에서 HTML, JS, CSS를 제공해야 한다는 뜻입니다.

10.6.1. 파일 시스템에서 제공하는 정적 파일#

microdot.Response.send_file()은 카메라의 파일 시스템에서 파일을 읽어, 확장자로부터 추론한 올바른 Content-Type과 함께 되돌려 보냅니다. 라우팅 테이블 맨 아래의 포괄(catch-all) 라우트 하나가 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 둘 다 통과합니다. 두 줄짜리 안전성 검사는 ..이나 절대 경로로 /sdcard/static/을 벗어나려는 모든 경로를 거부합니다 – 이것이 없으면 호기심 많은 클라이언트가 ../../boot.py/flash/secrets.txt를 읽을 수 있습니다. send_file() 자체는 어떤 정화(sanitization)도 하지 않습니다: 넘겨받은 경로가 무엇이든 그대로 엽니다.

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()은 파일의 확장자를 읽고 microdot.Response.types_map에서 Content-Type을 선택합니다. .htmltext/html, .jstext/javascript, .csstext/css, .jpgimage/jpeg가 됩니다. 대시보드에 추가한 확장자(.svg, .ico)는 이미 등록되어 있습니다. 알 수 없는 것은 application/octet-stream으로 기본 설정됩니다 – 새 매핑을 추가하려면 시작 시점에 Response.types_map을 한 번 확장하세요.

미리 압축해 둔 자산(style.css.gz, app.js.gz)의 경우 compressed=True를 전달하면 microdot가 .gz 파일을 Content-Encoding: gzip과 함께 제공합니다. 브라우저는 투명하게 압축을 해제하고, 페이지 로드마다 수 KB를 절약하게 됩니다.

이제 소유자는 http://yard-cam.local/index.html을 열어 실시간 피드, 현재 임계값, 그리고 비어 있는 이벤트 로그를 봅니다.