This commit is contained in:
2025-08-26 12:20:17 +02:00
parent be3cd771fc
commit ff43eddd27
4 changed files with 100 additions and 78 deletions

102
index.js
View File

@@ -11,6 +11,15 @@ const PORT = process.env.PORT || 3001;
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// File upload middleware
const fileUpload = require('express-fileupload');
app.use(fileUpload({
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit
abortOnLimit: true,
responseOnLimit: 'File size limit has been reached',
debug: false
}));
// UDP discovery configuration // UDP discovery configuration
const UDP_PORT = 4210; const UDP_PORT = 4210;
const DISCOVERY_MESSAGE = 'CLUSTER_DISCOVERY'; const DISCOVERY_MESSAGE = 'CLUSTER_DISCOVERY';
@@ -409,7 +418,7 @@ app.get('/api/node/status/:ip', async (req, res) => {
}); });
// File upload endpoint for firmware updates // File upload endpoint for firmware updates
app.post('/api/node/update', express.raw({ type: 'multipart/form-data', limit: '50mb' }), async (req, res) => { app.post('/api/node/update', async (req, res) => {
try { try {
const nodeIp = req.query.ip || req.headers['x-node-ip']; const nodeIp = req.query.ip || req.headers['x-node-ip'];
@@ -420,41 +429,51 @@ app.post('/api/node/update', express.raw({ type: 'multipart/form-data', limit: '
}); });
} }
// Parse multipart form data manually // Check if we have a file in the request
const boundary = req.headers['content-type']?.split('boundary=')[1]; if (!req.files || !req.files.file) {
if (!boundary) { console.log('File upload request received but no file found:', {
return res.status(400).json({ hasFiles: !!req.files,
error: 'Invalid content type', fileKeys: req.files ? Object.keys(req.files) : [],
message: 'Expected multipart/form-data with boundary' contentType: req.headers['content-type']
}); });
}
// Parse the multipart data to extract the file
const fileData = parseMultipartData(req.body, boundary);
if (!fileData.file) {
return res.status(400).json({ return res.status(400).json({
error: 'No file data received', error: 'No file data received',
message: 'Please select a firmware file to upload' message: 'Please select a firmware file to upload'
}); });
} }
console.log(`Uploading firmware to node ${nodeIp}, file size: ${fileData.file.data.length} bytes, filename: ${fileData.file.filename}`); const uploadedFile = req.files.file;
console.log(`File upload received:`, {
nodeIp: nodeIp,
filename: uploadedFile.name,
fileSize: uploadedFile.data.length,
mimetype: uploadedFile.mimetype,
encoding: uploadedFile.encoding
});
// Create a temporary client for the specific node // Create a temporary client for the specific node
const nodeClient = new SporeApiClient(`http://${nodeIp}`); const nodeClient = new SporeApiClient(`http://${nodeIp}`);
console.log(`Created SPORE client for node ${nodeIp}`);
// Send the firmware data to the node using multipart form data // Send the firmware data to the node
const updateResult = await nodeClient.updateFirmware(fileData.file.data, fileData.file.filename); console.log(`Starting firmware upload to SPORE device ${nodeIp}...`);
try {
const updateResult = await nodeClient.updateFirmware(uploadedFile.data, uploadedFile.name);
console.log(`Firmware upload to SPORE device ${nodeIp} completed successfully:`, updateResult);
res.json({ res.json({
success: true, success: true,
message: 'Firmware uploaded successfully', message: 'Firmware uploaded successfully',
nodeIp: nodeIp, nodeIp: nodeIp,
fileSize: fileData.file.data.length, fileSize: uploadedFile.data.length,
filename: fileData.file.filename, filename: uploadedFile.name,
result: updateResult result: updateResult
}); });
} catch (uploadError) {
console.error(`Firmware upload to SPORE device ${nodeIp} failed:`, uploadError);
throw new Error(`SPORE device upload failed: ${uploadError.message}`);
}
} catch (error) { } catch (error) {
console.error('Error uploading firmware:', error); console.error('Error uploading firmware:', error);
@@ -501,54 +520,7 @@ app.get('/api/health', (req, res) => {
res.status(statusCode).json(health); res.status(statusCode).json(health);
}); });
// Helper function to parse multipart form data
function parseMultipartData(buffer, boundary) {
const data = {};
const boundaryBuffer = Buffer.from(`--${boundary}`);
const endBoundaryBuffer = Buffer.from(`--${boundary}--`);
let start = 0;
let end = 0;
while (true) {
// Find the start of the next part
start = buffer.indexOf(boundaryBuffer, end);
if (start === -1) break;
// Find the end of this part
end = buffer.indexOf(boundaryBuffer, start + boundaryBuffer.length);
if (end === -1) {
end = buffer.indexOf(endBoundaryBuffer, start + boundaryBuffer.length);
if (end === -1) break;
}
// Extract the part content
const partBuffer = buffer.slice(start + boundaryBuffer.length + 2, end);
// Parse headers and content
const headerEnd = partBuffer.indexOf('\r\n\r\n');
if (headerEnd === -1) continue;
const headers = partBuffer.slice(0, headerEnd).toString();
const content = partBuffer.slice(headerEnd + 4);
// Parse the Content-Disposition header to get field name and filename
const nameMatch = headers.match(/name="([^"]+)"/);
const filenameMatch = headers.match(/filename="([^"]+)"/);
if (nameMatch) {
const fieldName = nameMatch[1];
const filename = filenameMatch ? filenameMatch[1] : null;
data[fieldName] = {
data: content,
filename: filename
};
}
}
return data;
}
// Start the server // Start the server
const server = app.listen(PORT, () => { const server = app.listen(PORT, () => {

34
package-lock.json generated
View File

@@ -9,7 +9,8 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^5.1.0" "express": "^5.1.0",
"express-fileupload": "^1.4.3"
} }
}, },
"node_modules/accepts": { "node_modules/accepts": {
@@ -45,6 +46,17 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -264,6 +276,18 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-fileupload": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.5.2.tgz",
"integrity": "sha512-wxUJn2vTHvj/kZCVmc5/bJO15C7aSMyHeuXYY3geKpeKibaAoQGcEv5+sM6nHS2T7VF+QHS4hTWPiY2mKofEdg==",
"license": "MIT",
"dependencies": {
"busboy": "^1.6.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
@@ -774,6 +798,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@@ -17,6 +17,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^5.1.0" "express": "^5.1.0",
"express-fileupload": "^1.4.3"
} }
} }

View File

@@ -199,15 +199,32 @@ class SporeApiClient {
/** /**
* Update firmware on the device * Update firmware on the device
* @param {Buffer|Uint8Array} firmwareData - Firmware binary data * @param {Buffer|Uint8Array} firmwareData - Firmware binary data
* @param {string} filename - Name of the firmware file
* @returns {Promise<Object>} Update response * @returns {Promise<Object>} Update response
*/ */
async updateFirmware(firmwareData, filename) { async updateFirmware(firmwareData, filename) {
// Send the raw firmware data directly to the SPORE device // Create multipart form data manually for Node.js compatibility
// The SPORE device expects the file data, not re-encoded multipart const boundary = '----WebKitFormBoundary' + Math.random().toString(16).substr(2, 8);
let body = '';
// Add the firmware file part
body += `--${boundary}\r\n`;
body += `Content-Disposition: form-data; name="firmware"; filename="${filename}"\r\n`;
body += 'Content-Type: application/octet-stream\r\n\r\n';
// Convert the body to Buffer and append the firmware data
const headerBuffer = Buffer.from(body, 'utf8');
const endBuffer = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
// Combine all parts
const finalBody = Buffer.concat([headerBuffer, firmwareData, endBuffer]);
// Send the multipart form data to the SPORE device
return this.request('POST', '/api/node/update', { return this.request('POST', '/api/node/update', {
body: firmwareData, body: finalBody,
headers: { headers: {
'Content-Type': 'application/octet-stream' 'Content-Type': `multipart/form-data; boundary=${boundary}`
} }
}); });
} }