chronodiagram/webserver.py

355 lines
12 KiB
Python
Raw Normal View History

2021-11-22 19:54:04 +00:00
import json
import logging
import os
import tornado.ioloop
import tornado.web
import tornado.websocket
from urllib.parse import urlparse
import uuid
import datetime
import html
import argparse
import coloredlogs
import glob
class DateTimeEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime.datetime):
return o.isoformat(timespec='milliseconds')
return super().default(self, o)
class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path):
"""For subclass to add extra headers to the response"""
if path[-5:] == '.html':
self.set_header("Access-Control-Allow-Origin", "*")
if path[-4:] == '.svg':
self.set_header("Content-Type", "image/svg+xml")
class WebSocketHandler(tornado.websocket.WebSocketHandler):
"""
Websocket from the workers
"""
2021-11-23 11:30:49 +00:00
# CORS_ORIGINS = ['localhost']
2021-11-22 19:54:04 +00:00
connections = set()
def initialize(self, config):
self.config = config
self.strokes = []
self.hasWritten = False
self.dimensions = [None, None]
2021-11-23 11:30:49 +00:00
# def check_origin(self, origin):
# parsed_origin = urlparse(origin)
# # parsed_origin.netloc.lower() gives localhost:3333
# valid = any([parsed_origin.hostname.endswith(origin) for origin in self.CORS_ORIGINS])
# return valid
2021-11-22 19:54:04 +00:00
# the client connected
def open(self, p = None):
self.__class__.connections.add(self)
self.prefix = datetime.datetime.now().strftime('%Y-%m-%d-')
self.filename = self.prefix + str(self.check_filenr()) + '-' + uuid.uuid4().hex[:6]
logger.info(f"{self.filename=}")
2021-11-22 19:54:04 +00:00
self.write_message(json.dumps({
"filename": self.filename
}))
def check_filenr(self):
files = glob.glob(os.path.join(self.config.storage, self.prefix +'*'))
return len(files) + 1
def appendEvent(self, row):
# write to an appendable json format. So basically a file that should be wrapped in [] to be json-parsable
with open(os.path.join(self.config.storage,self.filename +'.json_appendable'), 'a') as fp:
if not self.hasWritten:
#metadata to first row, but only on demand
fp.write(json.dumps([datetime.datetime.now().strftime("%Y-%m-%d %T"), self.dimensions[0], self.dimensions[1]]))
# writer.writerow()
self.hasWritten = True
fp.write(',\n')
# first column is color, rest is points
fp.write(json.dumps(row))
2021-11-22 19:54:04 +00:00
# the client sent the message
def on_message(self, message):
logger.info(f"recieve: {message}")
try:
msg = json.loads(message)
if msg['event'] == 'stroke':
logger.info('stroke')
self.appendEvent(msg)
elif msg['event'] == 'dimensions':
2021-11-22 19:54:04 +00:00
self.dimensions = [int(msg['width']), int(msg['height'])]
logger.info(f"{self.dimensions=}")
elif msg['event'] == 'viewbox':
logger.info('move or resize')
self.appendEvent(msg)
2021-11-22 19:54:04 +00:00
else:
# self.send({'alert': 'Unknown request: {}'.format(message)})
logger.warn('Unknown request: {}'.format(message))
except Exception as e:
# self.send({'alert': 'Invalid request: {}'.format(e)})
logger.exception(e)
# client disconnected
def on_close(self):
self.__class__.rmConnection(self)
logger.info(f"Client disconnected: {self.request.remote_ip}")
@classmethod
def rmConnection(cls, client):
if client not in cls.connections:
return
cls.connections.remove(client)
@classmethod
def hasConnection(cls, client):
return client in cls.connections
class AudioListingHandler(tornado.web.RequestHandler):
def initialize(self, config):
self.config = config
self.audiodir = os.path.join(self.config.storage, 'audio')
def get(self):
# filename = self.get_argument("file", None)
self.set_header("Content-Type", "application/json")
if not os.path.exists(self.audiodir):
names = []
else:
names = sorted([f"/audio/{name}" for name in os.listdir(self.audiodir) if name not in ['.gitignore']])
print(names)
self.write(json.dumps(names))
2021-11-22 19:54:04 +00:00
class AnimationHandler(tornado.web.RequestHandler):
def initialize(self, config):
self.config = config
def get(self, filename):
self.set_header("Content-Type", "application/json")
# filename = self.get_argument("file", None)
if filename == '':
2021-12-21 13:31:02 +00:00
files = []
names = [name for name in os.listdir(self.config.storage) if name.endswith('json_appendable')]
for name in names:
with open(os.path.join(self.config.storage, name), 'r') as fp:
first_line = fp.readline().strip()
if first_line.endswith(','):
first_line = first_line[:-1]
print(first_line)
metadata = json.loads(first_line)
files.append({
'name': f"/files/{name[:-16]}",
"time": metadata[0],
"dimensions": [metadata[1], metadata[2]],
})
files.sort(key=lambda k: k['time'])
self.write(json.dumps(files))
2021-11-22 19:54:04 +00:00
else:
path = os.path.join(self.config.storage,os.path.basename(filename)+".json_appendable")
2021-11-22 19:54:04 +00:00
drawing = {
"file": filename,
2021-11-22 19:54:04 +00:00
"shape": []
}
with open(path, 'r') as fp:
events = json.loads('['+fp.read()+']')
for i, event in enumerate(events):
2021-11-22 19:54:04 +00:00
if i == 0:
# metadata on first line
drawing['time'] = event[0]
drawing['dimensions'] = [event[1], event[2]]
else:
if event['event'] == 'viewbox':
pass
if event['event'] == 'stroke':
# points = []
# for i in range(int(len(stroke) / 4)):
# p = stroke[i*4:i*4+4]
# points.append([float(p[0]), float(p[1]), int(p[2]), float(p[3])])
drawing['shape'].append({
'color': event['color'],
'points': event['points']
})
2021-11-22 19:54:04 +00:00
self.write(json.dumps(drawing))
def strokes2D(strokes):
# strokes to a d attribute for a path
d = "";
last_stroke = None;
cmd = "";
for stroke in strokes:
if not last_stroke:
d += f"M{stroke[0]},{stroke[1]} "
cmd = 'M'
else:
if last_stroke[2] == 1:
d += " m"
cmd = 'm'
elif cmd != 'l':
d+=' l '
cmd = 'l'
rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += f"{rel_stroke[0]},{rel_stroke[1]} "
last_stroke = stroke
return d
class AnnotationsHandler(tornado.web.RequestHandler):
def initialize(self, config):
self.config = config
self.metadir = os.path.join(self.config.storage, 'metadata')
def prepare(self):
if self.request.headers.get("Content-Type", "").startswith("application/json"):
self.json_args = json.loads(self.request.body)
else:
self.json_args = None
2021-12-22 10:52:38 +00:00
def get_filenames(self):
return [name[:-16] for name in os.listdir(self.config.storage) if name.endswith('json_appendable')]
def get(self, filename):
self.set_header("Content-Type", "application/json")
2021-12-22 10:52:38 +00:00
filenames = self.get_filenames()
print(filenames, filename)
if filename not in filenames:
2021-12-22 10:52:38 +00:00
raise tornado.web.HTTPError(404)
meta_file = os.path.join(self.metadir, filename +'.json')
if not os.path.exists(meta_file):
self.set_status(404)
return
with open(meta_file, 'r') as fp:
self.write(json.load(fp))
def post(self, filename):
# filename = self.get_argument("file", None)
2021-12-22 10:52:38 +00:00
filenames = self.get_filenames()
print(filenames, filename)
if filename not in filenames:
2021-12-22 10:52:38 +00:00
raise tornado.web.HTTPError(404)
if not os.path.exists(self.metadir):
os.mkdir(self.metadir)
meta_file = os.path.join(self.metadir, filename +'.json')
with open(meta_file, 'w') as fp:
json.dump(self.json_args, fp)
2021-11-22 19:54:04 +00:00
class Server:
"""
Server for HIT -> plotter events
As well as for the Status interface
"""
loop = None
def __init__(self, config, logger):
self.config = config
self.logger = logger
#self.config['server']['port']
self.web_root = os.path.join('www')
def start(self):
application = tornado.web.Application([
(r"/ws(.*)", WebSocketHandler, {
'config': self.config,
}),
(r"/files/(.*)", AnimationHandler,
{'config': self.config}),
(r"/audio/(.+)", tornado.web.StaticFileHandler,
{"path": os.path.join(self.config.storage, 'audio')}),
(r"/audio", AudioListingHandler,
{'config': self.config}),
(r"/annotations/(.+)", AnnotationsHandler,
{'config': self.config}),
2021-11-22 19:54:04 +00:00
(r"/(.*)", StaticFileWithHeaderHandler,
{"path": self.web_root}),
], debug=True, autoreload=True)
application.listen(self.config.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == "__main__":
argParser = argparse.ArgumentParser(
description='Start up the vector animation server')
# argParser.add_argument(
# '--config',
# '-c',
# required=True,
# type=str,
# help='The yaml config file to load'
# )
argParser.add_argument(
'--port',
type=int,
default=7890,
help='Port'
)
argParser.add_argument(
'--storage',
type=str,
default='files',
help='directory name for output files'
)
argParser.add_argument(
'--verbose',
'-v',
action='count', default=0
)
args = argParser.parse_args()
loglevel = logging.NOTSET if args.verbose > 1 else logging.DEBUG if args.verbose > 0 else logging.INFO
coloredlogs.install(
level=loglevel,
# default: "%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s"
fmt="%(asctime)s %(hostname)s %(name)s[%(process)d,%(threadName)s] %(levelname)s %(message)s"
)
# File logging
formatter = logging.Formatter(fmt='%(asctime)s %(module)s:%(lineno)d %(levelname)8s | %(message)s',
datefmt='%Y/%m/%d %H:%M:%S') # %I:%M:%S %p AM|PM format
logFileHandler = logging.handlers.RotatingFileHandler(
'log/draw_log.log',
maxBytes=1024*512,
backupCount=5
)
logFileHandler.setFormatter(formatter)
logger = logging.getLogger("sorteerhoed")
logger.addHandler(
logFileHandler
)
2021-12-08 11:40:45 +00:00
logger.info(f"Start server: http://localhost:{args.port}")
2021-11-22 19:54:04 +00:00
server = Server(args, logger)
server.start()