From e0abc48efd43cba764397984ca54598933293818 Mon Sep 17 00:00:00 2001 From: marcsello Date: Fri, 27 Nov 2020 05:44:32 +0100 Subject: [PATCH] Initial commit --- .gitignore | 8 +++ caff_previewer_wrapper/app.py | 76 +++++++++++++++++++++++++++++ caff_previewer_wrapper/config.py | 9 ++++ caff_previewer_wrapper/converter.py | 41 ++++++++++++++++ caff_previewer_wrapper/utils.py | 37 ++++++++++++++ requirements.txt | 2 + 6 files changed, 173 insertions(+) create mode 100644 .gitignore create mode 100644 caff_previewer_wrapper/app.py create mode 100644 caff_previewer_wrapper/config.py create mode 100644 caff_previewer_wrapper/converter.py create mode 100644 caff_previewer_wrapper/utils.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..800368f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.swp +venv/* +*.pyc +__pycache__/* +__pycache__ +*.wpr +*.log +.idea/ \ No newline at end of file diff --git a/caff_previewer_wrapper/app.py b/caff_previewer_wrapper/app.py new file mode 100644 index 0000000..65e27cb --- /dev/null +++ b/caff_previewer_wrapper/app.py @@ -0,0 +1,76 @@ +import tempfile +import os +import os.path +import shutil +from flask import Flask, current_app + +from config import Config + +from utils import write_file_to_fd_while_calculating_md5, create_md5_sum_for_file +from converter import convert_caff_to_tga, convert_tga_to_png + +app = Flask(__name__) +app.config.from_object(Config) + + +def do_everything(workdir: str): + # Recieve file + uploaded_caff_fd, uploaded_caff_path = tempfile.mkstemp(suffix='.caff', dir=workdir) + + uploaded_caff_md5sum = write_file_to_fd_while_calculating_md5(uploaded_caff_fd) # Throws overflow error + + # Convert CAFF to TGA + converted_tga_fd, converted_tga_path = tempfile.mkstemp(suffix='.tga', dir=workdir) + os.close(converted_tga_fd) + + convert_caff_to_tga(uploaded_caff_path, converted_tga_path) + + if not os.path.isfile(converted_tga_path): + raise FileNotFoundError("Conversion output is missing") + + # Convert TGA to PNG + converted_png_fd, converted_png_path = tempfile.mkstemp(suffix='.png', dir=workdir) + os.close(converted_png_fd) + + convert_tga_to_png(converted_tga_path, converted_png_path) + + if not os.path.isfile(converted_tga_path): + raise FileNotFoundError("Conversion output is missing") + + converted_png_md5sum = create_md5_sum_for_file(converted_png_path) + + # Send back converted file + converted_png_handle = open(converted_png_path, 'rb') + + def stream_and_remove_file(): + # This really is some black magic here + # When flask transmits the file ... + yield from converted_png_handle # <- It transmits from file handle + # After it's done the rest of this function will be called, so it cleans up after itself + converted_png_handle.close() + shutil.rmtree(workdir) + + return current_app.response_class( + stream_and_remove_file(), + headers={ + 'X-request-checksum': uploaded_caff_md5sum, + 'X-response-checksum': converted_png_md5sum, + 'Content-type': 'image/png' + } + ) + + +@app.route('/preview', methods=['POST']) +def perform_conversion(): + workdir = tempfile.mkdtemp(prefix='caff') + try: + response = do_everything(workdir) # normally this would clean up workdir + except: + shutil.rmtree(workdir) # but sometimes it must be done externally + raise + + return response + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/caff_previewer_wrapper/config.py b/caff_previewer_wrapper/config.py new file mode 100644 index 0000000..5c60e49 --- /dev/null +++ b/caff_previewer_wrapper/config.py @@ -0,0 +1,9 @@ +import os + + +class Config: + CAFF_PREVIEWER_BINARY = os.environ.get('CAFF_PREVIEWER_BINARY', '/usr/local/bin/caff_previewer') + IMAGEMAGICK_CONVERT_BINARY = os.environ.get('IMAGEMAGICK_CONVERT_BINARY', '/usr/bin/convert') + CONVERSION_TIMEOUT = int(os.environ.get('IMAGEMAGICK_CONVERT_BINARY', 30)) + RECIEVE_CHUNKSIZE = int(os.environ.get('RECIEVE_CHUNKSIZE', 2048)) + MAX_RECIEVE_SIZE = int(os.environ.get('MAX_RECIEVE_SIZE', 536870912)) # 512 MB diff --git a/caff_previewer_wrapper/converter.py b/caff_previewer_wrapper/converter.py new file mode 100644 index 0000000..8382f0d --- /dev/null +++ b/caff_previewer_wrapper/converter.py @@ -0,0 +1,41 @@ +import subprocess +from flask import current_app +import werkzeug.exceptions + + +def run_abstract_converter(converter: str, source: str, destination: str) -> int: + """ + Just runs a binary and gives it two arguments + :param converter: the converter binary to run + :param source: source file + :param destination: destination file + :returns: exitcode of the converter + """ + completed_process = subprocess.run([converter, source, destination], + timeout=current_app.config['CONVERSION_TIMEOUT'], env={}) + + return completed_process.returncode + +def convert_caff_to_tga(source: str, destination: str): + """ + This function uses caff_previewer to convert a CAFF file into a TGA file + :param source: path of the source TGA file (must exists) + :param destination: path of the destination TGA file (will be created) + """ + INTERNAL_ERROR_CODES = [0x01, 0x03, 0x04, 0x05, 0x32, 0x51] + ret = run_abstract_converter(current_app.config['CAFF_PREVIEWER_BINARY'], source, destination) + if ret in INTERNAL_ERROR_CODES: + raise RuntimeError(f"Caff Previewer returned an unexpected error code: {ret}") + elif ret != 0: + raise werkzeug.exceptions.BadRequest("CAFF format violation") + + +def convert_tga_to_png(source: str, destination: str): + """ + This function uses ImageMagick to convert a TGA file into a PNG file + :param source: path of the source TGA file (must exists) + :param destination: path of the destination TGA file (will be created) + """ + ret = run_abstract_converter(current_app.config['IMAGEMAGICK_CONVERT_BINARY'], source, destination) + if ret != 0: + raise RuntimeError(f"Image magick convert returned an unexpected error code: {ret}") diff --git a/caff_previewer_wrapper/utils.py b/caff_previewer_wrapper/utils.py new file mode 100644 index 0000000..63d586f --- /dev/null +++ b/caff_previewer_wrapper/utils.py @@ -0,0 +1,37 @@ +from flask import request, current_app +import werkzeug.exceptions +import hashlib + + +def write_file_to_fd_while_calculating_md5(fd: int) -> str: + chunksize = current_app.config['RECIEVE_CHUNKSIZE'] + m = hashlib.md5() + + total_recieved = 0 + + # Begin recieving the file + with open(fd, "bw") as f: + + while True: # This is where uploading happens + chunk = request.stream.read(chunksize) + if len(chunk) == 0: + break + + total_recieved += len(chunk) + if total_recieved > current_app.config['MAX_RECIEVE_SIZE']: + raise werkzeug.exceptions.RequestEntityTooLarge() + + m.update(chunk) + f.write(chunk) + + return m.hexdigest() + + +def create_md5_sum_for_file(fname): + m = hashlib.md5() + + with open(fname, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + m.update(chunk) + + return m.hexdigest() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ed1285 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask~=1.1.2 +gunicorn~=20.0.4 \ No newline at end of file