major refactoring

This commit is contained in:
Sami Salkosuo 2020-04-20 09:25:22 +03:00
parent 9012927cd8
commit 1db51d5776
8 changed files with 251 additions and 152 deletions

View File

@ -1,13 +1,9 @@
const fs = require('fs');
const express = require('express');
const app = express();
const Busboy = require('busboy');
const compression = require('compression');
const ffmpeg = require('fluent-ffmpeg');
const uniqueFilename = require('unique-filename');
const endpoints = require('./endpoints.js');
const all_routes = require('express-list-endpoints');
const logger = require('./logger.js');
const logger = require('./utils/logger.js');
const constants = require('./constants.js');
fileSizeLimit = constants.fileSizeLimit;
@ -27,103 +23,22 @@ process.on('SIGTERM', handle);
app.use(compression());
for (let prop in endpoints.types) {
if (endpoints.types.hasOwnProperty(prop)) {
let ffmpegParams = endpoints.types[prop];
let bytes = 0;
app.post('/' + prop, function(req, res) {
let hitLimit = false;
let fileName = '';
let savedFile = uniqueFilename('/tmp/');
let busboy = new Busboy({
headers: req.headers,
limits: {
files: 1,
fileSize: fileSizeLimit,
}});
busboy.on('filesLimit', function() {
logger.error(`upload file size limit hit. max file size ${fileSizeLimit} bytes.`)
});
//routes to handle file upload for all POST methods
var upload = require('./routes/uploadfile.js');
app.use(upload);
busboy.on('file', function(
fieldname,
file,
filename,
encoding,
mimetype
) {
file.on('limit', function(file) {
hitLimit = true;
let msg = `${filename} exceeds max size limit. max file size ${fileSizeLimit} bytes.`
logger.error(msg);
res.writeHead(500, {'Connection': 'close'});
res.end(JSON.stringify({error: msg}));
});
let log = {
file: filename,
encoding: encoding,
mimetype: mimetype,
};
logger.debug(`file:${log.file}, encoding: ${log.encoding}, mimetype: ${log.mimetype}`);
file.on('data', function(data) {
bytes += data.length;
});
file.on('end', function(data) {
log.bytes = bytes;
logger.debug(`file: ${log.file}, encoding: ${log.encoding}, mimetype: ${log.mimetype}, bytes: ${log.bytes}`);
});
//test route for development
var test = require('./routes/test.js')
app.use('/test', test)
fileName = filename;
logger.debug(`uploading ${fileName}`)
let written = file.pipe(fs.createWriteStream(savedFile));
if (written) {
logger.debug(`${fileName} saved, path: ${savedFile}`)
}
});
busboy.on('finish', function() {
if (hitLimit) {
fs.unlinkSync(savedFile);
return;
}
logger.debug(`upload complete. file: ${fileName}`)
let outputFile = savedFile + '.' + ffmpegParams.extension;
logger.debug(`begin conversion from ${savedFile} to ${outputFile}`)
//ffmpeg processing...
let ffmpegConvertCommand = ffmpeg(savedFile);
ffmpegConvertCommand
.renice(15)
.outputOptions(ffmpegParams.outputOptions)
.on('error', function(err) {
logger.error(`${err}`);
fs.unlinkSync(savedFile);
res.writeHead(500, {'Connection': 'close'});
res.end(JSON.stringify({error: `${err}`}));
})
.on('end', function() {
fs.unlinkSync(savedFile);
logger.debug(`starting download to client ${savedFile}`);
//routes to convert audio/video/image files to mp3/mp4/jpg
var convert = require('./routes/convert.js')
app.use('/convert', convert)
res.download(outputFile, null, function(err) {
if (err) {
logger.error(`download ${err}`);
}
logger.debug(`deleting ${outputFile}`);
if (fs.unlinkSync(outputFile)) {
logger.debug(`deleted ${outputFile}`);
}
});
})
.save(outputFile);
});
return req.pipe(busboy);
});
}
}
require('express-readme')(app, {
filename: 'index.md',
routes: ['/', '/readme'],
routes: ['/'],
});
@ -140,6 +55,25 @@ server.on('connection', function(socket) {
server.keepAliveTimeout = timeout;
});
app.use(function(req, res, next) {
res.status(404).send(JSON.stringify({error: 'route not found'})+'\n');
app.get('/endpoints', function(req, res) {
let code = 200;
res.writeHead(code, {'content-type' : 'text/plain'});
res.end("Endpoints:\n\n"+JSON.stringify(all_routes(app),null,2)+'\n');
});
app.use(function(req, res, next) {
res.status(404).send({error: 'route not found'});
});
//custom error handler to return text/plain and message only
app.use(function(err, req, res, next){
let code = err.statusCode || 500;
let message = err.message;
res.writeHead(code, {'content-type' : 'text/plain'});
res.end(`${err.message}\n`);
});
logger.debug(JSON.stringify(all_routes(app)));

View File

@ -1,37 +0,0 @@
exports.types = {
jpg: {
extension: 'jpg',
outputOptions: [
'-pix_fmt yuv422p',
],
},
m4a: {
extension: 'm4a',
outputOptions: [
'-codec:a libfdk_aac',
],
},
mp3: {
extension: 'mp3',
outputOptions: [
'-codec:a libmp3lame',
],
},
mp4: {
extension: 'mp4',
outputOptions: [
'-codec:v libx264',
'-profile:v high',
'-r 15',
'-crf 23',
'-preset ultrafast',
'-b:v 500k',
'-maxrate 500k',
'-bufsize 1000k',
'-vf scale=-2:640',
'-threads 8',
'-codec:a libfdk_aac',
'-b:a 128k',
],
},
};

View File

@ -11,20 +11,6 @@ Based on:
- https://github.com/fluent-ffmpeg/node-fluent-ffmpeg
### Endpoints
- `POST /mp3` - Convert audio file in request body to mp3
- `POST /mp4` - Convert video file in request body to mp4
- `POST /jpg` - Convert image file to jpg
- `GET /` - Web Service Readme, this file
### Examples
- `curl -F "file=@input.wav" 127.0.0.1:3000/mp3 > output.mp3`
- `curl -F "file=@input.m4a" 127.0.0.1:3000/mp3 > output.mp3`
- `curl -F "file=@input.mov" 127.0.0.1:3000/mp4 > output.mp4`
- `curl -F "file=@input.mp4" 127.0.0.1:3000/mp4 > output.mp4`
- `curl -F "file=@input.tiff" 127.0.0.1:3000/jpg > output.jpg`
- `curl -F "file=@input.png" 127.0.0.1:3000/jpg > output.jpg`
# Endpoints
[API endpoints](./endpoints)

View File

@ -15,6 +15,7 @@
"express": "^4.17.1",
"express-readme": "0.0.5",
"fluent-ffmpeg": "^2.1.2",
"express-list-endpoints": "4.0.1",
"fs": "0.0.1-security",
"unique-filename": "^1.1.1",
"winston": "^3.2.1"

105
src/routes/convert.js Normal file
View File

@ -0,0 +1,105 @@
var express = require('express')
const fs = require('fs');
const ffmpeg = require('fluent-ffmpeg');
var router = express.Router()
const logger = require('../utils/logger.js')
//routes for /convert
//adds conversion type and format to res.locals. to be used in final post function
router.post('/audio/to/mp3', function (req, res,next) {
res.locals.conversion="audio"
res.locals.format="mp3"
next()
});
router.post('/video/to/mp4', function (req, res,next) {
res.locals.conversion="video"
res.locals.format="mp4"
next()
});
router.post('/image/to/jpg', function (req, res,next) {
res.locals.conversion="image"
res.locals.format="jpg"
next()
});
// convert audio or video or image to mp3 or mp4 or jpg
router.post('*', function (req, res,next) {
let format = res.locals.format;
let conversion = res.locals.conversion;
logger.debug(`path: ${req.path}, conversion: ${conversion}, format: ${format}`);
if (conversion == undefined || format == undefined)
{
res.status(400).send("Invalid convert URL. Use one of: /convert/image/to/jpg, /convert/audio/to/mp3 or /convert/video/to/mp4.\n");
return;
}
let ffmpegParams ={
extension: format
};
if (conversion == "image")
{
ffmpegParams.outputOptions= ['-pix_fmt yuv422p']
}
if (conversion == "audio")
{
ffmpegParams.outputOptions=['-codec:a libmp3lame' ]
}
if (conversion == "video")
{
ffmpegParams.outputOptions=[
'-codec:v libx264',
'-profile:v high',
'-r 15',
'-crf 23',
'-preset ultrafast',
'-b:v 500k',
'-maxrate 500k',
'-bufsize 1000k',
'-vf scale=-2:640',
'-threads 8',
'-codec:a libfdk_aac',
'-b:a 128k',
];
}
let savedFile = res.locals.savedFile;
let outputFile = savedFile + '-output.' + ffmpegParams.extension;
logger.debug(`begin conversion from ${savedFile} to ${outputFile}`)
//ffmpeg processing... converting file...
let ffmpegConvertCommand = ffmpeg(savedFile);
ffmpegConvertCommand
.renice(15)
.outputOptions(ffmpegParams.outputOptions)
.on('error', function(err) {
logger.error(`${err}`);
fs.unlinkSync(savedFile);
res.writeHead(500, {'Connection': 'close'});
res.end(JSON.stringify({error: `${err}`}));
})
.on('end', function() {
fs.unlinkSync(savedFile);
logger.debug(`starting download to client ${savedFile}`);
res.download(outputFile, null, function(err) {
if (err) {
logger.error(`download ${err}`);
}
logger.debug(`deleting ${outputFile}`);
if (fs.unlinkSync(outputFile)) {
logger.debug(`deleted ${outputFile}`);
}
});
})
.save(outputFile);
});
module.exports = router

22
src/routes/test.js Normal file
View File

@ -0,0 +1,22 @@
var express = require('express')
var router = express.Router()
const logger = require('../utils/logger.js')
//route to handle file upload in all POST requests
// convert audio or video or image to mp3 or mp4 or jpg
router.post("/",function (req, res,next) {
logger.debug("path: " + req.path);
logger.debug("req.params: ");
for (const key in req.params) {
logger.debug(`${key}: ${req.params[key]}`);
}
logger.debug("res.locals.savedFile: " + res.locals.savedFile);
res.status(200).send("Test OK.");
});
module.exports = router;

88
src/routes/uploadfile.js Normal file
View File

@ -0,0 +1,88 @@
var express = require('express')
const fs = require('fs');
const Busboy = require('busboy');
const uniqueFilename = require('unique-filename');
var router = express.Router()
const logger = require('../utils/logger.js')
//route to handle file upload in all POST requests
router.use(function (req, res,next) {
if(req.method == "POST")
{
logger.debug(`${__filename} path: ${req.path}`);
let bytes = 0;
let hitLimit = false;
let fileName = '';
var savedFile = uniqueFilename('/tmp/');
let busboy = new Busboy({
headers: req.headers,
limits: {
fields: 0, //no non-files allowed
files: 1,
fileSize: fileSizeLimit,
}});
busboy.on('filesLimit', function() {
logger.error(`upload file size limit hit. max file size ${fileSizeLimit} bytes.`)
});
busboy.on('fieldsLimit', function() {
let msg="Non-file field detected. Only files can be POSTed.";
logger.error(msg);
let err = new Error(msg);
err.statusCode = 400;
next(err);
});
busboy.on('file', function(
fieldname,
file,
filename,
encoding,
mimetype
) {
file.on('limit', function(file) {
hitLimit = true;
let msg = `${filename} exceeds max size limit. max file size ${fileSizeLimit} bytes.`
logger.error(msg);
res.writeHead(500, {'Connection': 'close'});
res.end(JSON.stringify({error: msg}));
});
let log = {
file: filename,
encoding: encoding,
mimetype: mimetype,
};
logger.debug(`file:${log.file}, encoding: ${log.encoding}, mimetype: ${log.mimetype}`);
file.on('data', function(data) {
bytes += data.length;
});
file.on('end', function(data) {
log.bytes = bytes;
logger.debug(`file: ${log.file}, encoding: ${log.encoding}, mimetype: ${log.mimetype}, bytes: ${log.bytes}`);
});
fileName = filename;
savedFile = savedFile + "-" + fileName;
logger.debug(`uploading ${fileName}`)
let written = file.pipe(fs.createWriteStream(savedFile));
if (written) {
logger.debug(`${fileName} saved, path: ${savedFile}`)
}
});
busboy.on('finish', function() {
if (hitLimit) {
fs.unlinkSync(savedFile);
return;
}
logger.debug(`upload complete. file: ${fileName}`)
res.locals.savedFile = savedFile;
next();
});
return req.pipe(busboy);
}
next();
});
module.exports = router;