diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 937daa2..1340909 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -21,4 +21,4 @@ updates: schedule: interval: "daily" assignees: - - "mpfeil" \ No newline at end of file + - "mpfeil" diff --git a/.github/workflows/registry-pr-purge.yaml b/.github/workflows/registry-pr-purge.yaml index 77ba87d..8c82712 100644 --- a/.github/workflows/registry-pr-purge.yaml +++ b/.github/workflows/registry-pr-purge.yaml @@ -16,4 +16,4 @@ jobs: token: ${{ secrets.GHCR_TOKEN}} organization: ${{ github.repository_owner}} container: ${{ github.event.repository.name }} - tag-regex: pr-${{github.event.pull_request.number}}$ \ No newline at end of file + tag-regex: pr-${{github.event.pull_request.number}}$ diff --git a/.github/workflows/registry-purge.yaml b/.github/workflows/registry-purge.yaml index 30a7b81..ac6b728 100644 --- a/.github/workflows/registry-purge.yaml +++ b/.github/workflows/registry-purge.yaml @@ -15,4 +15,4 @@ jobs: token: ${{ secrets.GHCR_TOKEN}} organization: ${{ github.repository_owner}} container: ${{ github.event.repository.name }} - untagged: true \ No newline at end of file + untagged: true diff --git a/.gitignore b/.gitignore index bbc97b3..03d0f3a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ yarn-error.log *.bin *.hex -ctrf \ No newline at end of file +ctrf + +*.DS_Store \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..277a8ba --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": false, + "quoteProps": "preserve" +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1f9f383..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -sudo: false -language: node_js -node_js: -- '10' -env: - global: - - IDE_VERSION=1.8.5 - - SENSEBOXCORE_VERSION=1.3.0 - - ARDUINO_SAMD_VERSION=1.6.21 - - ARDUINO_AVR_VERSION=1.6.21 - - SENSEBOXCORE_URL=https://raw.githubusercontent.com/sensebox/senseBoxMCU-core/master/package_sensebox_index.json - - SENSEBOX_LIBRARY_URL=https://github.com/sensebox/senseBox_library/archive/master.zip \ -before_script: - - echo $TRAVIS_BUILD_DIR - - wget http://downloads.arduino.cc/arduino-$IDE_VERSION-linux64.tar.xz - - tar xf arduino-$IDE_VERSION-linux64.tar.xz - - mv arduino-$IDE_VERSION $TRAVIS_BUILD_DIR/src/arduino-ide - - wget -O senseBox_Library.zip $SENSEBOX_LIBRARY_URL - - unzip senseBox_Library.zip -d $TRAVIS_BUILD_DIR/src/arduino-ide/libraries - - mkdir -p $TRAVIS_BUILD_DIR/src/arduino-ide/build-cache - - export PATH=$PATH:$TRAVIS_BUILD_DIR/src/arduino-ide - - arduino --install-boards arduino:samd:$ARDUINO_SAMD_VERSION - - arduino --install-boards arduino:avr:$ARDUINO_AVR_VERSION - - arduino --pref boardsmanager.additional.urls=$SENSEBOXCORE_URL --install-boards sensebox:samd:$SENSEBOXCORE_VERSION - - mv $HOME/.arduino15/packages $TRAVIS_BUILD_DIR/src/arduino-ide/packages -script: - - npm run test diff --git a/.vscode/launch.json b/.vscode/launch.json index 67de415..551d071 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,4 +11,4 @@ "program": "${workspaceFolder}/src/index.js" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 4e60c4b..626daf5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# A container based compiler for senseBox Blockly sketches +# A container based compiler for senseBox Blockly sketches + ![Build Status](https://github.com/sensebox/sensebox-sketches/actions/workflows/registry-build-publish.yaml/badge.svg) ## Usage @@ -32,6 +33,7 @@ You can also run the container image mutliple times. See [Scaling with docker-co ### Endpoints #### `POST /compile` + - have `application/json` as `content-type` - contain a valid JSON string with keys `board` and `sketch` with non-empty values. @@ -40,22 +42,25 @@ Possible `board` values are `sensebox-mcu` for the new senseBox MCU, `sensebox` The `sketch` value should be a valid Arduino sketch. Responses have a `content-type: application/json` header and contains the following response body: + ```json { - "code":201, - "message":"Sketch successfully compiled and created!", - "data":{ - "id":"77c1df527a874bd909b56bf1e3906604" - } + "code": 201, + "message": "Sketch successfully compiled and created!", + "data": { + "id": "77c1df527a874bd909b56bf1e3906604" + } } ``` The `id` is the identifier for your compiled sketch and must be used in the `GET /download/:id` route. #### `GET /download` + Downloads a compiled sketch. Parameters: + - `id` is the returned `id` from `/compile` - `board` specifies which compiled file should be downloaded. Posibile values `sensebox-mcu` or `sensebox` - `filename` name for the sketch. Default value is `sketch` diff --git a/mocha-reporters.json b/mocha-reporters.json index 207223b..460f245 100644 --- a/mocha-reporters.json +++ b/mocha-reporters.json @@ -1,3 +1,3 @@ { - "reporterEnabled": "spec, mocha-ctrf-json-reporter" -} \ No newline at end of file + "reporterEnabled": "spec, mocha-ctrf-json-reporter" +} diff --git a/src/download.js b/src/download.js index 6432724..b6ba525 100644 --- a/src/download.js +++ b/src/download.js @@ -1,45 +1,65 @@ -const fs = require('fs'); -const { boardBinaryFileextensions } = require('./builder'); -const { HTTPError, rimraf_promise } = require('./utils'); +const fs = require("fs"); +const { boardBinaryFileextensions } = require("./builder"); +const { HTTPError, rimraf_promise } = require("./utils"); -const readFile = async function readFile ({ id, board }) { - return Promise.resolve(fs.createReadStream(`/tmp/${id}/sketch.ino.${boardBinaryFileextensions[board]}`)); -} +const readFile = async function readFile({ id, board }) { + return Promise.resolve( + fs.createReadStream( + `/tmp/${id}/sketch.ino.${boardBinaryFileextensions[board]}` + ) + ); +}; -const downloadHandler = async function downloadHandler (req, res, next) { - if (req.method !== 'GET') { - return next(new HTTPError({ code: 405, error: 'Invalid HTTP method. Only GET requests allowed on /download.' })); +const downloadHandler = async function downloadHandler(req, res, next) { + if (req.method !== "GET") { + return next( + new HTTPError({ + code: 405, + error: "Invalid HTTP method. Only GET requests allowed on /download.", + }) + ); } const { id, board } = req._url.query; if (!id || !board) { - return next(new HTTPError({ code: 422, error: 'Parameters \'id\' and \'board\' are required' })); + return next( + new HTTPError({ + code: 422, + error: "Parameters 'id' and 'board' are required", + }) + ); } // execute builder with parameters from user try { const stream = await readFile(req._url.query); - const filename = req._url.query.filename || 'sketch'; - stream.on('error', function (err) { + const filename = req._url.query.filename || "sketch"; + stream.on("error", function (err) { return next(err); }); - stream.on('end', async () => { + stream.on("end", async () => { try { - await rimraf_promise(`/tmp/${req._url.query.id}`) + await rimraf_promise(`/tmp/${req._url.query.id}`); } catch (error) { - console.log(`Error deleting compile sketch folder with ${req._url.query.id}: `, error); + console.log( + `Error deleting compile sketch folder with ${req._url.query.id}: `, + error + ); } }); - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Disposition', `attachment; filename=${filename}.${boardBinaryFileextensions[req._url.query.board]}`); + res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader( + "Content-Disposition", + `attachment; filename=${filename}.${boardBinaryFileextensions[req._url.query.board]}` + ); stream.pipe(res); } catch (err) { return next(new HTTPError({ error: err.message })); } -} +}; module.exports = { - downloadHandler -} \ No newline at end of file + downloadHandler, +}; diff --git a/src/index.js b/src/index.js index 14d011b..8e437da 100644 --- a/src/index.js +++ b/src/index.js @@ -1,39 +1,43 @@ -const connect = require('connect'); -const http = require('http'); -const urlParser = require('url').parse; -const bodyParser = require('body-parser'); +const connect = require("connect"); +const http = require("http"); +const urlParser = require("url").parse; +const bodyParser = require("body-parser"); const app = connect(); -const responseTime = require('response-time'); -const morgan = require('morgan'); +const responseTime = require("response-time"); +const morgan = require("morgan"); -const { compileHandler, payloadValidator } = require('./builder'); -const { downloadHandler } = require('./download'); -const { HTTPError } = require('./utils'); +const { compileHandler, payloadValidator } = require("./builder"); +const { downloadHandler } = require("./download"); +const { HTTPError } = require("./utils"); +const { spawnSync } = require("child_process"); const defaultHeaders = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Allow': 'GET,POST', - 'X-Backend-Server': (require('os').hostname()) + "Content-Type": "application/json", + Accept: "application/json", + Allow: "GET,POST", + "X-Backend-Server": require("os").hostname(), }; -const preflight = function preflight (req, res, next) { +const preflight = function preflight(req, res, next) { // preflight POST request https://gist.github.com/balupton/3696140 - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Request-Method', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET,POST'); - res.setHeader('Access-Control-Allow-Headers', 'content-type'); - res.setHeader('Access-Control-Expose-Headers', 'x-backend-server, x-response-time'); - if (req.method === 'OPTIONS') { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Request-Method", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST"); + res.setHeader("Access-Control-Allow-Headers", "content-type"); + res.setHeader( + "Access-Control-Expose-Headers", + "x-backend-server, x-response-time" + ); + if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } next(); -} +}; -const preRequestValidator = function preRequestValidator (req, res, next) { +const preRequestValidator = function preRequestValidator(req, res, next) { // set some headers, just in case for (const [k, v] of Object.entries(defaultHeaders)) { res.setHeader(k, v); @@ -44,43 +48,80 @@ const preRequestValidator = function preRequestValidator (req, res, next) { req._url = url; // reject everything not coming through /compile or /download - if (url.pathname !== '/compile' && url.pathname !== '/download') { + if ( + url.pathname !== "/compile" && + url.pathname !== "/download" && + url.pathname !== "/libraries" + ) { return next(new HTTPError({ code: 404, error: `Cannot serve ${req.url}` })); } // reject all non POST request - if (req.method !== 'POST' && req.method !== 'GET') { - return next(new HTTPError({ code: 405, error: 'Invalid HTTP method. Only GET or POST requests allowed.' })); + if (req.method !== "POST" && req.method !== "GET") { + return next( + new HTTPError({ + code: 405, + error: "Invalid HTTP method. Only GET or POST requests allowed.", + }) + ); } next(); }; -const errorHandler = function errorHandler (err, req, res, next) { +const errorHandler = function errorHandler(err, req, res, next) { if (res.headersSent) { return next(err); } - res.statusCode = (err.statusCode ? err.statusCode : 500); - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ - code: http.STATUS_CODES[res.statusCode], - message: err.message - })); + res.statusCode = err.statusCode ? err.statusCode : 500; + res.setHeader("Content-Type", "application/json"); + res.end( + JSON.stringify({ + code: http.STATUS_CODES[res.statusCode], + message: err.message, + }) + ); }; -const startServer = function startServer () { - app.use(morgan(':date[iso] :res[x-backend-server] :remote-addr :req[x-real-ip] :method :url :response-time[0] :status', { skip: (req, res) => process.env.NODE_ENV === 'test' })); +const startServer = function startServer() { + app.use( + morgan( + ":date[iso] :res[x-backend-server] :remote-addr :req[x-real-ip] :method :url :response-time[0] :status", + { skip: (req, res) => process.env.NODE_ENV === "test" } + ) + ); app.use(responseTime()); app.use(preflight); app.use(preRequestValidator); - app.use('/compile', bodyParser.json()); - app.use('/compile', payloadValidator); - app.use('/compile', compileHandler); - app.use('/download', downloadHandler); + app.use("/compile", bodyParser.json()); + app.use("/compile", payloadValidator); + app.use("/compile", compileHandler); + app.use("/download", downloadHandler); + app.use("/libraries", function (req, res) { + // read request parameter format (json or text) + const format = req._url.query.format; + + if (format === "json") { + const child = spawnSync("arduino-cli", [ + "lib", + "list", + "--all", + "--format", + "json", + ]); + res.setHeader("Content-Type", "application/json"); + res.end(child.stdout.toString()); + return; + } + + const child = spawnSync("arduino-cli", ["lib", "list", "--all"]); + res.setHeader("Content-Type", "text/plain"); + res.end(child.stdout.toString()); + }); app.use(errorHandler); http.createServer(app).listen(3000); - console.log('Compiler started and listening on port 3000!'); + console.log("Compiler started and listening on port 3000!"); }; startServer(); diff --git a/src/utils.js b/src/utils.js index c974f54..079de4a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,8 @@ -const util = require('util'); -const rimraf = require('rimraf'); +const util = require("util"); +const rimraf = require("rimraf"); const rimraf_promise = util.promisify(rimraf); -const HTTPError = function HTTPError ({ code = 500, error = '' }) { +const HTTPError = function HTTPError({ code = 500, error = "" }) { const err = new Error(error); err.statusCode = code; @@ -11,5 +11,5 @@ const HTTPError = function HTTPError ({ code = 500, error = '' }) { module.exports = { HTTPError, - rimraf_promise -}; \ No newline at end of file + rimraf_promise, +}; diff --git a/test/index.js b/test/index.js index f14389e..07e4f1c 100644 --- a/test/index.js +++ b/test/index.js @@ -23,7 +23,7 @@ describe("Compiler", () => { chai .request(server) .post("/compile") - .send({ sketch: 'void setup() {} void loop() {}' }) + .send({ sketch: "void setup() {} void loop() {}" }) .end((err, res) => { res.should.have.status(422); res.body.should.have @@ -51,12 +51,17 @@ describe("Compiler", () => { chai .request(server) .post("/compile") - .send({ board: "esp8266", sketch: 'void setup() {} void loop() {}' }) + .send({ + board: "esp8266", + sketch: "void setup() {} void loop() {}", + }) .end((err, res) => { res.should.have.status(422); res.body.should.have .property("message") - .eql("Invalid board parameter. Valid values are: sensebox-mcu,sensebox,sensebox-esp32s2"); + .eql( + "Invalid board parameter. Valid values are: sensebox-mcu,sensebox,sensebox-esp32s2" + ); done(); }); }); @@ -71,7 +76,9 @@ describe("Compiler", () => { res.should.have.status(415); res.body.should.have .property("message") - .eql("Invalid Content-Type. Only application/json Content-Type allowed."); + .eql( + "Invalid Content-Type. Only application/json Content-Type allowed." + ); done(); }); }); @@ -80,12 +87,17 @@ describe("Compiler", () => { chai .request(server) .get("/compile") - .send({ board: "sensebox-mcu", sketch: 'void setup() {} void loop() {}' }) + .send({ + board: "sensebox-mcu", + sketch: "void setup() {} void loop() {}", + }) .end((err, res) => { res.should.have.status(405); res.body.should.have .property("message") - .eql("Invalid HTTP method. Only POST requests allowed on /compile."); + .eql( + "Invalid HTTP method. Only POST requests allowed on /compile." + ); done(); }); }); diff --git a/test/mcu.js b/test/mcu.js index 30c69aa..59a8b33 100644 --- a/test/mcu.js +++ b/test/mcu.js @@ -4,7 +4,6 @@ const server = require("../src/index"); const should = chai.should(); const fs = require("fs"); - chai.use(chaiHttp); describe("Compiler - MCU", () => { @@ -62,7 +61,9 @@ describe("Compiler - MCU", () => { .query({ board: "sensebox-mcu", id: downloadId_mcu }) .end((err, res) => { res.should.have.status(200); - res.header.should.have.property("content-disposition").eql("attachment; filename=sketch.bin"); + res.header.should.have + .property("content-disposition") + .eql("attachment; filename=sketch.bin"); done(); }); }); diff --git a/test/mcu_s2.js b/test/mcu_s2.js index 4a156a3..649592a 100644 --- a/test/mcu_s2.js +++ b/test/mcu_s2.js @@ -40,7 +40,10 @@ describe("Compiler - MCU S2 (ESP32S2)", () => { }); it("should compile the tof-distance-display sketch for senseBox MCU-S2 ESP32S2", (done) => { - const sketch = fs.readFileSync("test/sketches/mcu_s2/tof-distance-display.ino", "utf8"); + const sketch = fs.readFileSync( + "test/sketches/mcu_s2/tof-distance-display.ino", + "utf8" + ); chai .request(server) .post("/compile") @@ -60,7 +63,9 @@ describe("Compiler - MCU S2 (ESP32S2)", () => { .query({ board: "sensebox-esp32s2", id: downloadId_esp32s2 }) .end((err, res) => { res.should.have.status(200); - res.header.should.have.property("content-disposition").eql("attachment; filename=sketch.bin"); + res.header.should.have + .property("content-disposition") + .eql("attachment; filename=sketch.bin"); done(); }); }); diff --git a/test/uno.js b/test/uno.js index 4ef633c..aa1b832 100644 --- a/test/uno.js +++ b/test/uno.js @@ -20,8 +20,8 @@ describe("Compiler - UNO", () => { res.should.have.status(200); res.body.should.be.a("object"); res.body.should.have - .property("message") - .eql("Sketch successfully compiled and created!"); + .property("message") + .eql("Sketch successfully compiled and created!"); res.body.should.have.property("data"); res.body.data.should.be.a("object"); res.body.data.should.have.property("id"); @@ -41,8 +41,8 @@ describe("Compiler - UNO", () => { res.should.have.status(200); res.body.should.be.a("object"); res.body.should.have - .property("message") - .eql("Sketch successfully compiled and created!"); + .property("message") + .eql("Sketch successfully compiled and created!"); res.body.should.have.property("data"); res.body.data.should.be.a("object"); res.body.data.should.have.property("id"); @@ -58,7 +58,9 @@ describe("Compiler - UNO", () => { .query({ board: "sensebox", id: downloadId_uno }) .end((err, res) => { res.should.have.status(200); - res.header.should.have.property("content-disposition").eql("attachment; filename=sketch.hex"); + res.header.should.have + .property("content-disposition") + .eql("attachment; filename=sketch.hex"); done(); }); });