Files
archived-makesweet-api/index.js
2022-01-21 16:45:21 -05:00

207 lines
6.5 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; -*-
const express = require('express')
const childProcess = require('child_process')
const multer = require('multer')
const uuid = require('uuid')
const fs = require ('fs')
const os = require('os')
const path = require('path')
const host = process.env.MAKESWEET_HOST || 'localhost'
const port = process.env.MAKESWEET_PORT || 3000
const workDir = '/tmp/makesweet-api'
const upload = multer({ dest: workDir })
const app = express()
app.use(express.static('public'));
app.post('/make/:template', upload.any('images'), handleErrors, (req, res) => {
console.log("HEADERS", req.headers);
const authorization = String(req.header('authorization')).replace(/[^a-zA-Z0-9]/g, '');
if (authorization.length === 0 || !fs.existsSync(path.join(process.cwd(), 'keys', authorization))) {
throw new ApiError(401, 'You need a good Authorization header');
}
const textHtml = `${process.cwd()}/text.html`;
const output = `${workDir}/${uuid.v4()}.gif`;
const template = getTemplatePathFromName(req.params.template);
const generator = new Generator();
generator.useTemplate(template);
try {
if (req.query.textborder) {
generator.setTextBorder(Number(req.query.textborder));
}
generator.addTexts(req.query.text, textHtml);
generator.addImages(req.files);
if (req.query.textfirst) {
generator.setTextFirst();
}
generator.setOutput(output);
res.send(generator.apply());
} finally {
generator.clean();
}
})
app.use(handleErrors)
app.listen(port, host, () => {
console.log(`Example app listening at http://${host}:${port}`)
})
function execSync(command) {
console.log("Executing: " + command);
childProcess.execSync(command, { stdio: 'inherit' });
}
function text2image(htmlTemplate, outputDir, prefix, txt, border) {
console.log("text2image:", txt);
const data = {
text: txt.replace(/[ \n\r\t]+\/\/[ \n\r\t]+/g, '\n').trim()
};
fs.writeFileSync(`${outputDir}/text.json`, JSON.stringify(data));
fs.copyFileSync(htmlTemplate, `${outputDir}/text.html`);
console.log("ready");
execSync(`wkhtmltoimage --enable-local-file-access --transparent --width 3000 --window-status ready_to_print ${outputDir}/text.html ${outputDir}/${prefix}.png`);
console.log("123");
execSync(`convert ${outputDir}/${prefix}.png -trim -bordercolor none -border ` + border + ` ${outputDir}/${prefix}2.png`);
console.log("234");
return `${outputDir}/${prefix}2.png`;
}
function convertImage(pre, post) {
execSync(`convert ${pre} ${post}`);
}
function getTemplatePathFromName(name) {
if (!name) {
throw new ApiError(400, 'i must have a template i cannot do anything without a template');
}
const nameStr = String(name);
if (!nameStr.match(/^[-a-zA-Z0-9]+$/)) {
throw new ApiError(400, 'is that really a template: ' + nameStr);
}
const template = `templates/${nameStr}.zip`
if (!fs.existsSync(template)) {
throw new ApiError(404, 'i seek the template everywhere, but it is not to be found.');
}
return template;
}
function handleErrors(err, req, res, next) {
console.error("Problem:", {err, code: err.code});
if (err.code) {
return res.status(err.code).json({error: err.message})
} else {
res.status(500).json({error: String(err).replace(/\/home\/[^\/]*\//g, '/home/***/')})
}
}
class ApiError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
class Generator {
constructor() {
this.fnames = [];
this.images = [];
this.texts = [];
this.template = null;
this.output = null;
this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'makesweet-api'));
this.textFirst = false;
this.textBorder = 60;
}
useFile(fname) {
this.fnames.push(fname);
}
useTemplate(fname) {
this.useFile(fname);
this.template = fname;
}
setTextFirst() {
this.textFirst = true;
}
setTextBorder(border) {
this.textBorder = border;
}
addImages(files) {
console.log("FILES", {files});
if (!files) { return; }
for (const file of files) {
// Make sure images are in a format the animator can read.
// It is quite fussy - but has good png support.
const normalized = file.path + ".png";
convertImage(file.path, normalized);
this.images.push(normalized);
this.fnames.push(file.path);
this.fnames.push(normalized);
}
}
addTexts(textOrTexts, htmlTemplate) {
console.log({textOrTexts, htmlTemplate});
const texts = !textOrTexts ? [] :
(typeof textOrTexts === 'string') ? [textOrTexts] :
[...textOrTexts];
console.log("texts", texts)
let i = 0;
for (const txt of texts) {
const fname = text2image(htmlTemplate, this.tmpDir, `text_${i}`, txt, this.textBorder);
this.texts.push(fname);
this.fnames.push(fname);
i++;
}
}
setOutput(output) {
this.output = output;
this.fnames.push(output);
}
getVolumes() {
const dirs = new Set(this.fnames.map(fname => path.resolve(path.dirname(fname))));
return [...dirs].sort();
}
getCommand() {
const command = ['run', '--rm'];
for (const volume of this.getVolumes()) {
command.push('-v', `${volume}:${volume}`);
}
command.push('--mount', 'type=tmpfs,destination=/share')
command.push('paulfitz/makesweet');
command.push('--zip', path.resolve(this.template));
command.push('--in');
const parts = this.textFirst ? [this.texts, this.images] : [this.images, this.texts];
for (const part of parts) {
for (const file of part) {
command.push(file);
}
}
command.push('--gif', this.output);
return command;
}
apply() {
const command = this.getCommand();
console.log("command:", command);
childProcess.execFileSync('docker', command);
return fs.readFileSync(this.output);
}
clean() {
try {
fs.rmSync(this.tmpDir, { recursive: true });
for (const image of this.images) {
fs.unlinkSync(image);
}
if (this.output) {
fs.unlinkSync(this.output);
}
} catch (e) {
console.error(`Error removing tmpDir: ${e}`);
}
}
}