import configparser import json import magic import os import re import shutil import uuid from flask import Flask, Response, request from flask import redirect, url_for from werkzeug.utils import safe_join from api.auth import Authenticator, BackendException import api.daemon_detect def create_app(): app = Flask(__name__) app.root_path, _ = os.path.split(app.root_path) config = configparser.ConfigParser() config.read(os.path.join(app.root_path, 'config.ini')) queuedir = os.path.join(app.root_path, 'queue') errordir = os.path.join(app.root_path,'error') storagedir = os.path.join(app.root_path, 'storage') app.logger.setLevel(config['api'].get('log_level', 'ERROR')) authenticator = Authenticator(config, app.logger) # Flask apparently breaks mod_xsendfile by sending too many # headers if you use the built-in flask.send_file() with # app.use_x_sendfile=True, hence this helper instead. def send_file(filepath, extra_headers={}): r = Response(status=200) r.headers['X-Sendfile'] = filepath if extra_headers: for key, value in extra_headers.items(): r.headers[key] = value return r @app.route('/') def base(): return Response(response='REST') @app.route('/login') def login(): return redirect(request.args.get('next')) @app.route('/presentation') def list_presentations(): presentations = os.listdir(storagedir) return Response(response=json.dumps(presentations), content_type='application/json') @app.route('/presentation/', methods=['GET']) def get_presentation(presentation): path = os.path.join(storagedir, presentation, 'package.json') try: with open(path, 'r') as p: package = p.read() #for some reason a missing file doesn't give the appropriate error except PermissionError: return Response(response='Not found', status_code=404) return Response(response=package, content_type='application/json') @app.route('/presentation/', methods=['DELETE']) def delete_presentation(presentation): data = request.get_json() if 'Authorization' in request.headers: ok = 'authorized' result = _authenticate(request.headers['Authorization'], ok) if result != ok: return result elif ('auth' not in data or config['api']['password'] != data['auth']): m = "Use correct credentials to access this endpoint" return Response(response=m, status=401) return _delete(presentation) @app.route('/presentation//') def serve_file(presentation, path): realpath = safe_join(storagedir, os.path.join(presentation, path)) mimetype = None try: mimetype = magic.from_file(realpath, mime=True) except FileNotFoundError as e: return Response(status=404) if mimetype.startswith('image/'): return send_file(realpath) return _authenticate(request.args.get('token'), send_file(realpath)) @app.route('/notify', methods=['POST', 'GET']) def notify(suf=''): return Response(response="Deprecated endpoint. " "Use /notify/{default|cattura|mediasite} instead.") @app.route('/notify/default', methods=['POST']) def notify_default(): return Response(response=_enqueue({'type': 'default', 'data': request.json})) @app.route('/notify/mediasite', methods=['POST']) def notify_mediasite(): return Response(response=_enqueue({'type': 'mediasite', 'data': request.json})) @app.route('/notify/cattura', methods=['POST']) def notify_cattura(): indata = request.form result = indata['eventType'] if result != 'PUBLISH_SUCCESS': _log_publish_failure(indata['eventSource'], indata['eventData']) return Response(response='Error logged') queuepackage = {'type': 'cattura', 'recorder': indata['eventSource'], 'data': json.loads(indata['eventData'])} result = _enqueue(queuepackage) return Response(response=result) @app.route('/notify/arec', methods=['POST']) def notify_arec(): indata = request.form upload_dir = indata['upload_dir'] queuepackage = {'type': 'arec', 'upload_dir': upload_dir} result = _enqueue(queuepackage) return Response(response=result) @app.route('/status/daemon') def daemon_status(): running = False if api.daemon_detect.is_running(): running = True return Response(response=json.dumps({'running': running}), content_type='application/json') @app.route('/status/queue', methods=['GET']) def status_queue(): def path_filter(path): if not re.match(r'^[0-9a-f-]+$', os.path.basename(path)): return False if not os.path.isfile(path): return False return True os_queue_list = filter(path_filter, [os.path.join(queuedir, item) for item in os.listdir(queuedir)]) os_error_list = os.listdir(errordir) queue_list = [] for path in sorted(os_queue_list, key=os.path.getctime): item = os.path.basename(path) with open(path, 'r') as f: data = json.load(f) mydata = {'type': data['type'], 'id': item, 'uploaded': os.path.getctime(path)} if data['type'] == 'cattura': mydata['recorder'] = data['recorder'] mydata['title'] = data['data']['mediaPackageID'] else: mydata['title'] = data['data']['title'] mydata['description'] = data['data']['description'] mydata['courses'] = data['data']['courses'] mydata['presenters'] = data['data']['presenters'] mydata['tags'] = data['data']['tags'] mydata['created'] = data['data']['created'] if item in os_error_list: with open(os.path.join(errordir, item), 'r') as e: mydata['error'] = e.read() queue_list.append(mydata) return Response(response=json.dumps(queue_list), content_type='application/json') def _authenticate(token, success_response): if token: try: result = authenticator.authenticate(token) except BackendException as e: return Response(response=f'{e.status}: {e.message}', status=502) if result == 'granted': return success_response elif result == 'invalid': return Response(status=205) return Response(response='Unauthorized', status=403) def _delete(presentation): path = os.path.join(storagedir, presentation) if os.path.exists(path): shutil.rmtree(path) return Response(response=f'Deleted {presentation}') return Response(response=f'{presentation} not found', status=404) def _enqueue(package, suf=''): uuid = _get_uuid() filename = f'{uuid}{suf}' tempname = f'{filename}.tmp' # write to a temporary file first in order to avoid races. # the daemon will ignore files with an extension with open(os.path.join(queuedir, tempname), 'x') as f: f.write(json.dumps(package)) # move to the final location with an atomic move os.rename(os.path.join(queuedir, tempname), os.path.join(queuedir, filename)) return uuid def _get_uuid(): candidate = str(uuid.uuid4()) for path in [os.path.join(p, candidate) for p in [queuedir, storagedir]]: if os.path.exists(path): return _get_uuid() return candidate def _log_publish_failure(source, data): app.logger.warning('%s failed to publish %s', source, data) return app