b1c3ec3af4
- Protocol definition with 6-byte header (chunk_idx: 2B, payload: N B, crc32: 4B) - Node.js server with chunk-based HTTP delivery - C client library with resumable download support - CRC32 implementation (IEEE 802.3) - Test suites for both server and CRC32 - 1024B default chunk size (0.59% overhead)
244 lines
6.9 KiB
JavaScript
244 lines
6.9 KiB
JavaScript
/**
|
|
* @file server.js
|
|
* @brief Node.js Chunk Transfer Server
|
|
*/
|
|
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const EventEmitter = require('events');
|
|
|
|
class ChunkServer extends EventEmitter {
|
|
constructor(config) {
|
|
super();
|
|
this.config = {
|
|
port: 8080,
|
|
rootPath: '.',
|
|
chunkSize: 1024,
|
|
...config
|
|
};
|
|
this.server = null;
|
|
this.running = false;
|
|
}
|
|
|
|
/**
|
|
* Calculate CRC32 (IEEE 802.3)
|
|
*/
|
|
static crc32(data) {
|
|
const table = ChunkServer.crc32Table;
|
|
let crc = 0xFFFFFFFF;
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
const index = (crc ^ data[i]) & 0xFF;
|
|
crc = (crc >>> 8) ^ table[index];
|
|
}
|
|
|
|
return crc ^ 0xFFFFFFFF;
|
|
}
|
|
|
|
/**
|
|
* CRC32 lookup table
|
|
*/
|
|
static get crc32Table() {
|
|
if (!ChunkServer._crc32Table) {
|
|
const table = new Uint32Array(256);
|
|
const poly = 0xEDB88320;
|
|
|
|
for (let i = 0; i < 256; i++) {
|
|
let crc = i;
|
|
for (let j = 0; j < 8; j++) {
|
|
crc = (crc & 1) ? ((crc >>> 1) ^ poly) : (crc >>> 1);
|
|
}
|
|
table[i] = crc >>> 0;
|
|
}
|
|
ChunkServer._crc32Table = table;
|
|
}
|
|
return ChunkServer._crc32Table;
|
|
}
|
|
|
|
/**
|
|
* Start server
|
|
*/
|
|
start() {
|
|
return new Promise((resolve, reject) => {
|
|
this.server = http.createServer((req, res) => {
|
|
this.handleRequest(req, res).catch(err => {
|
|
console.error('Request error:', err);
|
|
res.writeHead(500);
|
|
res.end('Internal Server Error');
|
|
});
|
|
});
|
|
|
|
this.server.listen(this.config.port, () => {
|
|
console.log(`Chunk server running on port ${this.config.port}`);
|
|
console.log(`Root path: ${this.config.rootPath}`);
|
|
console.log(`Chunk size: ${this.config.chunkSize} bytes`);
|
|
this.running = true;
|
|
resolve();
|
|
});
|
|
|
|
this.server.on('error', reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop server
|
|
*/
|
|
stop() {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.server) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
this.running = false;
|
|
this.server.close(err => {
|
|
if (err) reject(err);
|
|
else {
|
|
console.log('Server stopped');
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle HTTP request
|
|
*/
|
|
async handleRequest(req, res) {
|
|
// Parse URL
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
|
|
// Only handle /ota endpoint
|
|
if (url.pathname !== '/ota') {
|
|
res.writeHead(404);
|
|
res.end('Not Found');
|
|
return;
|
|
}
|
|
|
|
const filename = url.searchParams.get('file');
|
|
const chunkParam = url.searchParams.get('chunk');
|
|
|
|
if (!filename) {
|
|
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
res.end('Missing file parameter');
|
|
return;
|
|
}
|
|
|
|
// Security: Prevent path traversal
|
|
const safeFilename = path.basename(filename);
|
|
const filePath = path.join(this.config.rootPath, safeFilename);
|
|
|
|
// Check file exists
|
|
let stat;
|
|
try {
|
|
stat = fs.statSync(filePath);
|
|
if (!stat.isFile()) {
|
|
res.writeHead(404);
|
|
res.end('File not found');
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
res.writeHead(404);
|
|
res.end('File not found');
|
|
return;
|
|
}
|
|
|
|
const fileSize = stat.size;
|
|
const totalChunks = Math.ceil(fileSize / this.config.chunkSize);
|
|
|
|
// Calculate chunk index
|
|
let chunkIdx;
|
|
if (chunkParam !== null) {
|
|
chunkIdx = parseInt(chunkParam, 10);
|
|
if (isNaN(chunkIdx) || chunkIdx < 0 || chunkIdx >= totalChunks) {
|
|
res.writeHead(416, {
|
|
'Content-Type': 'text/plain',
|
|
'Content-Range': `chunks 0-${totalChunks-1}/${totalChunks}`
|
|
});
|
|
res.end('Range Not Satisfiable');
|
|
return;
|
|
}
|
|
} else {
|
|
chunkIdx = 0;
|
|
}
|
|
|
|
// Read chunk data
|
|
const offset = chunkIdx * this.config.chunkSize;
|
|
const length = Math.min(this.config.chunkSize, fileSize - offset);
|
|
|
|
const buffer = Buffer.alloc(length);
|
|
const fd = fs.openSync(filePath, 'r');
|
|
try {
|
|
fs.readSync(fd, buffer, 0, length, offset);
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
|
|
// Calculate CRC32
|
|
const crc32 = ChunkServer.crc32(buffer);
|
|
|
|
// Build response: [chunk_idx: 2B][payload: N B][crc32: 4B]
|
|
const responseBuffer = Buffer.alloc(2 + length + 4);
|
|
responseBuffer.writeUInt16LE(chunkIdx, 0);
|
|
buffer.copy(responseBuffer, 2);
|
|
responseBuffer.writeUInt32LE(crc32 >>> 0, 2 + length); // Ensure unsigned
|
|
|
|
// Set headers
|
|
const headers = {
|
|
'Content-Type': 'application/octet-stream',
|
|
'Content-Length': responseBuffer.length,
|
|
'X-Chunk-Index': chunkIdx,
|
|
'X-Chunk-Size': length,
|
|
'X-Total-Chunks': totalChunks,
|
|
'X-File-Size': fileSize
|
|
};
|
|
|
|
// Response status
|
|
if (chunkIdx > 0) {
|
|
res.writeHead(206, headers); // Partial Content for resumed chunks
|
|
} else {
|
|
res.writeHead(200, headers); // OK for first chunk
|
|
}
|
|
|
|
res.end(responseBuffer);
|
|
|
|
// Emit event
|
|
this.emit('chunk-sent', {
|
|
file: filename,
|
|
chunkIdx: chunkIdx,
|
|
chunkSize: length,
|
|
totalChunks: totalChunks,
|
|
fileSize: fileSize
|
|
});
|
|
}
|
|
}
|
|
|
|
// Export
|
|
module.exports = ChunkServer;
|
|
|
|
// CLI usage
|
|
if (require.main === module) {
|
|
const port = process.argv[2] || 8080;
|
|
const rootPath = process.argv[3] || '.';
|
|
|
|
const server = new ChunkServer({ port: parseInt(port), rootPath });
|
|
|
|
server.on('chunk-sent', (info) => {
|
|
console.log(`Sent chunk ${info.chunkIdx}/${info.totalChunks} for ${info.file} (${info.chunkSize}B)`);
|
|
});
|
|
|
|
server.start().then(() => {
|
|
console.log(`Server started on port ${port}`);
|
|
console.log(`Root path: ${rootPath}`);
|
|
}).catch(err => {
|
|
console.error('Failed to start server:', err);
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on('SIGINT', async () => {
|
|
await server.stop();
|
|
process.exit(0);
|
|
});
|
|
}
|