Scriptrunner REST Endpoint MultiPart Form Data

Daniel Garcia July 12, 2017

I'm trying to upload a file to a scriptrunner REST custom end point. The file upload is being initiated from a dialog box with the following code

 

(function ($) {
    $("#dialog-close-button").click(function (e) {
        e.preventDefault();
        AJS.dialog2("#img-dialog").hide();
    });
    $("#img-form").submit(function(e) {
        var fd = new FormData($("#img-form").get(0));
        $.ajax({
            type: "POST",
            data: fd,
            url: "/rest/scriptrunner/latest/custom/adminImgUploadDialogSave",
            dataType   : 'json',
            processData: false,
            contentType: false,
            headers: {
                'Content-Type' : 'multipart/form-data',
                'X-Atlassian-Token': 'no-check',
                'Access-Control-Allow-Origin' : '*'
            },
        }).done(function(response) {
            if (response.status=="ok") {
                AJS.dialog2("#img-dialog").hide();
            }
        });
        e.preventDefault();
        return false;
    });
})(AJS.$);

In the rest endpoint I have

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import groovy.json.JsonBuilder
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import javax.servlet.http.HttpServletRequest

@BaseScript CustomEndpointDelegate delegate

adminImgUploadDialogSave(
    httpMethod: "POST", groups: ["jira-administrators"]
) { MultivaluedMap queryParams, String body, HttpServletRequest req ->
    log.error("adminImgUploadDialogSave")
    log.error(body)
    log.error(req.getParts().toString())
    
    //if (filename==null) {
    //    return Response.ok(new JsonBuilder([status: 'not ok']).toString()).build()
    //} else {
    //    do stuff
    //    return Response.ok(new JsonBuilder([status: 'ok']).toString()).build()
    //}
}

body contains what I would expect

-----------------------------183231904016123
    Content-Disposition: form-data; name="atl_token"
    
    BD3R-R2T8-HXRR-WMXA|b2e4ac8fc23366985d8cb8d8b7e9e128d47a5c0b|lin
    -----------------------------183231904016123
    Content-Disposition: form-data; name="image"; filename="nodetype-child.png"
    Content-Type: image/png
    
    lots of binary data
    -----------------------------183231904016123--

but req.getParts() always returns an empty list.

How do I load/parse the form data?

 

 

 

 

 

3 answers

2 votes
Slava Dobromyslov March 26, 2023

First of all, I created a new class `StringMultiPartFormParser ` which parses the form and places all the plain form parameters into `parameters` and all files into `files`.

package mypackage

import
groovy.transform.CompileStatic
import org.apache.commons.fileupload.FileItem
import org.apache.commons.fileupload.FileUpload
import org.apache.commons.fileupload.UploadContext
import org.apache.commons.fileupload.disk.DiskFileItemFactory

import javax.servlet.http.HttpServletRequest

@CompileStatic
class StringMultiPartFormParser implements UploadContext {
private final String contentType
private final String characterEncoding
private final Long contentLength
private final String body

Map<String, String> parameters = [:]
Map<String, FileItem> files = [:]

StringMultiPartFormParser(HttpServletRequest request, String body) {
this.contentType = request.contentType
this.characterEncoding = request.characterEncoding
this.contentLength = request.contentLength
this.body = body

new FileUpload(new DiskFileItemFactory()).parseRequest(this).forEach { item ->
if (item.isFormField()) {
parameters.put(item.getFieldName(), item.getString())
} else {
files.put(item.getFieldName(), item)
}
}
}

@Override
long contentLength() {
return contentLength
}

@Override
String getCharacterEncoding() {
return characterEncoding
}

@Override
String getContentType() {
return contentType
}

@Override
int getContentLength() {
return -1
}

@Override
InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(body.getBytes())
}
}

 

And I use it in the REST endpoint like this:

 

package mypackage

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript

import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response


@BaseScript
CustomEndpointDelegate delegate

uploadTemporaryAttachment(
httpMethod: 'POST',
groups: ['administrators']
) { MultivaluedMap queryParams, String body, HttpServletRequest request ->
final form = new StringMultiPartFormParser(request, body)
final someParameter = form.parameters.get('someParameter')
final file = form.files.get('file')

// Parameters and files validation goes here
// ...
//

log.error(someParameter)
log.error(file.getName())
log.error(file.getContentType())
log.error(file.getSize())

return Response.status(200).entity([
someParameter: someParameter,
fileName: file.getName(),
fileContentType: file.getContentType(),
fileSize: file.getSize(),
]).build()
}
1 vote
Jonny Carter
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 11, 2017

Hey, Daniel. So, after hacking with this at length, I concluded that there's not a really robust way to do this with ScriptRunner out of the box. Like you, I was able to hack together something that handled uploading basic text files, but images proved more problematic. I've submitted a bug report for that. https://productsupport.adaptavist.com/browse/SRPLAT-211

We may just end up documenting a working example or pointing people to how to roll their own REST Endpoint using JAX, but you can watch that for further developments on multipart support in ScriptRunner REST Endpoints.

Daniel Garcia June 11, 2018

I ended up doing this

 

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import com.atlassian.jira.config.properties.ApplicationProperties
import com.atlassian.jira.component.ComponentAccessor
import groovy.transform.BaseScript
import groovy.json.JsonBuilder
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import javax.servlet.http.HttpServletRequest
import java.io.FileOutputStream
import groovy.json.JsonSlurper
import java.util.regex.*

@BaseScript CustomEndpointDelegate delegate

TestImageUpload(httpMethod: "POST") { MultivaluedMap queryParams, String body, HttpServletRequest req ->
def params = new JsonSlurper().parseText(body)
Pattern re = Pattern.compile('^data:[^;]*;base64,(?<data>.*)$')
Matcher m1 = re.matcher((String)params['image'])
if (m1.matches()) {
String imageb64 = m1.group('data')
byte[] filedata = imageb64.decodeBase64()
String filename = '/var/something/somefile'
File file = new File(filename)
if (!file.exists()) {
file.createNewFile()
}
FileOutputStream fos = new FileOutputStream(filename)
fos.write(filedata)
fos.close()
return Response.ok(new JsonBuilder([status: 'ok']).toString()).build()
} else {
return Response.ok(new JsonBuilder([status: 'not ok']).toString()).build()
}
}
Like # people like this
0 votes
Deleted user March 28, 2023

Im trying to do something similar, but in my case it must be a xlsx file. But payload in body has a corrupted encoding. Maybe you do know the reason? Didn't you faced an issue like this?

Only binary data is corrupted, headers are ok.

Jamie Echlin _ScriptRunner - The Adaptavist Group_
Marketplace Partner
Marketplace Partners provide apps and integrations available on the Atlassian Marketplace that extend the power of Atlassian products.
April 27, 2023

Hi - this wasn't possible until a recent release.

In the most recent releases we have added a new closure signature where we do not attempt to read the request body (and corrupt it for binary files).

A worked example is here: https://library.adaptavist.com/entity/file-upload-to-rest-endpoint

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events