141 - Support for Package Management in SO
[osm/SO.git] / rwlaunchpad / plugins / rwstagingmgr / rift / tasklets / rwstagingmgr / server / handler.py
diff --git a/rwlaunchpad/plugins/rwstagingmgr/rift/tasklets/rwstagingmgr/server/handler.py b/rwlaunchpad/plugins/rwstagingmgr/rift/tasklets/rwstagingmgr/server/handler.py
new file mode 100644 (file)
index 0000000..ce26e06
--- /dev/null
@@ -0,0 +1,148 @@
+#
+#   Copyright 2016 RIFT.IO Inc
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# Author(s): Varun Prasad
+# Creation Date: 09/28/2016
+#
+
+import tornado.httpclient
+import tornado.web
+import tornadostreamform.multipart_streamer as multipart_streamer
+import logging
+import os
+
+MB = 1024 * 1024
+GB = 1024 * MB
+
+MAX_STREAMED_SIZE = 5 * GB
+
+class HttpMessageError(Exception):
+    def __init__(self, code, msg):
+        self.code = code
+        self.msg = msg
+
+
+class RequestHandler(tornado.web.RequestHandler):
+    def options(self, *args, **kargs):
+        pass
+
+    def set_default_headers(self):
+        self.set_header('Access-Control-Allow-Origin', '*')
+        self.set_header('Access-Control-Allow-Headers',
+                        'Content-Type, Cache-Control, Accept, X-Requested-With, Authorization')
+        self.set_header('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE')
+
+
+class StoreStreamerPart(multipart_streamer.MultiPartStreamer):
+    """
+    Create a Part streamer with a custom temp directory. Using the default
+    tmp directory and trying to move the file to $RIFT_ARTIFACTS occasionally
+    causes link errors. So create a temp directory within the staging area.
+    """
+    def __init__(self, store, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.store = store
+
+    def create_part(self, headers):
+        return multipart_streamer.TemporaryFileStreamedPart(self, headers, tmp_dir=self.store.tmp_dir)
+
+
+@tornado.web.stream_request_body
+class UploadStagingHandler(RequestHandler):
+    def initialize(self, store):
+        """Initialize the handler
+
+        Arguments:
+            log  - the logger that this handler should use
+            loop - the tasklets ioloop
+
+        """
+        self.log = logging.getLogger()
+        self.store = store
+
+        self.part_streamer = None
+
+    @tornado.gen.coroutine
+    def prepare(self):
+        """Prepare the handler for a request
+
+        The prepare function is the first part of a request transaction. It
+        creates a temporary file that uploaded data can be written to.
+
+        """
+        if self.request.method != "POST":
+            return
+
+        self.request.connection.set_max_body_size(MAX_STREAMED_SIZE)
+
+        # Retrieve the content type and parameters from the request
+        content_type = self.request.headers.get('content-type', None)
+        if content_type is None:
+            raise tornado.httpclient.HTTPError(400, "No content type set")
+
+        content_type, params = tornado.httputil._parse_header(content_type)
+
+        if "multipart/form-data" != content_type.lower():
+            raise tornado.httpclient.HTTPError(415, "Invalid content type")
+
+        # You can get the total request size from the headers.
+        try:
+            total = int(self.request.headers.get("Content-Length", "0"))
+        except KeyError:
+            self.log.warning("Content length header not found")
+            # For any well formed browser request, Content-Length should have a value.
+            total = 0
+
+        # And here you create a streamer that will accept incoming data
+        self.part_streamer = StoreStreamerPart(self.store, total)
+
+
+    @tornado.gen.coroutine
+    def data_received(self, chunk):
+        """When a chunk of data is received, we forward it to the multipart streamer."""
+        self.part_streamer.data_received(chunk)
+
+    def post(self, staging_id):
+        """Handle a post request
+
+        The function is called after any data associated with the body of the
+        request has been received.
+
+        """
+        try:
+            # You MUST call this to close the incoming stream.
+            self.part_streamer.data_complete()
+            desc_parts = self.part_streamer.get_parts_by_name("file")
+            if len(desc_parts) != 1:
+                raise tornado.httpclient.HTTPError(400, "File option not found")
+
+            binary_data = desc_parts[0]
+            staging_area = self.store.get_staging_area(staging_id)
+            filename = binary_data.get_filename()
+            staging_area.model.name = filename
+            staging_area.model.size = binary_data.get_size()
+
+            dest_file = os.path.join(staging_area.model.path, filename)
+            binary_data.move(dest_file)
+
+            self.set_status(200)
+            self.write(tornado.escape.json_encode({
+                "path": "/api/download/{}/{}".format(staging_id, filename)
+                    }))
+
+        finally:
+            self.part_streamer.release_parts()
+            self.finish()
+