Compare commits

...

12 Commits

Author SHA1 Message Date
3c9d62b840 Merge pull request #1 from KevinMidboe/feat/drone-ci
Feat Drone ci
2022-11-02 22:32:11 +01:00
4dae70a45f Only publish package when merging master 2022-11-02 22:24:37 +01:00
25ae3a2b65 Build files outside and move in & install only !dev dependencies 2022-11-02 22:21:00 +01:00
ef3a394657 Run node directly in Dockerfile CMD
Without yarn in cmd we don't need to import package.json
2022-11-02 22:05:48 +01:00
d713786685 Use compiled files from ci instead of Dockerfile 2022-11-02 22:01:03 +01:00
4311a4c841 Test always push new docker image 2022-11-02 21:55:31 +01:00
48fde2c0b6 Also run drone job on PR 2022-11-02 21:53:56 +01:00
3ed57b75f4 Drone lint, build and publish definitions 2022-11-02 21:48:58 +01:00
f8708909b6 Linted project with eslint 2022-11-02 21:48:40 +01:00
2d42605c59 Dockerfile updated for Typescript 2022-11-02 21:34:45 +01:00
35760c2da2 Extended with examples & api query options 2022-11-02 21:31:17 +01:00
c8f87ade58 Refactored to typescript & more consistent code
Updated how we interface with figlet and unified error handling &
response.
2022-11-02 21:14:12 +01:00
22 changed files with 1724 additions and 130 deletions

77
.drone.yml Normal file
View File

@@ -0,0 +1,77 @@
---
kind: pipeline
type: docker
name: figlet-http build
platform:
os: linux
arch: amd64
volumes:
- name: cache
host:
path: /tmp/cache
steps:
- name: Load cached node_modules packages
image: sinlead/drone-cache:1.0.0
settings:
action: load
key: yarn.lock
mount: node_modules
prefix: yarn-modules-figlet-http
volumes:
- name: cache
path: /cache
- name: Install
image: node:18.2.0
commands:
- node -v
- yarn --version
- yarn
- name: Cache node_modules packages
image: sinlead/drone-cache:1.0.0
settings:
action: save
key: yarn.lock
mount: node_modules
prefix: yarn-modules-figlet-http
volumes:
- name: cache
path: /cache
- name: Build ts
image: node:18.2.0
commands:
- yarn build
- name: Lint project using eslint
image: node:18.2.0
commands:
- yarn lint
failure: ignore
- name: Build and publish docker image
image: plugins/docker
settings:
registry: ghcr.io
repo: ghcr.io/kevinmidboe/figlet-http
dockerfile: Dockerfile
username:
from_secret: GITHUB_USERNAME
password:
from_secret: GITHUB_PASSWORD
tags: latest
when:
event:
- push
branch:
- master
trigger:
event:
include:
- push
- pull_request

41
.eslintrc.json Normal file
View File

@@ -0,0 +1,41 @@
{
"root": true,
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": [
"eslint-config-airbnb-base",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["@typescript-eslint"],
"rules": {
"lines-between-class-members": [
"error",
"always",
{ "exceptAfterSingleLine": true }
],
"max-classes-per-file": 1,
"no-empty": [
2,
{
"allowEmptyCatch": true
}
],
"no-promise-executor-return": 1,
"import/extensions": "off",
"max-classes-per-file": "off",
"no-shadow": "off",
"no-underscore-dangle": "off",
"@typescript-eslint/no-var-requires": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".ts"]
}
}
}
}

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules/
lib/
.DS_Store

View File

@@ -1,16 +1,14 @@
FROM node:18
LABEL org.opencontainers.image.source https://github.com/KevinMidboe/figlet-http
RUN mkdir -p /opt/figlet-http/src
RUN mkdir -p /opt/figlet-http/lib
WORKDIR /opt/figlet-http
COPY src/ src
COPY package.json .
COPY yarn.lock .
COPY lib/ lib
RUN yarn
RUN yarn install --production
EXPOSE 3000
CMD ["yarn", "start"]
CMD ["node", "lib/app.js"]

View File

@@ -20,10 +20,23 @@ yarn start
## Examples
Generate Larry 3D ascii response from text:
Generate Larry 3D figlet response from text:
```bash
curl localhost:3000/ascii\?text\=ragnhild
curl localhost:3000/figlet\?text\=ragnhild
```
Outputs:
```
__ ___ __
/\ \ __ /\_ \ /\ \
_ __ __ __ ___\ \ \___ /\_\\//\ \ \_\ \
/\`'__\/'__`\ /'_ `\ /' _ `\ \ _ `\/\ \ \ \ \ /'_` \
\ \ \//\ \L\.\_/\ \L\ \/\ \/\ \ \ \ \ \ \ \ \_\ \_/\ \L\ \
\ \_\\ \__/.\_\ \____ \ \_\ \_\ \_\ \_\ \_\/\____\ \___,_\
\/_/ \/__/\/_/\/___L\ \/_/\/_/\/_/\/_/\/_/\/____/\/__,_ /
/\____/
\_/__/
```
Generate motd executable with text and save to file:
@@ -32,35 +45,60 @@ Generate motd executable with text and save to file:
curl localhost:3000/motd\?text\=ragnhild > 20-hostname
```
### Options
Api endpoints `/text` & `/motd` have the following query options:
| query param | type | description | required |
|-------------|--------|--------------------------------------|----------|
| text | string | text to generate | yes |
| font | string | select font, get list from /fonts | no |
| width | number | max character width before linebreak | no |
## Docker install
Run as a docker container using:
Run as a docker container from github container registry:
```bash
docker run -d \
sudo docker run -d \
--name figlet-http \
-p 3000:3000 \
ghcr.io/kevinmidboe/figlet-http
```
Run as docker locally:
```bash
yarn build; \
sudo docker build -t figlet-http .; \
sudo docker run -d \
--name figlet-http \
-p 3000:3000 \
figlet-http
```
## Systemd service
Example systemd config for running http server.
Clone repo to: `/opt/figlet-http`
`/etc/systemd/system/figlet-http.service`
```
[Unit]
Description=Http server for running homename-larry
Description=Http server for running figlet-http
[Service]
WorkingDirectory=/opt/hostname-larry
ExecStart=/usr/bin/node index.js
WorkingDirectory=/opt/figlet-http
ExecStart=/usr/bin/yarn start
Restart=always
# Restart service after 10 seconds if node service crashes
RestartSec=10
# Output to syslog
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=hostname-larry
SyslogIdentifier=figlet-http
[Install]
WantedBy=multi-user.target

View File

@@ -1,9 +1,21 @@
{
"version": "1.0.0",
"scripts": {
"start": "node src/app.js"
"build": "tsc",
"start": "node lib/app.js",
"lint": "eslint src"
},
"dependencies": {
"express": "^4.18.2",
"figlet": "^1.5.2"
},
"devDependencies": {
"@types/node": "^18.11.2",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"eslint": "^8.25.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"typescript": "^4.8.4"
}
}

View File

@@ -1,15 +0,0 @@
const express = require("express");
const asciiFontController = require("./controllers/asciiController.js");
const modtFontController = require("./controllers/motdController.js");
const requiredQueryMiddleware = require("./requiredQueryMiddleware");
const app = express();
const port = 3000;
app.get("/ascii", requiredQueryMiddleware, asciiFontController);
app.get("/motd", requiredQueryMiddleware, modtFontController);
app.listen(port, () => {
console.log(`Font generation application listening on port ${port}.`);
});

17
src/app.ts Normal file
View File

@@ -0,0 +1,17 @@
import express from 'express';
import figletontController from './controllers/figletController';
import modtFontController from './controllers/motdController';
import fontsController from './controllers/fontsController';
import requiredQueryMiddleware from './requiredQueryMiddleware';
const app = express();
const port = 3000;
app.get('/figlet', requiredQueryMiddleware, figletontController);
app.get('/motd', requiredQueryMiddleware, modtFontController);
app.get('/fonts', fontsController);
app.listen(port, () => {
console.log(`Figlet generation application listening on port ${port}.`); // eslint-disable-line no-console
});

View File

@@ -1,33 +0,0 @@
const figlet = require("figlet");
const defaultFontOptions = {
font: "Larry 3D",
horizontalLayout: "default",
verticalLayout: "default",
width: 80,
whitespaceBreak: true,
};
function generateAscii(message, options) {
const _options = {
...defaultFontOptions,
...options,
};
return new Promise((resolve, reject) => {
figlet.text(message, _options, (err, data) => {
if (err) {
console.error("error from figlet:", error);
return reject(err);
}
if (_options.font === "Larry 3D") {
data = data.replaceAll("L", "_");
}
resolve(data);
});
});
}
module.exports = generateAscii;

View File

@@ -1,16 +0,0 @@
const generateAscii = require("../ascii.js");
const asciiFontController = (req, res) => {
const { text } = req.query;
return generateAscii(text)
.then((ascii) => res.send(ascii))
.catch((error) =>
res.status(error?.statusCode || 500).send({
success: false,
message: error?.message || "Unexpected error from font generation",
})
);
};
module.exports = asciiFontController;

View File

@@ -0,0 +1,20 @@
import FigletFonts from '../figletFonts';
import IFigletOptions from '../interfaces/IFigletOptions';
const figletFonts = new FigletFonts();
const figletTextController = async (req, res) => {
const { text, font, width } = req.query;
let options: IFigletOptions;
if (font) options = { ...options, font };
if (width) options = { ...options, width };
figletFonts.generateText(text, options)
.then((data) => res.send(data))
.catch((error) => {
const msg = error?.message || 'Unexpected error from generating figlet';
res.status(error?.statusCode || 500).send(msg);
});
};
export default figletTextController;

View File

@@ -0,0 +1,12 @@
import FigletFonts from '../figletFonts';
const figletFonts = new FigletFonts();
const fontsController = (req, res) => figletFonts.fonts
.then((fonts) => res.send(fonts))
.catch((error) => {
const msg = error?.message || 'Unexpected error from fetching fonts';
res.status(error?.statusCode || 500).send(msg);
});
export default fontsController;

View File

@@ -1,17 +0,0 @@
const generateAscii = require("../ascii.js");
const modtFontController = (req, res) => {
const { text } = req.query;
return generateAscii(message)
.then((ascii) => generateCatOutput(ascii))
.then((motd) => res.send(motd))
.catch((error) => {
return res.status(500).send({
success: false,
message: "Unexpected error from font generation",
});
});
};
module.exports = modtFontController;

View File

@@ -0,0 +1,29 @@
import FigletFonts from '../figletFonts';
import IFigletOptions from '../interfaces/IFigletOptions';
const figletFonts = new FigletFonts();
function generateCatOutput(data: string) {
return Promise.resolve(`#!/bin/sh
cat << 'EOF'
${data}
EOF
`);
}
const modtFontController = (req, res) => {
const { text, font, width } = req.query;
let options: IFigletOptions;
if (font) options = { ...options, font };
if (width) options = { ...options, width };
figletFonts.generateText(text, options)
.then((data) => generateCatOutput(data))
.then((motd) => res.send(motd))
.catch((error) => {
const msg = error?.message || 'Unexpected error from generating motd figlet';
res.status(error?.statusCode || 500).send(msg);
});
};
export default modtFontController;

View File

@@ -1,13 +0,0 @@
class MissingTextError extends Error {
constructor(error = null) {
const message = `Missing query parameter 'text'`;
super(message);
this.error = error;
this.statusCode = 400;
}
}
module.exports = {
MissingTextError,
};

47
src/errors.ts Normal file
View File

@@ -0,0 +1,47 @@
class MissingTextError extends Error {
error: string;
statusCode: number;
constructor(error = null) {
const message = 'Missing query parameter \'text\'.';
super(message);
this.error = error;
this.statusCode = 400;
}
}
class FontNotFoundError extends Error {
error: string;
statusCode: number;
constructor(font, error = null) {
const message = `Font '${font}' not found. Check /fonts.`;
super(message);
this.error = error;
this.statusCode = 404;
}
}
class UnexpectedFigletError extends Error {
error: string;
statusCode: number;
constructor(error) {
const message = 'Unexpected error from figlet!';
super(message);
this.error = error;
this.statusCode = 500;
console.log(message); // eslint-disable-line no-console
console.log(error); // eslint-disable-line no-console
}
}
export {
MissingTextError,
FontNotFoundError,
UnexpectedFigletError,
};

58
src/figletFonts.ts Normal file
View File

@@ -0,0 +1,58 @@
import figlet from 'figlet';
import IFigletOptions from './interfaces/IFigletOptions';
import { UnexpectedFigletError } from './errors';
class FigletFonts {
cachedFonts: Array<string>;
defaultFontOptions: IFigletOptions;
constructor() {
this.cachedFonts = [];
this.defaultFontOptions = {
font: 'Larry 3D',
horizontalLayout: 'default',
verticalLayout: 'default',
width: 80,
whitespaceBreak: true,
};
}
static sanitizeLarry(text: string) {
return text.replaceAll('L', '_');
}
generateText(text: string, options: IFigletOptions): Promise<string> {
const _options: IFigletOptions = {
...this.defaultFontOptions,
...options,
};
return new Promise((resolve, reject) => {
figlet.text(text, _options, (err, data) => {
if (err) {
return reject(new UnexpectedFigletError(err));
}
return resolve(data);
});
});
}
get fonts(): Promise<Array<string>> {
if (this.cachedFonts.length > 0) {
return Promise.resolve(this.cachedFonts);
}
return new Promise((resolve, reject) => {
figlet.fonts((err, fonts) => {
if (err) {
return reject(new UnexpectedFigletError(err));
}
return resolve(fonts);
});
});
}
}
export default FigletFonts;

View File

@@ -0,0 +1,9 @@
interface IFigletOptions {
font: string;
width: number;
horizontalLayout?: string;
verticalLayout?: string;
whitespaceBreak?: boolean;
}
export default IFigletOptions;

View File

@@ -1,14 +0,0 @@
const { MissingTextError } = require("./errors.js");
const requiredQueryMiddleware = (req, res, next) => {
const { text } = req.query;
if (!text || text?.length === 0) {
const error = new MissingTextError();
return res.status(error?.statusCode || 500).send(error?.message);
}
next();
};
module.exports = requiredQueryMiddleware;

View File

@@ -0,0 +1,30 @@
import FigletFonts from './figletFonts';
import { MissingTextError, FontNotFoundError } from './errors';
const figletFonts = new FigletFonts();
async function doesFontExist(font: string) {
const fontLowercased = font.toLowerCase();
const fonts: Array<string> = await figletFonts.fonts;
return fonts.findIndex((_font) => _font.toLowerCase() === fontLowercased) === -1;
}
const requiredQueryMiddleware = async (req, res, next) => {
const { text, font = '' } = req.query;
let error = null;
if (!text || text?.length === 0) {
error = new MissingTextError();
}
if (font && (await doesFontExist(font))) {
error = new FontNotFoundError(font, error);
}
if (error) return res.status(error?.statusCode || 500).send(error?.message);
return next();
};
export default requiredQueryMiddleware;

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"target": "esnext",
"module": "commonjs",
"moduleResolution": "node",
"strict": false,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"outDir": "lib"
},
"include": ["src/**/*.js", "src/**/*.ts", "src/**/*.d.ts"],
"exclude": ["node_modules"]
}

1312
yarn.lock

File diff suppressed because it is too large Load Diff