diff bitten/master.py @ 835:59acaa8b52c0

Slave attachment support via `<attach />` is totally redone to use multi-part form post instead of inlining it in the XML (ie. like a web file upload form). For larger binaries the previous inlining would effectively be an internal denial-of-service attack... Closes #615.
author osimons
date Sun, 10 Oct 2010 20:18:53 +0000
parents 7c80375d4817
children f320205ca1f9
line wrap: on
line diff
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -131,6 +131,8 @@
 
         if req.args['collection'] == 'steps':
             return self._process_build_step(req, config, build)
+        elif req.args['collection'] == 'attach':
+            return self._process_attachment(req, config, build)
         elif req.args['collection'] == 'keepalive':
             return self._process_keepalive(req, config, build)
         else:
@@ -258,6 +260,7 @@
         target_platform = TargetPlatform.fetch(self.env, build.platform)
         xml.attr['platform'] = target_platform.name
         xml.attr['name'] = build.slave
+        xml.attr['form_token'] = req.form_token # For posting attachments
         body = str(xml)
 
         self.log.info('Build slave %r initiated build %d', build.slave,
@@ -349,25 +352,6 @@
                 report.items.append(item)
             report.insert(db=db)
 
-        # Collect attachments from the request body
-        for attach_elem in elem.children(Recipe.ATTACH):
-            attach_elem = list(attach_elem.children('file'))[0] # One file only
-            filename = attach_elem.attr.get('filename')
-            resource_id = attach_elem.attr.get('resource') == 'config' \
-                                    and build.config or build.resource.id
-
-            try: # Delete attachment if it already exists
-                old_attach = Attachment(self.env, 'build',
-                                    parent_id=resource_id, filename=filename)
-                old_attach.delete()
-            except ResourceNotFound:
-                pass
-            attachment = Attachment(self.env, 'build', parent_id=resource_id)
-            attachment.description = attach_elem.attr.get('description')
-            attachment.author = req.authname
-            fileobj = StringIO(attach_elem.gettext().decode('base64'))
-            attachment.insert(filename, fileobj, fileobj.len, db=db)
-
         # If this was the last step in the recipe we mark the build as
         # completed otherwise just update last_activity
         if last_step:
@@ -418,6 +402,38 @@
                             'Location': req.abs_href.builds(
                                     build.id, 'steps', stepname)})
 
+    def _process_attachment(self, req, config, build):
+        resource_id = req.args['member'] == 'config' \
+                    and build.config or build.resource.id
+        upload = req.args['file']
+        if not upload.file:
+            send_error(req, message="Attachment not received.")
+        self.log.debug('Received attachment %s for attaching to build:%s',
+                      upload.filename, resource_id)
+
+        # Determine size of file
+        upload.file.seek(0, 2) # to the end
+        size = upload.file.tell()
+        upload.file.seek(0)    # beginning again
+
+        # Delete attachment if it already exists
+        try:
+            old_attach = Attachment(self.env, 'build',
+                            parent_id=resource_id, filename=upload.filename)
+            old_attach.delete()
+        except ResourceNotFound:
+            pass
+
+        # Save new attachment
+        attachment = Attachment(self.env, 'build', parent_id=resource_id)
+        attachment.description = req.args.get('description', '')
+        attachment.author = req.authname
+        attachment.insert(upload.filename, upload.file, size)
+
+        self._send_response(req, 201, 'Attachment created', headers={
+                            'Content-Type': 'text/plain',
+                            'Content-Length': str(len('Attachment created'))})
+
     def _process_keepalive(self, req, config, build):
         build.last_activity = int(time.time())
         build.update()
@@ -430,7 +446,6 @@
                             'Content-Type': 'text/plain',
                             'Content-Length': str(len(body))})
 
-
     def _start_new_step(self, build, stepname):
         """Creates the in-memory representation for a newly started
         step, ready to be persisted to the database.
Copyright (C) 2012-2017 Edgewall Software