fix: OTA
This commit is contained in:
116
index.js
116
index.js
@@ -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}...`);
|
||||||
|
|
||||||
res.json({
|
try {
|
||||||
success: true,
|
const updateResult = await nodeClient.updateFirmware(uploadedFile.data, uploadedFile.name);
|
||||||
message: 'Firmware uploaded successfully',
|
console.log(`Firmware upload to SPORE device ${nodeIp} completed successfully:`, updateResult);
|
||||||
nodeIp: nodeIp,
|
|
||||||
fileSize: fileData.file.data.length,
|
res.json({
|
||||||
filename: fileData.file.filename,
|
success: true,
|
||||||
result: updateResult
|
message: 'Firmware uploaded successfully',
|
||||||
});
|
nodeIp: nodeIp,
|
||||||
|
fileSize: uploadedFile.data.length,
|
||||||
|
filename: uploadedFile.name,
|
||||||
|
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
34
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user