WebServer.py 12 KB

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