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.js와 css/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을 선택합니다. .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과 함께 제공합니다. 브라우저는 투명하게 압축을 해제하고, 페이지 로드마다 수 KB를 절약하게 됩니다.
이제 소유자는 http://yard-cam.local/index.html을 열어 실시간 피드, 현재 임계값, 그리고 비어 있는 이벤트 로그를 봅니다.