소스 검색

Merge branch '22-use-post-or-put-to-specific-paths-to-trigger-actions' into 'master'

Resolve "use post or put to specific paths to trigger actions"

Closes #37 and #22

See merge request troebs/pycs!30
Eric Tröbs 4 년 전
부모
커밋
7ccd21812f

+ 0 - 117
pycs/frontend/FileProvider.py

@@ -1,117 +0,0 @@
-from os import path, mkdir, getcwd
-from time import time
-from uuid import uuid1
-
-from flask import Flask, make_response, send_from_directory, request
-from werkzeug import formparser
-
-from pycs.ApplicationStatus import ApplicationStatus
-from pycs.util.GenericWrapper import GenericWrapper
-from pycs.util.ProgressFileWriter import ProgressFileWriter
-
-
-class FileProvider(Flask):
-    def __init__(self, app_status: ApplicationStatus, cors=False):
-        super().__init__(__name__)
-
-        # add download handler
-        @self.route('/media/<identifier>', methods=['GET'])
-        def media(identifier):
-            # get current project
-            opened_projects = list(filter(lambda x: x['status'] == 'open', app_status['projects']))
-            if len(opened_projects) == 0:
-                return make_response('no open project available', 500)
-
-            current_project = opened_projects[0]
-            file_directory = path.join(getcwd(), 'projects', current_project['id'], 'data')
-
-            # get object
-            data_objects = list(filter(lambda x: x['id'] == identifier, current_project['data']))
-            if len(data_objects) == 0:
-                return make_response('data object not avilable', 500)
-
-            target_object = data_objects[0]
-
-            # return data
-            file_name = target_object['id'] + target_object['extension']
-            return send_from_directory(file_directory, file_name)
-
-        # add upload handler
-        @self.route('/upload', methods=['POST'])
-        def upload():
-            file_uuid = str(uuid1())
-
-            # get current project path
-            opened_projects = list(filter(lambda x: x['status'] == 'open', app_status['projects']))
-            if len(opened_projects) == 0:
-                return make_response('no open project available', 500)
-
-            current_project = opened_projects[0]
-            upload_path = path.join('projects', current_project['id'], 'data')
-
-            job = GenericWrapper()
-            file_name = GenericWrapper()
-            file_extension = GenericWrapper()
-            file_size = GenericWrapper(0)
-
-            # save upload to file
-            def custom_stream_factory(total_content_length, filename, content_type, content_length=None):
-                file_name.value, file_extension.value = path.splitext(filename)
-                file_path = path.join(upload_path, f'{file_uuid}{file_extension.value}')
-
-                # add job to app status
-                job.value = app_status['jobs'].append({
-                    'id': file_uuid,
-                    'type': 'upload',
-                    'progress': 0,
-                    'filename': filename,
-                    'created': int(time()),
-                    'finished': None
-                })
-
-                # create upload path if not exists
-                if not path.exists(upload_path):
-                    mkdir(upload_path)
-
-                # define progress callback
-                length = content_length if content_length is not None and content_length != 0 else total_content_length
-
-                def callback(progress):
-                    file_size.value += progress
-                    relative = progress / length
-
-                    if relative - job.value['progress'] > 0.02:
-                        job.value['progress'] = relative
-
-                # open file handler
-                return ProgressFileWriter(file_path, 'wb', callback)
-
-            stream, form, files = formparser.parse_form_data(request.environ, stream_factory=custom_stream_factory)
-
-            if 'file' not in files.keys():
-                return make_response('no file uploaded', 500)
-
-            # set progress to 1 after upload is done
-            job = job.value
-
-            job['progress'] = 1
-            job['finished'] = int(time())
-
-            # add to project files
-            if 'data' not in current_project:
-                current_project['data'] = []
-
-            current_project['data'].append({
-                'id': file_uuid,
-                'name': file_name.value,
-                'extension': file_extension.value,
-                'size': file_size.value,
-                'created': job['created']
-            })
-
-            # create response and allow cors if needed
-            response = make_response()
-            if cors:
-                response.headers['Access-Control-Allow-Origin'] = '*'
-
-            return response

+ 172 - 29
pycs/frontend/WebServer.py

@@ -1,11 +1,18 @@
 from glob import glob
+from os import path, mkdir, getcwd
 from os.path import exists
+from time import time
+from uuid import uuid1
 
 import eventlet
 import socketio
+from flask import Flask, make_response, send_from_directory, request
+from werkzeug import formparser
 
 from pycs.ApplicationStatus import ApplicationStatus
-from pycs.frontend.FileProvider import FileProvider
+from pycs.util.GenericWrapper import GenericWrapper
+from pycs.util.ProgressFileWriter import ProgressFileWriter
+from pycs.util.RecursiveDictionary import set_recursive
 
 
 class WebServer:
@@ -13,22 +20,38 @@ class WebServer:
         # initialize web server
         if exists('webui/index.html'):
             print('production build')
-            files = FileProvider(app_status)
+
+            # TODO update file upload
+            # files = FileProvider(app_status)
 
             # find svg icons and add them as separate static files to
             # set their correct mime type / content_type
             static_files = {'/': 'webui/'}
-            for path in glob('webui/img/*.svg'):
-                path = path.replace('\\', '/')
-                static_files[path[5:]] = {'content_type': 'image/svg+xml', 'filename': path}
+            for svg_path in glob('webui/img/*.svg'):
+                svg_path = svg_path.replace('\\', '/')
+                static_files[svg_path[5:]] = {'content_type': 'image/svg+xml', 'filename': svg_path}
 
             self.__sio = socketio.Server()
-            self.__app = socketio.WSGIApp(self.__sio, files, static_files=static_files)
+            self.__flask = Flask(__name__)
+            self.__app = socketio.WSGIApp(self.__sio, self.__flask, static_files=static_files)
+
+            def response():
+                rsp = make_response()
+                return rsp
         else:
             print('development build')
-            files = FileProvider(app_status, cors=True)
+
+            # TODO update file upload
+            # files = FileProvider(app_status, cors=True)
+
             self.__sio = socketio.Server(cors_allowed_origins='*')
-            self.__app = socketio.WSGIApp(self.__sio, files)
+            self.__flask = Flask(__name__)
+            self.__app = socketio.WSGIApp(self.__sio, self.__flask)
+
+            def response():
+                rsp = make_response()
+                rsp.headers['Access-Control-Allow-Origin'] = '*'
+                return rsp
 
         # save every change in application status and send it to the client
         app_status.subscribe(self.__update_application_status, immediate=True)
@@ -36,29 +59,149 @@ class WebServer:
         # define events
         @self.__sio.event
         def connect(id, msg):
+            print('connect')
             self.__sio.emit('app_status', self.__status, to=id)
 
-        @self.__sio.on('set')
-        def set_status(id, msg):
-            path = msg['path'].split('/')
-            value = msg['value']
-
-            obj = app_status
-            for p in path[:-1]:
-                obj = obj[p]
-
-            obj[path[-1]] = value
-
-        @self.__sio.on('add')
-        def add_status(id, msg):
-            path = msg['path'].split('/')
-            value = msg['value']
-
-            obj = app_status
-            for p in path[:-1]:
-                obj = obj[p]
-
-            obj[path[-1]].append(value)
+        @self.__flask.route('/settings', methods=['POST'])
+        def edit_settings():
+            data = request.get_json(force=True)
+            set_recursive(data, app_status['settings'])
+
+            response = make_response()
+            response.headers['Access-Control-Allow-Origin'] = '*'
+            return response
+
+        @self.__flask.route('/projects', methods=['POST'])
+        def create_project():
+            data = request.get_json(force=True)
+
+            # TODO move to project manager
+            app_status['projects'].append({
+                'status': 'create',
+                'name': data['name'],
+                'description': data['description'],
+                'model': data['model']
+            })
+
+            response = make_response()
+            response.headers['Access-Control-Allow-Origin'] = '*'
+            return response
+
+        @self.__flask.route('/projects/<identifier>', methods=['POST'])
+        def edit_project(identifier):
+            data = request.get_json(force=True)
+
+            # TODO move to project manager
+            for project in app_status['projects']:
+                if project['id'] == identifier:
+                    # delete
+                    if 'delete' in data.keys():
+                        project['action'] = 'delete'
+                    # update
+                    else:
+                        set_recursive(data, project)
+                        project['action'] = 'update'
+
+            return response()
+
+        @self.__flask.route('/projects/<identifier>/data', methods=['POST'])
+        def upload_file(identifier):
+            # TODO move to project manager
+            file_uuid = str(uuid1())
+
+            # get current project path
+            opened_projects = list(filter(lambda x: x['id'] == identifier, app_status['projects']))
+            if len(opened_projects) == 0:
+                return make_response('no open project available', 500)
+
+            current_project = opened_projects[0]
+            upload_path = path.join('projects', current_project['id'], 'data')
+
+            job = GenericWrapper()
+            file_name = GenericWrapper()
+            file_extension = GenericWrapper()
+            file_size = GenericWrapper(0)
+
+            # save upload to file
+            def custom_stream_factory(total_content_length, filename, content_type, content_length=None):
+                file_name.value, file_extension.value = path.splitext(filename)
+                file_path = path.join(upload_path, f'{file_uuid}{file_extension.value}')
+
+                # add job to app status
+                job.value = app_status['jobs'].append({
+                    'id': file_uuid,
+                    'type': 'upload',
+                    'progress': 0,
+                    'filename': filename,
+                    'created': int(time()),
+                    'finished': None
+                })
+
+                # create upload path if not exists
+                if not path.exists(upload_path):
+                    mkdir(upload_path)
+
+                # define progress callback
+                length = content_length if content_length is not None and content_length != 0 else total_content_length
+
+                def callback(progress):
+                    file_size.value += progress
+                    relative = progress / length
+
+                    if relative - job.value['progress'] > 0.02:
+                        job.value['progress'] = relative
+
+                # open file handler
+                return ProgressFileWriter(file_path, 'wb', callback)
+
+            stream, form, files = formparser.parse_form_data(request.environ, stream_factory=custom_stream_factory)
+
+            if 'file' not in files.keys():
+                return make_response('no file uploaded', 500)
+
+            # set progress to 1 after upload is done
+            job = job.value
+
+            job['progress'] = 1
+            job['finished'] = int(time())
+
+            # add to project files
+            if 'data' not in current_project:
+                current_project['data'] = []
+
+            current_project['data'].append({
+                'id': file_uuid,
+                'name': file_name.value,
+                'extension': file_extension.value,
+                'size': file_size.value,
+                'created': job['created']
+            })
+
+            # return default success response
+            return response()
+
+        @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', methods=['GET'])
+        def get_file(project_identifier, file_identifier):
+            # get current project
+            opened_projects = list(filter(lambda x: x['id'] == project_identifier, app_status['projects']))
+            if len(opened_projects) == 0:
+                return make_response('no open project available', 500)
+
+            current_project = opened_projects[0]
+            file_directory = path.join(getcwd(), 'projects', current_project['id'], 'data')
+
+            print(current_project)
+
+            # get object
+            data_objects = list(filter(lambda x: x['id'] == file_identifier, current_project['data']))
+            if len(data_objects) == 0:
+                return make_response('data object not avilable', 500)
+
+            target_object = data_objects[0]
+
+            # return data
+            file_name = target_object['id'] + target_object['extension']
+            return send_from_directory(file_directory, file_name)
 
         # finally start web server
         host = app_status['settings']['frontend']['host']

+ 6 - 0
pycs/util/RecursiveDictionary.py

@@ -0,0 +1,6 @@
+def set_recursive(source_object, target_object):
+    for key in source_object.keys():
+        if isinstance(source_object[key], dict):
+            set_recursive(source_object[key], target_object[key])
+        else:
+            target_object[key] = source_object[key]

+ 39 - 21
webui/src/App.vue

@@ -11,8 +11,8 @@
       <side-navigation-bar :window="window"
                            :socket="socket"
                            :status="status"
-                           :project-path="currentProjectPath"
-                           v-on:close="window.menu = false"></side-navigation-bar>
+                           :current-project="currentProject"
+                           v-on:close="closeProject"></side-navigation-bar>
 
       <!-- actual content -->
       <div class="content">
@@ -20,17 +20,20 @@
                              :projects="projects"
                              :status="status"
                              :socket="socket"
-                             v-on:open="show('settings')"/>
+                             :window="window"
+                             v-on:open="openProject"/>
 
         <template v-else>
           <project-settings-window v-if="window.content === 'settings'"
                                    :current-project="currentProject"
-                                   :current-project-path="currentProjectPath"
                                    :status="status"
-                                   :socket="socket"/>
+                                   :socket="socket"
+                                   v-on:close="closeProject"/>
 
           <project-data-window v-if="window.content === 'add_data'"
-                               :status="status"/>
+                               :current-project="currentProject"
+                               :status="status"
+                               :socket="socket"/>
 
           <about-window v-if="window.content === 'about'"/>
         </template>
@@ -60,14 +63,25 @@ export default {
   },
   data: function() {
     return {
-      // initialize socket.io connection
       socket: {
-        io: io('http://' + window.location.hostname + ':5000'),
-        set: function(path, value) {
-          this.io.emit('set', {path: path, value: value});
+        baseurl: window.location.protocol + '//' + window.location.hostname + ':5000',
+        // initialize socket.io connection
+        io: io(window.location.protocol + '//' + window.location.hostname + ':5000'),
+        // http methods
+        post: function(name, value) {
+          return fetch(this.baseurl + name, {
+            method: 'POST',
+            body: JSON.stringify(value)
+          });
         },
-        add: function(path, value) {
-          this.io.emit('add', {path: path, value: value});
+        upload: function(name, file) {
+          const form = new FormData();
+          form.append('file', file);
+
+          return fetch(this.baseurl + name, {
+            method: 'POST',
+            body: form
+          });
         }
       },
       status: null,
@@ -75,6 +89,7 @@ export default {
         wide: true,
         menu: false,
         content: 'settings',
+        project: null
       }
     }
   },
@@ -84,16 +99,9 @@ export default {
     },
     currentProject: function() {
       for (let i = 0; i < this.projects.length; i++)
-        if (this.projects[i].status === 'open')
+        if (this.projects[i].id === this.window.project)
           return this.projects[i];
 
-      return false;
-    },
-    currentProjectPath: function() {
-      for (let i = 0; i < this.projects.length; i++)
-        if (this.projects[i].status === 'open')
-          return 'projects/' + i;
-
       return false;
     }
   },
@@ -103,10 +111,20 @@ export default {
     },
     show: function(value) {
       this.window.content = value;
+    },
+    openProject: function(project) {
+      this.window.project = project.id;
+      this.show('settings');
+    },
+    closeProject: function() {
+      this.window.project = false;
+      this.window.menu = false;
     }
   },
   created: function() {
-    this.socket.io.on('app_status', status => this.status = status);
+    this.socket.io.on('app_status', status => {
+      this.status = status;
+    });
 
     window.addEventListener('resize', this.resize);
     this.resize();

+ 7 - 10
webui/src/components/base/file-input.vue

@@ -8,7 +8,7 @@
            multiple
            @change="change">
 
-    <img src="@/assets/icons/package-dependencies.svg">
+    <img alt="file upload" src="@/assets/icons/package-dependencies.svg">
     <div class="text">
       Click or drop a file here to upload it to the current project directory.
     </div>
@@ -18,7 +18,7 @@
 <script>
 export default {
   name: "file-input",
-  props: ['url'],
+  props: ['socket', 'name'],
   data: function() {
     return {
       drag: false
@@ -62,16 +62,13 @@ export default {
     },
     upload: function(files) {
       for (let file of files) {
-        const form = new FormData();
-        // form.append('size', file.size);
-        form.append('file', file);
-
-        fetch(this.url, {
-          method: 'POST',
-          body: form
-        })
+        console.log(this.socket.upload);
+        console.log(this.name);
+        this.socket.upload(this.name, file);
+        /* TODO then / catch
             .then(() => console.log('done'))
             .catch(e => console.log(e));
+         */
       }
     }
   }

+ 6 - 9
webui/src/components/projects/project-creation-window.vue

@@ -79,15 +79,12 @@ export default {
       if (this.nameError || this.descriptionError)
         return;
 
-      this.socket.add(
-          'projects',
-          {
-            status: 'create',
-            name: this.name,
-            description: this.description,
-            model: this.model
-          }
-      );
+      // TODO then / error
+      this.socket.post('/projects', {
+        name: this.name,
+        description: this.description,
+        model: this.model
+      });
       this.$emit('cancel', null);
     }
   }

+ 2 - 7
webui/src/components/projects/project-data-window.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="project-data-window">
     <!-- TODO use valid url -->
-    <file-input :url="url"></file-input>
+    <file-input :socket="socket" :name="'/projects/' + currentProject.id + '/data'"></file-input>
 
     <template v-if="uploads.length > 0">
       <h1>Uploads</h1>
@@ -23,12 +23,7 @@ import ProgressBar from "@/components/base/progress-bar";
 export default {
   name: "project-data-window",
   components: {ProgressBar, FileInput},
-  props: ['status'],
-  data: function() {
-    return {
-      'url': 'http://' + window.location.hostname + ':5000/upload'
-    }
-  },
+  props: ['status', 'socket', 'currentProject'],
   computed: {
     uploads: function() {
       const filtered = this.status.jobs.filter(x => x.type === 'upload');

+ 1 - 1
webui/src/components/projects/project-open-window.vue

@@ -68,7 +68,7 @@ export default {
   },
   methods: {
     load: function(index) {
-      this.socket.set('projects/' + index + '/status', 'load');
+      this.$emit('open', this.projects[index]);
     },
     datetime: function(timestamp) {
       const date = new Date(timestamp * 1000);

+ 21 - 5
webui/src/components/projects/project-settings-window.vue

@@ -46,7 +46,7 @@ import ButtonInput from "@/components/base/button-input";
 export default {
   name: "project-settings-window",
   components: {ButtonInput, ConfirmedButtonInput, TextareaInput, TextInput},
-  props: ['currentProject', 'currentProjectPath', 'socket'],
+  props: ['currentProject', 'socket'],
   data: function() {
     return {
       project: null,
@@ -56,20 +56,36 @@ export default {
       descriptionError: false
     }
   },
+  computed: {
+    path: function() {
+      return '/projects/' + this.currentProject.id;
+    }
+  },
   methods: {
     nameF: function(value) {
-      this.socket.set(this.currentProjectPath + '/name', value);
+      // TODO then / error
+      this.socket.post(this.path, {
+        'name': value
+      });
       this.update();
     },
     descriptionF: function(value) {
-      this.socket.set(this.currentProjectPath + '/description', value);
+      // TODO then / error
+      this.socket.post(this.path, {
+        'description': value
+      });
       this.update();
     },
     update: function() {
-      this.socket.set(this.currentProjectPath + '/action', 'update');
+      // TODO is this function actually needed?
+      // this.socket.set(/* this.currentProjectPath + */ '/action', 'update');
     },
     deleteProject: function() {
-      this.socket.set(this.currentProjectPath + '/action', 'delete');
+      // TODO then / error
+      this.socket.post(this.path, {
+        delete: true
+      });
+      this.$emit('close', true);
     }
   },
   created: function() {

+ 13 - 10
webui/src/components/window/side-navigation-bar.vue

@@ -9,28 +9,28 @@
     <div class="items">
       <!-- project settings -->
       <div class="item"
-           :class="{active: window.content === 'settings', inactive: !projectPath}"
+           :class="{active: window.content === 'settings', inactive: !currentProject}"
            @click="ifProjectIsOpened(show, 'settings')">
         <img src="@/assets/icons/gear.svg">
         <span>Project Settings</span>
       </div>
 
       <div class="item"
-           :class="{active: window.content === 'add_data', inactive: !projectPath}"
+           :class="{active: window.content === 'add_data', inactive: !currentProject}"
            @click="ifProjectIsOpened(show, 'add_data')">
         <img src="@/assets/icons/package-dependencies.svg">
         <span>Add Data</span>
       </div>
 
       <div class="item"
-           :class="{inactive: !projectPath}"
+           :class="{inactive: !currentProject}"
            @click="close">
         <img src="@/assets/icons/sign-in.svg">
         <span>Close</span>
       </div>
 
       <div class="item"
-           :class="{active: window.content === 'about', inactive: !projectPath}"
+           :class="{active: window.content === 'about', inactive: !currentProject}"
            @click="ifProjectIsOpened(show, 'about')">
         <img src="@/assets/icons/info.svg">
         <span>About PyCS</span>
@@ -55,7 +55,7 @@
 <script>
 export default {
   name: "side-navigation-bar",
-  props: ['window', 'socket', 'status', 'projectPath'],
+  props: ['window', 'socket', 'status', 'currentProject'],
   computed: {
     collapsed: function() {
       if (!this.status)
@@ -66,21 +66,24 @@ export default {
   },
   methods: {
     ifProjectIsOpened: function(fun, ...args) {
-      if (this.projectPath)
+      if (this.currentProject)
         fun.bind(fun)(...args);
     },
     show: function(value) {
       this.window.content = value;
-      this.$emit('close', null);
     },
     close: function() {
-      if (this.projectPath) {
-        this.socket.set(this.projectPath + '/status', 'save');
+      if (this.currentProject) {
         this.$emit('close', null);
       }
     },
     collapse: function() {
-      this.socket.set('settings/frontend/collapse', !this.collapsed);
+      this.socket.post('/settings', {
+        frontend: {
+          collapse: !this.collapsed
+        }
+      });
+      // TODO then / error
     }
   }
 }