10.6. 建構儀表板¶
相機的 JSON API 拿來搭配 curl 很好用,但擁有者想要拿手機對準一個 URL 並 看到 一些東西。這代表要從相機端在提供 API 之外,同時提供 HTML、JS 與 CSS。
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() 會讀取檔案的副檔名,並從 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 便會以 Content-Encoding: gzip 提供該 .gz 檔案。瀏覽器會透明地解壓縮,而你在每次頁面載入時省下幾 KB。
擁有者現在開啟 http://yard-cam.local/index.html,便會看到即時畫面、目前的閾值,以及一份空的事件記錄。