Skip to content

Commit

Permalink
Merge pull request #125 from sensebox/feat/libraries-api
Browse files Browse the repository at this point in the history
Feat/libraries api
  • Loading branch information
felixerdy authored Dec 20, 2024
2 parents 915a44c + f7048cc commit 70ee113
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 119 deletions.
2 changes: 1 addition & 1 deletion .github/dependabot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ updates:
schedule:
interval: "daily"
assignees:
- "mpfeil"
- "mpfeil"
2 changes: 1 addition & 1 deletion .github/workflows/registry-pr-purge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}$
tag-regex: pr-${{github.event.pull_request.number}}$
2 changes: 1 addition & 1 deletion .github/workflows/registry-purge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
token: ${{ secrets.GHCR_TOKEN}}
organization: ${{ github.repository_owner}}
container: ${{ github.event.repository.name }}
untagged: true
untagged: true
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ yarn-error.log
*.bin
*.hex

ctrf
ctrf

*.DS_Store
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"quoteProps": "preserve"
}
27 changes: 0 additions & 27 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
"program": "${workspaceFolder}/src/index.js"
}
]
}
}
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions mocha-reporters.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"reporterEnabled": "spec, mocha-ctrf-json-reporter"
}
"reporterEnabled": "spec, mocha-ctrf-json-reporter"
}
60 changes: 40 additions & 20 deletions src/download.js
Original file line number Diff line number Diff line change
@@ -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
}
downloadHandler,
};
119 changes: 80 additions & 39 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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();
Expand Down
10 changes: 5 additions & 5 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -11,5 +11,5 @@ const HTTPError = function HTTPError ({ code = 500, error = '' }) {

module.exports = {
HTTPError,
rimraf_promise
};
rimraf_promise,
};
Loading

0 comments on commit 70ee113

Please sign in to comment.