10.6. بناء لوحة المعلومات¶
إن الواجهة البرمجية بصيغة JSON الخاصة بالكاميرا جيدة لـ curl، لكن المالك يريد توجيه هاتفه إلى عنوان URL ثم يرى شيئًا ما. وهذا يعني تقديم HTML وJS وCSS من الكاميرا إلى جانب الواجهة البرمجية.
10.6.1. الملفات الساكنة من نظام الملفات¶
تقرأ microdot.Response.send_file() ملفًا من نظام ملفات الكاميرا وتكتبه مرة أخرى بـ Content-Type الصحيح المُستنتَج من الامتداد. ويرسل مسار شامل واحد في أسفل جدول التوجيه كل ما لم يطابق مسارًا برمجيًا:
@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> -- إذ يعالج المتصفح الاستجابة متعددة الأجزاء بشفافية. وتقرأ المنزلقة العتبة من /config عند تحميل الصفحة وترسل القيمة الجديدة عند كل تغيير.
10.6.3. أنواع MIME والضغط¶
تقرأ send_file() امتداد الملف وتختار Content-Type من microdot.Response.types_map. فيصبح .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. ويفك المتصفح الضغط بشفافية فتوفّر بضعة كيلوبايتات عند كل تحميل للصفحة.
يفتح المالك الآن http://yard-cam.local/index.html فيرى البث المباشر، والعتبة الحالية، وسجل أحداث فارغ.