224 lines
8.4 KiB
Python
224 lines
8.4 KiB
Python
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/<string: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/<string: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/<string:presentation>/<path:path>')
|
|
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
|