diff --git a/index.js b/index.js index 3c4f934..eda1bca 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,15 @@ const PORT = process.env.PORT || 3001; app.use(express.json()); 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 const UDP_PORT = 4210; const DISCOVERY_MESSAGE = 'CLUSTER_DISCOVERY'; @@ -409,7 +418,7 @@ 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) => { +app.post('/api/node/update', async (req, res) => { try { 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 - 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' + // Check if we have a file in the request + if (!req.files || !req.files.file) { + console.log('File upload request received but no file found:', { + hasFiles: !!req.files, + fileKeys: req.files ? Object.keys(req.files) : [], + 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({ 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}`); + 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 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 - const updateResult = await nodeClient.updateFirmware(fileData.file.data, fileData.file.filename); + // Send the firmware data to the node + console.log(`Starting firmware upload to SPORE device ${nodeIp}...`); - res.json({ - success: true, - message: 'Firmware uploaded successfully', - nodeIp: nodeIp, - fileSize: fileData.file.data.length, - filename: fileData.file.filename, - result: updateResult - }); + try { + const updateResult = await nodeClient.updateFirmware(uploadedFile.data, uploadedFile.name); + console.log(`Firmware upload to SPORE device ${nodeIp} completed successfully:`, updateResult); + + res.json({ + success: true, + 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) { console.error('Error uploading firmware:', error); @@ -501,54 +520,7 @@ app.get('/api/health', (req, res) => { 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 const server = app.listen(PORT, () => { diff --git a/package-lock.json b/package-lock.json index 939ce63..91ddd20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^5.1.0" + "express": "^5.1.0", + "express-fileupload": "^1.4.3" } }, "node_modules/accepts": { @@ -45,6 +46,17 @@ "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": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -264,6 +276,18 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -774,6 +798,14 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index a08de7b..ea4a4e6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "author": "", "license": "ISC", "dependencies": { - "express": "^5.1.0" + "express": "^5.1.0", + "express-fileupload": "^1.4.3" } } diff --git a/src/client/index.js b/src/client/index.js index acbb41a..7b0a339 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -199,15 +199,32 @@ class SporeApiClient { /** * Update firmware on the device * @param {Buffer|Uint8Array} firmwareData - Firmware binary data + * @param {string} filename - Name of the firmware file * @returns {Promise} Update response */ async updateFirmware(firmwareData, filename) { - // Send the raw firmware data directly to the SPORE device - // The SPORE device expects the file data, not re-encoded multipart + // Create multipart form data manually for Node.js compatibility + 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', { - body: firmwareData, + body: finalBody, headers: { - 'Content-Type': 'application/octet-stream' + 'Content-Type': `multipart/form-data; boundary=${boundary}` } }); }