diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b09f7e7..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - "extends": "google", - "parserOptions": { - "ecmaVersion": 6 - } -}; - diff --git a/Dockerfile b/Dockerfile index eb2c9de..1f701cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,9 @@ -FROM jrottenberg/ffmpeg:centos - -MAINTAINER Paul Visco - ##################################################################### # # A Docker image to convert audio and video for web using web API # # with -# - Latest FFMPEG (built) +# - FFMPEG (built) # - NodeJS # - fluent-ffmpeg # @@ -15,47 +11,46 @@ MAINTAINER Paul Visco # # https://github.com/fluent-ffmpeg/node-fluent-ffmpeg # +# Original image and FFMPEG API by Paul Visco +# https://github.com/surebert/docker-ffmpeg-service +# ##################################################################### -# Add the following two dependencies for nodejs -RUN yum install -y git -RUN yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -RUN yum install -y nodejs npm --enablerepo=epel +FROM node:12.16.2-alpine3.11 as build -WORKDIR /usr/local/src +RUN apk add --no-cache git -# Custom Builds go here -RUN npm install -g fluent-ffmpeg +# install pkg +RUN npm install -g pkg -# Remove all tmpfile and cleanup -# ================================= -WORKDIR /usr/local/ -RUN rm -rf /usr/local/src -RUN yum clean all -RUN rm -rf /var/cache/yum +ENV PKG_CACHE_PATH /usr/cache -# ================================= - -# Setup a working directory to allow for -# docker run --rm -ti -v ${PWD}:/work ... -# ======================================= -WORKDIR /work - -# Make sure Node.js is installed -RUN node -v -RUN npm -v - -#Create app dir -RUN mkdir -p /usr/src/app WORKDIR /usr/src/app -#Install Dependencies -COPY package.json /usr/src/app +# Bundle app source +COPY ./src . RUN npm install -#Bundle app source -COPY . /usr/src/app +# Create single binary file +RUN pkg --targets node12-alpine-x64 /usr/src/app/package.json + + +FROM jrottenberg/ffmpeg:4.2-alpine311 + +# Create user and change workdir +RUN adduser --disabled-password --home /home/ffmpgapi ffmpgapi +WORKDIR /home/ffmpgapi + +# Copy files from build stage +COPY --from=build /usr/src/app/ffmpegapi . +COPY --from=build /usr/src/app/index.md . +RUN chown ffmpgapi:ffmpgapi * && chmod 755 ffmpegapi EXPOSE 3000 + +# Change user +USER ffmpgapi + ENTRYPOINT [] -CMD [ "node", "app.js" ] \ No newline at end of file +CMD [ "./ffmpegapi" ] + diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..a373311 --- /dev/null +++ b/README.adoc @@ -0,0 +1,65 @@ += FFMPEG API + +An web service for converting audio/video files using FFMPEG. + +Based on: + +* https://github.com/surebert/docker-ffmpeg-service +* https://github.com/jrottenberg/ffmpeg + +FFMPEG API is provided as Docker image for easy consumption. + +== 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 /' - API Readme + +== Usage + +Convert audio/video/image files using the API. + +* `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` + +== Configuration and New Endpoints + +You can change the ffmpeg conversion settings or add new endpoints by editing +the link:src/endpoints.js[src/endpoints.js] file. + +== Docker image + +=== Build your own + +* Clone this repository. +* Build Docker image: +** `docker build -t ffmpeg-api .` +* Run image in foreground: +** `docker run -it --rm -p 3000:3000 ffmpeg-api` +* Run image in background: +** `docker run -d -name ffmpeg-api -p 3000:3000 ffmpeg-api` + +=== Use existing + +* Run image in foreground: +** `docker run -it --rm -p 3000:3000 kazhar/ffmpeg-api` +* Run image in background: +** `docker run -d --name ffmpeg-api -p 3000:3000 kazhar/ffmpeg-api` + +=== Logging + +Default log level is INFO. Set log level using environment variable. + +- Set log level to debug: + - `docker run -it --rm -p 3000:3000 -e LOG_LEVEL=debug kazhar/ffmpeg-api` + +== Background + +Originally developed by https://github.com/surebert[Paul Visco]. + +Changes include updated Node.js version, Docker image based on Alpine, logging and others. diff --git a/README.md b/README.md deleted file mode 100644 index da1c15c..0000000 --- a/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# ffmpeg web service API - -An web service for converting audio/video files using Nodejs, Express and FFMPEG - -Based off of jrottenberg/ffmpeg container - -## 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 /, /readme - Web Service Readme - -### /mp3, /m4a - -Curl Ex: - -> 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 - -## Configuration and New Endpoints -You can change the ffmpeg conversion settings or add new endpoints by editing -the /app/endpoints.js file - -## Installation - -Requires local Node and FFMPEG installation. - -1) Install FFMPEG https://ffmpeg.org/download.html - -2) Install node https://nodejs.org/en/download/ -Using homebrew: -> $ brew install node - -## Dev - Running Local Node.js Web Service - -Navigate to project directory and: - -Install dependencies: -> $ npm install - -Start app: -> $ node app.js - -Check for errors with ESLint: -> $ ./node_modules/.bin/eslint . - -## Running Local Docker Container - -Build Docker Image from Dockerfile with a set image tag. ex: docker-ffpmeg -> $ docker build -t surebert/docker-ffpmeg . - -Launch Docker Container from Docker Image, exposing port 9025 on localhost only - -> docker run -d \ - --name ffmpeg-service \ - --restart=always \ - -v /storage/tmpfs:/usr/src/app/uploads \ - -p 127.0.0.1:9025:3000 \ - surebert/docker-ffpmeg - -Launch Docker Container from Docker Image, exposing port 9026 on all IPs -> docker run -p 9025:3000 -d surebert/docker-ffpmeg diff --git a/package.json b/package.json deleted file mode 100644 index 57fdb21..0000000 --- a/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "node-ffmpeg-av", - "version": "1.0.0", - "description": "media conversion utilities", - "main": "index.js", - "author": { - "name": "Paul Visco", - "email": "paul.visco@gmail.com" - }, - "license": "ISC", - "dependencies": { - "busboy": "^0.2.14", - "compression": "^1.7.2", - "express": "^4.16.3", - "express-readme": "0.0.5", - "fluent-ffmpeg": "^2.1.2", - "fs": "0.0.1-security", - "unique-filename": "^1.1.0", - "winston": "^2.3.1" - }, - "devDependencies": { - "eslint": "^3.19.0", - "eslint-config-google": "^0.8.0" - } -} diff --git a/app.js b/src/app.js similarity index 72% rename from app.js rename to src/app.js index 0b9596f..47c7565 100644 --- a/app.js +++ b/src/app.js @@ -5,13 +5,41 @@ const Busboy = require('busboy'); const compression = require('compression'); const ffmpeg = require('fluent-ffmpeg'); const uniqueFilename = require('unique-filename'); -const consts = require(__dirname + '/app/constants.js'); -const endpoints = require(__dirname + '/app/endpoints.js'); -const winston = require('winston'); +const consts = require('./constants.js'); +const endpoints = require('./endpoints.js'); + +//const winston = require('winston'); +//setup custom logger +const { createLogger, format, transports } = require('winston'); +const { combine, timestamp, label, printf } = format; + +const logFormat = printf(({ level, message, label, timestamp }) => { + return `${timestamp} [${label}] ${level}: ${message}`; +}); + +const logger = createLogger({ + format: combine( + label({ label: 'ffmpegapi' }), + timestamp(), + logFormat + ), + transports: [new transports.Console({ + level: process.env.LOG_LEVEL || 'info' + })] +}); + +// catch SIGINT and SIGTERM and exit +// Using a single function to handle multiple signals +function handle(signal) { + console.log(`Received ${signal}. Exiting...`); + process.exit(1) + } +//SIGINT is typically CTRL-C +process.on('SIGINT', handle); +//SIGTERM is sent to terminate process, for example docker stop sends SIGTERM +process.on('SIGTERM', handle); app.use(compression()); -winston.remove(winston.transports.Console); -winston.add(winston.transports.Console, {'timestamp': true}); for (let prop in endpoints.types) { if (endpoints.types.hasOwnProperty(prop)) { @@ -20,7 +48,8 @@ for (let prop in endpoints.types) { app.post('/' + prop, function(req, res) { let hitLimit = false; let fileName = ''; - let savedFile = uniqueFilename(__dirname + '/uploads/'); + //let savedFile = uniqueFilename(__dirname + '/uploads/'); + let savedFile = uniqueFilename('/tmp/'); let busboy = new Busboy({ headers: req.headers, limits: { @@ -28,7 +57,7 @@ for (let prop in endpoints.types) { fileSize: consts.fileSizeLimit, }}); busboy.on('filesLimit', function() { - winston.error(JSON.stringify({ + logger.error(JSON.stringify({ type: 'filesLimit', message: 'Upload file size limit hit', })); @@ -45,7 +74,7 @@ for (let prop in endpoints.types) { hitLimit = true; let err = {file: filename, error: 'exceeds max size limit'}; err = JSON.stringify(err); - winston.error(err); + logger.error(err); res.writeHead(500, {'Connection': 'close'}); res.end(err); }); @@ -54,24 +83,24 @@ for (let prop in endpoints.types) { encoding: encoding, mimetype: mimetype, }; - winston.info(JSON.stringify(log)); + logger.log('debug',JSON.stringify(log)); file.on('data', function(data) { bytes += data.length; }); file.on('end', function(data) { log.bytes = bytes; - winston.info(JSON.stringify(log)); + logger.log('debug',JSON.stringify(log)); }); fileName = filename; - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'Uploading', name: fileName, })); let written = file.pipe(fs.createWriteStream(savedFile)); if (written) { - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'saved', path: savedFile, })); @@ -82,12 +111,12 @@ for (let prop in endpoints.types) { fs.unlinkSync(savedFile); return; } - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'upload complete', name: fileName, })); let outputFile = savedFile + '.' + ffmpegParams.extension; - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'begin conversion', from: savedFile, to: outputFile, @@ -101,31 +130,31 @@ for (let prop in endpoints.types) { type: 'ffmpeg', message: err, }); - winston.error(log); + logger.error(log); fs.unlinkSync(savedFile); res.writeHead(500, {'Connection': 'close'}); res.end(log); }) .on('end', function() { fs.unlinkSync(savedFile); - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'starting download to client', file: savedFile, })); res.download(outputFile, null, function(err) { if (err) { - winston.error(JSON.stringify({ + logger.error(JSON.stringify({ type: 'download', message: err, })); } - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'deleting', file: outputFile, })); if (fs.unlinkSync(outputFile)) { - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'deleted', file: outputFile, })); @@ -140,21 +169,23 @@ for (let prop in endpoints.types) { } require('express-readme')(app, { - filename: 'README.md', + filename: 'index.md', routes: ['/', '/readme'], }); + const server = app.listen(consts.port, function() { let host = server.address().address; let port = server.address().port; - winston.info(JSON.stringify({ + logger.info(JSON.stringify({ action: 'listening', url: 'http://'+host+':'+port, })); }); + server.on('connection', function(socket) { - winston.info(JSON.stringify({ + logger.log('debug',JSON.stringify({ action: 'new connection', timeout: consts.timeout, })); diff --git a/app/constants.js b/src/constants.js similarity index 100% rename from app/constants.js rename to src/constants.js diff --git a/app/endpoints.js b/src/endpoints.js similarity index 100% rename from app/endpoints.js rename to src/endpoints.js diff --git a/src/index.md b/src/index.md new file mode 100644 index 0000000..5ce20f0 --- /dev/null +++ b/src/index.md @@ -0,0 +1,24 @@ +# ffmpeg API + +An web service for converting audio/video files using FFMPEG. + +Based on https://github.com/surebert/docker-ffmpeg-service and https://github.com/jrottenberg/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` + + diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..a167565 --- /dev/null +++ b/src/package.json @@ -0,0 +1,26 @@ +{ + "name": "ffmpegapi", + "version": "1.0.0", + "description": "API for FFMPEG, media conversion utility", + "main": "app.js", + "bin" : { "ffmpegapi" : "./app.js" }, + "author": { + "name": "Paul Visco", + "email": "paul.visco@gmail.com" + }, + "license": "ISC", + "dependencies": { + "busboy": "~0.3.1", + "compression": "^1.7.4", + "express": "^4.17.1", + "express-readme": "0.0.5", + "fluent-ffmpeg": "^2.1.2", + "fs": "0.0.1-security", + "unique-filename": "^1.1.1", + "winston": "^3.2.1" + }, + "devDependencies": { + "eslint": "^6.8.0", + "eslint-config-google": "^0.14.0" + } +}