354 lines
12 KiB
Python
354 lines
12 KiB
Python
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
|
|
"""
|
|
# CORS_ORIGINS = ['localhost']
|
|
connections = set()
|
|
|
|
def initialize(self, config):
|
|
self.config = config
|
|
self.strokes = []
|
|
self.hasWritten = False
|
|
self.dimensions = [None, None]
|
|
|
|
# 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
|
|
|
|
# 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=}")
|
|
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))
|
|
|
|
# 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':
|
|
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)
|
|
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))
|
|
|
|
|
|
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 == '':
|
|
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))
|
|
else:
|
|
path = os.path.join(self.config.storage,os.path.basename(filename)+".json_appendable")
|
|
drawing = {
|
|
"file": filename,
|
|
"shape": []
|
|
}
|
|
with open(path, 'r') as fp:
|
|
events = json.loads('['+fp.read()+']')
|
|
for i, event in enumerate(events):
|
|
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']
|
|
})
|
|
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
|
|
|
|
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")
|
|
filenames = self.get_filenames()
|
|
|
|
print(filenames, filename)
|
|
|
|
if filename not in filenames:
|
|
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)
|
|
|
|
filenames = self.get_filenames()
|
|
print(filenames, filename)
|
|
|
|
if filename not in filenames:
|
|
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)
|
|
|
|
|
|
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}),
|
|
(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
|
|
)
|
|
logger.info(f"Start server: http://localhost:{args.port}")
|
|
|
|
server = Server(args, logger)
|
|
server.start()
|
|
|