feat: half baked fw upload

This commit is contained in:
2025-08-25 09:41:56 +02:00
parent 2938b5f5fe
commit 9514bf7ac5
4 changed files with 251 additions and 0 deletions

107
index.js
View File

@@ -1,5 +1,6 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const SporeApiClient = require('./src/client');
const app = express();
@@ -77,6 +78,112 @@ app.get('/api/node/status/:ip', async (req, res) => {
}
});
// File upload endpoint for firmware updates
app.post('/api/node/update', express.raw({ type: 'multipart/form-data', limit: '50mb' }), async (req, res) => {
try {
const nodeIp = req.query.ip || req.headers['x-node-ip'];
if (!nodeIp) {
return res.status(400).json({
error: 'Node IP address is required',
message: 'Please provide the target node IP address'
});
}
// Parse multipart form data manually
const boundary = req.headers['content-type']?.split('boundary=')[1];
if (!boundary) {
return res.status(400).json({
error: 'Invalid content type',
message: 'Expected multipart/form-data with boundary'
});
}
// Parse the multipart data to extract the file
const fileData = parseMultipartData(req.body, boundary);
if (!fileData.file) {
return res.status(400).json({
error: 'No file data received',
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}`);
// Create a temporary client for the specific node
const nodeClient = new SporeApiClient(`http://${nodeIp}`);
// Send the firmware data to the node using multipart form data
const updateResult = await nodeClient.updateFirmware(fileData.file.data, fileData.file.filename);
res.json({
success: true,
message: 'Firmware uploaded successfully',
nodeIp: nodeIp,
fileSize: fileData.file.data.length,
filename: fileData.file.filename,
result: updateResult
});
} catch (error) {
console.error('Error uploading firmware:', error);
res.status(500).json({
error: 'Failed to upload firmware',
message: error.message
});
}
});
// 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
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);