123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- import base64, uuid
- import datetime
- import hashlib
- import os
- import subprocess
- from subprocess import Popen, PIPE
- import tornado.ioloop
- import tornado.web
- from tornado.escape import url_escape
- from tornado.options import define, options
- from cgi import escape as htmlspecialchars
- import xattr
- import logging
- log = logging.getLogger(__name__)
- log.setLevel(logging.DEBUG)
- class KonvertHandler(tornado.web.RequestHandler):
- def writeln(self, *args, **kwargs):
- super().write(*args, **kwargs)
- self.write("\n")
- class DownloadHandler(tornado.web.StaticFileHandler):
- def get(self, *args, head=False, **kwargs):
- with open(path, 'rb') as f:
- self.set_header('Expires', datetime.datetime.utcnow() + datetime.timedelta(1000000))
- try:
- mimetype = xattr.get(f, 'user.mime_type').decode('utf-8')
- log.debug('Found mime_type in xattrs')
- self.set_header('Content-Type', mimetype)
- except OSError:
- pass
- try:
- orig_filename = xattr.get(f, 'user.filename').decode('utf-8')
- self.set_header('Content-Disposition',' inline; filename="{}"'.format(url_escape(orig_filename, plus=False)))
- except OSError:
- pass
- self.set_header('Content-Length',os.stat(f.fileno()).st_size)
- if head:
- self.finish()
- return
- return super().get(*args, **kwargs)
- def head(self, *args, **kwargs):
- return self.get(*args, head=True, **kwargs)
- class IndexHandler(KonvertHandler):
- def get(self):
- log.debug
- @tornado.web.stream_request_body
- class MainHandler(KonvertHandler):
- expect_hash = None
- def sha512_existed(self):
- self.load_uuid_by_hash('sha512', self.sha512sum)
- with open(os.path.join(self._metadatadir, 'filenames'), 'a') as f:
- ''' Append the new filename (maybe check for dupes in the future? '''
- f.write(self._filename + "\n")
- self.report_back()
- self.finish()
- def initialize(self):
- log.debug('Initializing variables')
- self.recv_bytes = 0
- self.sha512 = hashlib.sha512()
- self._sha512dir = os.path.join('uploads', 'by-hash', 'sha512')
- os.makedirs(self._sha512dir, exist_ok=True)
- def prepare(self):
- supplied_hash = self.request.headers.get('X-Body-Hash')
- if supplied_hash:
- ''' should be able to handle something like: hexdigest; algo=sha512'''
- hexdigest = None
- algo = None # default
- for part in supplised_hash.split(';'):
- if hexdigest is None:
- hexdigest = part.strip()
- continue
- try:
- (var_name, var_value) = list(x.strip() for x in part.split('='))
- if var_name == 'algo':
- if not var_value in supported_algos:
- algo = False
- raise KeyError('Algorithm "{}" not supported, try sha512 instead.'.format(var_value))
- algo = var_value
- except ValueError as e:
- log.info('Malformed X-Body-Hash part could not be parsed: {}'.format(part))
- except KeyError as e:
- log.info(e)
-
- if algo or algo is None:
- self.expecthash = (algo, hexdigest)
- log.debug('Expected hash value presented: '.self.expecthash)
- if os.path.exists(os.path.join(self._sha512dir, self.expecthash[1].lower())):
- self.sha512sum = self.expect_hash
- ''' this runs finish() '''
- self.sha512_existed()
- log.debug('Preparing to receive file data')
- self._uuid = uuid.uuid4().hex
- log.debug('Using uuid={}'.format(self._uuid))
- self._metadatadir = os.path.join('metadata', self._uuid)
- self._uploaddir = os.path.join('receiving', self._uuid)
- os.makedirs(self._metadatadir)
- os.makedirs(self._uploaddir)
- self._recvpath = os.path.join(self._uploaddir, 'receiving')
- def data_received(self, data):
- if self.recv_bytes == 0:
- self._file = open(self._recvpath, 'wb')
- log.debug('receiving %d bytes' % len(data))
- self.recv_bytes += len(data)
- self.sha512.update(data)
- self._file.write(data)
- log.debug('%d bytes received in total' % self.recv_bytes)
- def load_uuid_by_hash(self, hash_algo, hexdigest):
- with open(os.path.join('uploads', 'by-hash', hash_algo, hexdigest + '.uuid'), 'r') as f:
- uuid = f.read(127).strip()
- log.debug('read uuid=%s' % uuid)
- self._uuid = uuid
- return uuid
- def put(self, filename):
- if len(filename) > 127:
- (filename, ext) = os.path.splitext(filename)
- extlen = len(ext)+1 # with a dot
- filename = '%s.%s' % (filename[:(127-extlen)], ext,)
- self._file.close()
- self.sha512sum = self.sha512.hexdigest()
- if self.expect_hash and self.expect_hash != ('sha512', self.sha512sum,):
- raise ValueError('Actual hash did not match client X-Body-Hash header')
- sha512path = os.path.join(self._sha512dir, self.sha512sum)
- self._filename = os.path.basename(filename)
- if os.path.exists(sha512path):
- try:
- os.remove(self._recvpath)
- except FileNotFoundError as e:
- log.info('File already gone or never existed: {}'.format(self._recvpath))
- ''' FIXME: remove metadatadir with old uuid too '''
- self.sha512_existed()
- os.rename(self._recvpath, sha512path)
- xattr.setxattr(sha512path, 'user.filename', filename.encode())
- mimetype = Popen(['file', '-b','--mime-type', sha512path], stdout=PIPE).communicate()[0].decode('utf8').strip()
- xattr.setxattr(sha512path, 'user.mime_type', mimetype.encode())
- with open(os.path.join(self._metadatadir, 'filenames'), 'a') as f:
- f.write(filename + "\n")
- with open(os.path.join(sha512path + '.uuid'), 'w') as f:
- f.write(self._uuid)
- with open(os.path.join(self._metadatadir, 'mimetype'), 'w') as f:
- f.write(mimetype)
- with open(os.path.join(self._metadatadir, 'SHA512SUMS'), 'w') as f:
- f.write('{} file/{}'.format(self.sha512sum, self._filename))
- return self.report_back()
- def report_back(self, url=None, redirect=False):
- self.write(url or 'http://{}/uploads/by-hash/sha512/{}\n'.format(self.request.host, self.sha512sum))
- if redirect:
- self.redirect('http://{}/metadata/by-hash/sha512/{}'.format(self.request.host, self.sha512sum), status=303)
- supported_algos = ['sha512']
- templates = {
- 'ffmpeg__opus': {
- 'streams': ['audio'],
- 'command': 'ffmpeg',
- 'file_ext': 'opus',
- 'args': [
- ('-loglevel', 'warning',),
- ('-i', '%%FILENAME%%',),
- ('-f', 'opus',),
- ('-c:a', 'opus',),
- ('-vn'),
- ],
- },
- 'ffmpeg__flac': {
- 'streams': ['audio'],
- 'command': 'ffmpeg',
- 'file_ext': 'flac',
- 'args': [
- ('-loglevel', 'warning',),
- ('-f', 'flac',),
- ('-c:a', 'libvorbis',),
- ('-vn'),
- ],
- },
- 'ffmpeg__ogg_vorbis': {
- 'streams': ['audio'],
- 'command': 'ffmpeg',
- 'file_ext': 'ogg',
- 'args': [
- ('-loglevel', 'warning',),
- ('-i', '%%FILENAME%%',),
- ('-f', 'oga',),
- ('-c:a', 'libvorbis',),
- ('-vn'),
- ],
- },
- 'ffmpeg__wvga_webm_vorbis@128k_vp8@600k': {
- 'streams': ['video', 'audio'],
- 'command': 'ffmpeg',
- 'file_ext': 'webm',
- 'args': [
- ('-loglevel', 'warning',),
- ('-progress', '%%FFMPEG_PROGRESS_URL%%',),
- ('-i', '%%FILENAME%%',),
- ('-s', 'wvga',),
- ('-f', 'webm',),
- ('-c:a', 'libvorbis',),
- ('-b:a', '128k',),
- ('-c:v', 'libvpx',),
- ('-b:v', '600k',),
- ],
- },
- }
- class ProcessHandler(KonvertHandler):
- def load_filename(self, uuid):
- with open(os.path.join('uploads', 'by-uuid', uuid, 'filename'), 'r') as f:
- filename = f.read(127)
- log.debug('read filename=%s' % filename)
- return filename
- def load_mimetype(self, uuid):
- with open(os.path.join('uploads', 'by-uuid', uuid, 'mimetype'), 'r') as f:
- mimetype = f.read(127)
- log.debug('read mimetype=%s' % mimetype)
- return mimetype
- def load_template(self, template_name):
- return templates[template_name]
- def get(self, uuid, template_name):
- filename = self.load_filename(uuid)
- metadata_dir = os.path.join('uploads', 'by-uuid', uuid)
- filepath = os.path.join(metadata_dir, 'file', filename)
- mimetype = self.load_mimetype(uuid)
- self.writeln('%s from %s to %s' % (uuid, mimetype, template_name,))
- template = self.load_template(template_name)
- outname = '%s.%s' % (os.path.splitext(filename)[0], template['file_ext'],)
- outdir = os.path.join('uploads', 'by-uuid', uuid, 'convert', template_name)
- os.makedirs(outdir)
- outtmp = os.path.join(outdir, 'converting')
- outpath = os.path.join(outdir, outname)
- progress_url = 'http://{}/uploads/{}/convert/{}/progress'.format(self.request.host, uuid, url_escape(template_name, plus=False))
- ffmpeg_cmd = [template['command']]
- for arg in template['args']:
- for part in arg:
- log.debug('replacing macros with values in: %s' % part)
- part = part.replace('%%FILENAME%%', filepath)
- part = part.replace('%%FFMPEG_PROGRESS_URL%%', progress_url)
- ffmpeg_cmd.append(part)
- ffmpeg_cmd += [outtmp]
- ffmpeg_log = open(os.path.join(outdir, 'ffmpeg.log'), 'wb')
- log.debug('Running %s' % ffmpeg_cmd)
- p_conv = Popen(ffmpeg_cmd, stdout=ffmpeg_log, stderr=subprocess.STDOUT)
- try:
- p_conv.wait(2)
- except subprocess.TimeoutExpired as e:
- self.writeln('Resulting file will be available at %s' % 'http://{}/uploads/{}/convert/{}/{}'.format(self.request.host, uuid, url_escape(template_name, plus=False), url_escape(filename, plus=False)))
- self.set_status(202, reason='Accepted, will konvert it. Please come back later.')
- return
- if p_conv.poll() is None:
- raise IOError('Process returncode None despite that it should have ended!')
- if p_conv.returncode < 0:
- raise ValueError('ffmpeg exited unnaturally with POSIX signal %d' % abs(p_conv.returncode))
- elif p_conv.returncode > 0:
- self.set_status(500, 'ffmpeg exited due to an error, please see the log output at {}'.format('http://{}/uploads/{}/convert/{}/ffmpeg.log'.format(self.request.host, uuid, template_name), status=500))
- return
- return self.report_back(uuid, template_name, outname)
- def report_back(self, uuid, template_name, filename):
- url = 'http://{}/uploads/{}/convert/{}'.format(self.request.host, uuid, url_escape(filename, plus=False))
- self.writeln(url)
- self.redirect(url)
- @tornado.web.stream_request_body
- class ProgressHandler(tornado.web.RequestHandler):
- def load_progress(self, uuid, template_name):
- template_name = os.path.basename(template_name)
- with open(os.path.join('uploads', 'by-uuid', uuid, 'convert', template_name, 'progress'), 'r') as f:
- progress = f.read(127)
- log.debug('read progress=%s' % progress)
- return progress
- def prepare(self):
- log.debug('new progress for %s template %s' % (self.path_kwargs['uuid'], self.path_kwargs['template_name']))
- self._uuid = self.path_kwargs['uuid']
- self._template_name = self.path_kwargs['template_name']
- def data_received(self, data):
- #self.save_progress(data)
- log.debug('progress ongoing: %s' % data)
- def get(self, uuid, template_name):
- self.write(self.load_progress(uuid, template_name))
- def post(self, uuid, template_name):
- log.debug('%s with %s' % (uuid, template_name,))
- class HelpHandler(KonvertHandler):
- def get(self, section):
- self.writeln('<h1>Konvert Help</h1>')
- if section == 'convert':
- self.writeln('<h2>Convert</h2>')
- self.writeln('<p>Convert by appending the UUID URL with <code>/convert/%%TEMPLATE_NAME%%</code> where <code>%%TEMPLATE_NAME%%</code> is one of the templates listed below.</p>')
- self.writeln('<h3>Templates</h3>')
- self.writeln('<ul>')
- for template in templates:
- self.write('<li>{}</li>'.format(htmlspecialchars(template)))
- self.writeln('</ul>')
- application = tornado.web.Application([
- (r'/uploads/(by-hash/sha512/[a-f0-9]{16,})', tornado.web.StaticFileHandler, {
- 'path': os.path.join(os.path.dirname(__file__), 'uploads'),
- }),
- (r'/uploads/([a-f0-9]{16,}/file/.*)', tornado.web.StaticFileHandler, {
- 'path': os.path.join(os.path.dirname(__file__), 'uploads'),
- }),
- (r'/uploads/([a-f0-9]{16,}/convert/[\w\d\-\_\.\@\%]+/(?!progress).*)', tornado.web.StaticFileHandler, {
- 'path': os.path.join(os.path.dirname(__file__), 'uploads'),
- }),
- (r'/uploads/(?P<uuid>[a-f0-9]{16,})/convert/(?P<template_name>[\w\d\-\_\.\@\%]+)/progress', ProgressHandler),
- (r'/uploads/(?P<uuid>[a-f0-9]{16,})/convert/(?P<template_name>[\w\d\-\_\.\@\%]+)', ProcessHandler),
- (r'/help/(\w+)', HelpHandler),
- (r'/index.html', IndexHandler, {'path': os.path.join(os.path.dirname(__file__), 'uploads')}),
- (r'/(.*)', MainHandler),
- ], autoreload=True, debug=True)
- if __name__ == '__main__':
- tornado.options.parse_command_line()
- path = os.path.join(os.path.join(os.path.dirname(__file__)), 'uploads')
- if not os.path.exists(path):
- os.makedirs(path)
- port = 8888
- log.info('Listening on %d' % port)
- application.listen(port)
- tornado.ioloop.IOLoop.instance().start()
|