6
0

WebServer.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. from glob import glob
  2. from json import dumps
  3. from os import path, getcwd
  4. from os.path import exists
  5. from time import time
  6. import eventlet
  7. import socketio
  8. from flask import Flask, make_response, send_from_directory, request
  9. from werkzeug import formparser
  10. from pycs.ApplicationStatus import ApplicationStatus
  11. from pycs.util.GenericWrapper import GenericWrapper
  12. from pycs.util.ProgressFileWriter import ProgressFileWriter
  13. from pycs.util.RecursiveDictionary import set_recursive
  14. class WebServer:
  15. def __init__(self, app_status: ApplicationStatus):
  16. # initialize web server
  17. if exists('webui/index.html'):
  18. print('production build')
  19. # find svg icons and add them as separate static files to
  20. # set their correct mime type / content_type
  21. static_files = {'/': 'webui/'}
  22. for svg_path in glob('webui/img/*.svg'):
  23. svg_path = svg_path.replace('\\', '/')
  24. static_files[svg_path[5:]] = {'content_type': 'image/svg+xml', 'filename': svg_path}
  25. self.__sio = socketio.Server()
  26. self.__flask = Flask(__name__)
  27. self.__app = socketio.WSGIApp(self.__sio, self.__flask, static_files=static_files)
  28. def response(data=None):
  29. if data is None:
  30. return make_response()
  31. else:
  32. return make_response(data)
  33. else:
  34. print('development build')
  35. self.__sio = socketio.Server(cors_allowed_origins='*')
  36. self.__flask = Flask(__name__)
  37. self.__app = socketio.WSGIApp(self.__sio, self.__flask)
  38. def response(data=None):
  39. if data is None:
  40. rsp = make_response()
  41. else:
  42. rsp = make_response(data)
  43. rsp.headers['Access-Control-Allow-Origin'] = '*'
  44. return rsp
  45. # save every change in application status and send it to the client
  46. app_status.subscribe(self.__update_application_status, immediate=True)
  47. # define events
  48. @self.__sio.event
  49. def connect(id, msg):
  50. self.__sio.emit('app_status', {
  51. 'keys': [],
  52. 'value': self.__status
  53. }, to=id)
  54. @self.__flask.route('/settings', methods=['POST'])
  55. def edit_settings():
  56. data = request.get_json(force=True)
  57. set_recursive(data, app_status['settings'])
  58. return response()
  59. @self.__flask.route('/projects', methods=['POST'])
  60. def create_project():
  61. data = request.get_json(force=True)
  62. app_status['projects'].create_project(data['name'], data['description'], data['model'], data['unmanaged'])
  63. return response()
  64. @self.__flask.route('/projects/<identifier>', methods=['POST'])
  65. def edit_project(identifier):
  66. data = request.get_json(force=True)
  67. if 'delete' in data.keys():
  68. app_status['projects'].delete_project(identifier)
  69. elif 'fit' in data.keys():
  70. app_status['projects'].fit(identifier)
  71. elif 'predictAll' in data.keys():
  72. app_status['projects'].predict(identifier)
  73. elif 'predict' in data.keys():
  74. app_status['projects'].predict(identifier, data['predict'])
  75. else:
  76. app_status['projects'].update_project(identifier, data)
  77. return response()
  78. @self.__flask.route('/projects/<identifier>/data', methods=['POST'])
  79. def upload_file(identifier):
  80. # abort if project id is not valid
  81. if identifier not in app_status['projects'].keys():
  82. # TODO return 404
  83. return make_response('project does not exist', 500)
  84. # get project and upload path
  85. project = app_status['projects'][identifier]
  86. upload_path, file_uuid = project.new_media_file_path()
  87. # prepare wrapper objects
  88. job = GenericWrapper()
  89. file_name = GenericWrapper()
  90. file_extension = GenericWrapper()
  91. file_size = GenericWrapper(0)
  92. # save upload to file
  93. def custom_stream_factory(total_content_length, filename, content_type, content_length=None):
  94. file_name.value, file_extension.value = path.splitext(filename)
  95. file_path = path.join(upload_path, f'{file_uuid}{file_extension.value}')
  96. # add job to app status
  97. project['jobs'][file_uuid] = {
  98. 'id': file_uuid,
  99. 'type': 'upload',
  100. 'progress': 0,
  101. 'filename': filename,
  102. 'created': int(time()),
  103. 'finished': None
  104. }
  105. job.value = project['jobs'][file_uuid]
  106. # define progress callback
  107. length = content_length if content_length is not None and content_length != 0 else total_content_length
  108. def callback(progress):
  109. file_size.value += progress
  110. relative = progress / length
  111. if relative - job.value['progress'] > 0.02:
  112. job.value['progress'] = relative
  113. # open file handler
  114. return ProgressFileWriter(file_path, 'wb', callback)
  115. stream, form, files = formparser.parse_form_data(request.environ, stream_factory=custom_stream_factory)
  116. if 'file' not in files.keys():
  117. return make_response('no file uploaded', 500)
  118. # set progress to 1 after upload is done
  119. job = job.value
  120. job['progress'] = 1
  121. job['finished'] = int(time())
  122. # add to project files
  123. project.add_media_file(file_uuid, file_name.value, file_extension.value, file_size.value, job['created'])
  124. # return default success response
  125. return response()
  126. @self.__flask.route('/projects/<project_identifier>/unmanaged/<int:file_number>', methods=['GET'])
  127. def get_unmanaged_file(project_identifier, file_number):
  128. # abort if project id is not valid
  129. if project_identifier not in app_status['projects'].keys():
  130. return make_response('project does not exist', 500)
  131. project = app_status['projects'][project_identifier]
  132. # abort if file id is not valid
  133. if file_number >= len(project.unmanaged_files_keys):
  134. return make_response('file does not exist', 500)
  135. file_identifier = project.unmanaged_files_keys[file_number]
  136. if file_identifier not in project.unmanaged_files_keys:
  137. return make_response('file does not exist', 500)
  138. target_object = project.unmanaged_files[file_identifier]
  139. # return element data
  140. return response(target_object.get_data())
  141. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', defaults={'size': None}, methods=['GET'])
  142. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>/<size>', methods=['GET'])
  143. def get_file(project_identifier, file_identifier, size):
  144. # abort if project id is not valid
  145. if project_identifier not in app_status['projects'].keys():
  146. return make_response('project does not exist', 500)
  147. project = app_status['projects'][project_identifier]
  148. # abort if file id is not valid
  149. target_object = project.get_media_file(file_identifier)
  150. if target_object is None:
  151. return make_response('file does not exist', 500)
  152. # resize image to requested size
  153. if size is not None:
  154. target_object = target_object.resize(size)
  155. # construct directory and filename
  156. file_directory = path.join(getcwd(), target_object.directory)
  157. file_name = target_object.full_name
  158. # return data
  159. return send_from_directory(file_directory, file_name)
  160. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', methods=['POST'])
  161. def add_result(project_identifier, file_identifier):
  162. # abort if project id is not valid
  163. if project_identifier not in app_status['projects'].keys():
  164. return make_response('project does not exist', 500)
  165. project = app_status['projects'][project_identifier]
  166. # abort if file id is not valid
  167. target_object = project.get_media_file(file_identifier)
  168. if target_object is None:
  169. return make_response('file does not exist', 500)
  170. # add result
  171. result = request.get_json(force=True)
  172. if result:
  173. if 'delete' in result:
  174. project.remove_media_file(file_identifier)
  175. elif 'reset' in result:
  176. target_object.remove_results()
  177. elif 'x' not in result:
  178. if result['label']:
  179. target_object.add_global_result(result)
  180. else:
  181. target_object.remove_global_result()
  182. else:
  183. target_object.add_result(result)
  184. # return default success response
  185. return response()
  186. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>/<result_identifier>', methods=['POST'])
  187. def edit_result(project_identifier, file_identifier, result_identifier):
  188. # abort if project id is not valid
  189. if project_identifier not in app_status['projects'].keys():
  190. return make_response('project does not exist', 500)
  191. project = app_status['projects'][project_identifier]
  192. # abort if file id is not valid
  193. target_object = project.get_media_file(file_identifier)
  194. if target_object is None:
  195. return make_response('file does not exist', 500)
  196. # parse post data
  197. result = request.get_json(force=True)
  198. if result:
  199. # remove result
  200. if 'delete' in result.keys():
  201. target_object.remove_result(result_identifier)
  202. # update result
  203. else:
  204. target_object.update_result(result_identifier, result)
  205. # return default success response
  206. return response()
  207. @self.__flask.route('/projects/<project_identifier>/labels', methods=['POST'])
  208. def create_label(project_identifier):
  209. # abort if project id is not valid
  210. if project_identifier not in app_status['projects'].keys():
  211. return make_response('project does not exist', 500)
  212. project = app_status['projects'][project_identifier]
  213. # add result
  214. result = request.get_json(force=True)
  215. if result:
  216. project.add_label(result['name'])
  217. # return default success response
  218. return response()
  219. @self.__flask.route('/projects/<project_identifier>/labels/<label_identifier>', methods=['POST'])
  220. def edit_label(project_identifier, label_identifier):
  221. # abort if project id is not valid
  222. if project_identifier not in app_status['projects'].keys():
  223. return make_response('project does not exist', 500)
  224. project = app_status['projects'][project_identifier]
  225. # parse post data
  226. result = request.get_json(force=True)
  227. if result:
  228. # remove label
  229. if 'delete' in result.keys():
  230. project.remove_label(label_identifier)
  231. # update label
  232. else:
  233. project.update_label(label_identifier, result['name'])
  234. # return default success response
  235. return response()
  236. @self.__flask.route('/projects/<project_identifier>/predictions', methods=['GET'])
  237. def download_predictions(project_identifier):
  238. # abort if project id is not valid
  239. if project_identifier not in app_status['projects'].keys():
  240. return make_response('project does not exist', 404)
  241. project = app_status['projects'][project_identifier]
  242. # create export
  243. result = []
  244. def mk_obj(type, name, extension, predictions):
  245. data_res = {
  246. 'type': type,
  247. 'filename': name + extension,
  248. 'predictions': []
  249. }
  250. for result_key in predictions:
  251. result_obj = predictions[result_key]
  252. data_res['predictions'].append(result_obj)
  253. return data_res
  254. for data_key in project['data']:
  255. data_obj = project['data'][data_key]
  256. result.append(mk_obj(
  257. data_obj['type'],
  258. data_obj['name'],
  259. data_obj['extension'],
  260. data_obj['predictionResults']
  261. ))
  262. for data_key in project.unmanaged_files:
  263. data_obj = project.unmanaged_files[data_key].get_data()
  264. result.append(mk_obj(
  265. data_obj['type'],
  266. data_obj['id'],
  267. data_obj['extension'],
  268. data_obj['predictionResults']
  269. ))
  270. # send to user
  271. rsp = make_response(dumps(result))
  272. rsp.headers['Content-Type'] = 'text/json;charset=UTF-8'
  273. rsp.headers['Content-Disposition'] = 'attachment;filename=predictions.json'
  274. return rsp
  275. # finally start web server
  276. host = app_status['settings']['frontend']['host']
  277. port = app_status['settings']['frontend']['port']
  278. eventlet.wsgi.server(eventlet.listen((host, port)), self.__app)
  279. def __update_application_status(self, status, keys):
  280. value = status
  281. for key in keys[:-1]:
  282. value = value[key]
  283. self.__status = status
  284. self.__sio.emit('app_status', {
  285. 'keys': keys[:-1],
  286. 'value': value
  287. })