mirror of
https://github.com/SrIzan10/makesweet-api.git
synced 2026-05-01 10:55:14 +00:00
207 lines
6.5 KiB
JavaScript
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}`);
|
|
}
|
|
}
|
|
}
|