// ============================================================ // Infinite Flash Drive — Cloud Server (pure Node.js, ไม่ต้อง npm install) // ใช้กับคลิป ESP32-S3 "The Infinite Flash Drive" // // รันด้วย: node server.js // (ใช้แค่โมดูล http + fs ในตัว Node เท่านั้น ไม่มี dependency) // // Endpoints: // GET /health -> เช็คว่า server ยังไม่ตาย // GET /files -> คืน list ไฟล์ [{name,size}] (Level 3 ใช้) // GET /file/ -> ดาวน์โหลดไฟล์ รองรับ HTTP Range (Level 3 streaming) // POST /upload?name= -> อัปโหลดไฟล์ (body = ไบต์ไฟล์ดิบ) (Level 2 ใช้) // // ไฟล์ทั้งหมดเก็บในโฟลเดอร์ ./storage // ============================================================ const http = require('http'); const fs = require('fs'); const path = require('path'); const url = require('url'); const PORT = process.env.PORT || 3000; const STORAGE = path.join(__dirname, 'storage'); // สร้างโฟลเดอร์ storage + ไฟล์ตัวอย่าง เพื่อให้ Level 3 มีของโชว์ทันที if (!fs.existsSync(STORAGE)) fs.mkdirSync(STORAGE, { recursive: true }); seedSampleFiles(); function seedSampleFiles() { const hello = path.join(STORAGE, 'HELLO.TXT'); if (!fs.existsSync(hello)) { fs.writeFileSync( hello, 'สวัสดีครับ! ไฟล์นี้ไม่ได้อยู่ในบอร์ด ESP32 เลย\r\n' + 'มันถูกสตรีมมาจาก server แบบ real-time ตอนคุณเปิดอ่าน\r\n' + 'นี่คือ Infinite Flash Drive :)\r\n' ); } // ไฟล์ใหญ่ปลอมๆ ไว้โชว์ว่า "ความจุไม่จริง" — 5 MB ของอักษร A const big = path.join(STORAGE, 'BIGFILE.TXT'); if (!fs.existsSync(big)) { const chunk = Buffer.alloc(1024 * 1024, 0x41); // 1MB ของ 'A' const fd = fs.openSync(big, 'w'); for (let i = 0; i < 5; i++) fs.writeSync(fd, chunk); fs.closeSync(fd); } } function listFiles() { return fs.readdirSync(STORAGE) .filter((n) => fs.statSync(path.join(STORAGE, n)).isFile()) .map((n) => ({ name: n, size: fs.statSync(path.join(STORAGE, n)).size })); } // กัน path หลุดออกนอกโฟลเดอร์ storage (path traversal) function safePath(name) { const clean = path.basename(name || ''); if (!clean || clean === '.' || clean === '..') return null; return path.join(STORAGE, clean); } const server = http.createServer((req, res) => { const parsed = url.parse(req.url, true); const pathname = decodeURIComponent(parsed.pathname); const t = new Date().toISOString().slice(11, 19); console.log(`[${t}] ${req.method} ${pathname}`); // ---- GET /health ---- if (req.method === 'GET' && pathname === '/health') { return json(res, 200, { ok: true, files: listFiles().length }); } // ---- GET /files ---- (Level 3 ดึงรายชื่อไฟล์) if (req.method === 'GET' && pathname === '/files') { return json(res, 200, listFiles()); } // ---- POST /upload?name=xxx ---- (Level 2 push ไฟล์ขึ้นมา) if (req.method === 'POST' && pathname === '/upload') { const name = parsed.query.name; const dest = safePath(name); if (!dest) return json(res, 400, { error: 'missing or bad ?name=' }); const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const body = Buffer.concat(chunks); fs.writeFileSync(dest, body); console.log(` ↳ saved ${path.basename(dest)} (${body.length} bytes)`); json(res, 200, { ok: true, name: path.basename(dest), size: body.length }); }); req.on('error', () => json(res, 500, { error: 'upload failed' })); return; } // ---- GET /file/ ---- (Level 3 streaming, รองรับ Range) ---- if (req.method === 'GET' && pathname.startsWith('/file/')) { const name = pathname.slice('/file/'.length); const fpath = safePath(name); if (!fpath || !fs.existsSync(fpath)) return json(res, 404, { error: 'not found' }); const size = fs.statSync(fpath).size; const range = req.headers['range']; if (range) { // ตัวอย่าง header: Range: bytes=0-511 const m = /bytes=(\d+)-(\d*)/.exec(range); let start = m ? parseInt(m[1], 10) : 0; let end = m && m[2] ? parseInt(m[2], 10) : size - 1; if (isNaN(start) || start < 0) start = 0; if (isNaN(end) || end >= size) end = size - 1; if (start > end) { res.writeHead(416); return res.end(); } res.writeHead(206, { 'Content-Type': 'application/octet-stream', 'Content-Range': `bytes ${start}-${end}/${size}`, 'Accept-Ranges': 'bytes', 'Content-Length': end - start + 1, }); return fs.createReadStream(fpath, { start, end }).pipe(res); } // ไม่มี Range = ส่งทั้งไฟล์ res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Accept-Ranges': 'bytes', 'Content-Length': size, }); return fs.createReadStream(fpath).pipe(res); } json(res, 404, { error: 'unknown route' }); }); function json(res, code, obj) { const body = JSON.stringify(obj); res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(body); } server.listen(PORT, () => { console.log('=================================================='); console.log(' Infinite Flash Drive — Cloud Server'); console.log(` ฟังอยู่ที่พอร์ต ${PORT}`); console.log(` เก็บไฟล์ที่: ${STORAGE}`); console.log(' ---'); console.log(` เทสได้เลย: curl http://localhost:${PORT}/files`); console.log('=================================================='); });