关于REST:将文件和相关数据发布到RESTfulWebservice,最好是JSON

Posting a File and Associated Data to a RESTful WebService preferably as JSON

这可能会是一个愚蠢的问题,但我有一个晚上。在一个应用程序中,我正在开发RESTfulAPI,我们希望客户机以JSON的形式发送数据。此应用程序的一部分要求客户端上载文件(通常是图像)以及有关图像的信息。

我很难在一个请求中跟踪这是如何发生的。是否可以将文件数据作为基64放到JSON字符串中?我需要对服务器执行2个发布吗?我不应该为此使用JSON吗?

另一方面,我们在后端使用Grails,这些服务由本地移动客户机(iPhone、Android等)访问,如果有什么不同的话。


我在这里问了一个类似的问题:

如何使用RESTWeb服务上载包含元数据的文件?

你基本上有三个选择:

  • base64对文件进行编码,以增加大约33%的数据大小为代价,并在服务器和客户机中添加用于编码/解码的处理开销。
  • multipart/form-data邮件中首先发送文件,然后将ID返回给客户。然后,客户机将元数据与ID一起发送,服务器将文件与元数据重新关联。
  • 首先发送元数据,然后向客户机返回一个ID。然后,客户机用ID发送文件,服务器将文件和元数据重新关联。

  • 您可以使用multipart/form数据内容类型在一个请求中发送文件和数据:

    In many applications, it is possible for a user to be presented with
    a form. The user will fill out the form, including information that
    is typed, generated by user input, or included from files that the
    user has selected. When the form is filled out, the data from the
    form is sent from the user to the receiving application.

    The definition of MultiPart/Form-Data is derived from one of those
    applications...

    从http://www.faqs.org/rfcs/rfc2388.html:

    "multipart/form-data" contains a series of parts. Each part is
    expected to contain a content-disposition header [RFC 2183] where the
    disposition type is"form-data", and where the disposition contains
    an (additional) parameter of"name", where the value of that
    parameter is the original field name in the form. For example, a part
    might contain a header:

    Content-Disposition: form-data; name="user"

    with the value corresponding to the entry of the"user" field.

    您可以在边界之间的每个部分中包含文件信息或字段信息。我成功地实现了一个RESTful服务,它要求用户提交数据和表单,并且多部分/表单数据工作得很好。该服务是使用Java/Spring构建的,客户机使用的是Cype,所以不幸的是,我没有任何GRAIUS实例来告诉您如何设置服务。在这种情况下不需要使用JSON,因为每个"表单数据"部分都为您提供了一个指定参数名称及其值的位置。

    使用multipart/form数据的好处在于,您使用的是HTTP定义的头文件,所以您坚持使用现有HTTP工具创建服务的其余原则。


    我知道这条线已经很旧了,但是,我这里缺少一个选项。如果您有要随数据一起发送的元数据(任何格式),则可以发出单个multipart/related请求。

    The Multipart/Related media type is intended for compound objects consisting of several inter-related body parts.

    您可以查看RFC2387规范以获得更深入的详细信息。

    基本上,这样一个请求的每个部分都可以有不同类型的内容,并且所有部分都以某种方式相关(例如图像和IT元数据)。这些部分由一个边界字符串标识,最后一个边界字符串后跟两个连字符。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    POST /upload HTTP/1.1
    Host: www.hostname.com
    Content-Type: multipart/related; boundary=xyz
    Content-Length: [actual-content-length]

    --xyz
    Content-Type: application/json; charset=UTF-8

    {
       "name":"Sample image",
       "desc":"...",
        ...
    }

    --xyz
    Content-Type: image/jpeg

    [image data]
    [image data]
    [image data]
    ...
    --foo_bar_baz--


    我知道这个问题由来已久,但在过去的几天里,我搜索了整个网络来解决这个问题。我有GrailsRestWebServices和iPhone客户端,它们发送图片、标题和描述。

    我不知道我的方法是否是最好的,但简单易行。

    我使用uiImagePickerController拍摄照片,并使用请求的头标记将nsdata发送到服务器,以发送图片的数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"myServerAddress"]];
    [request setHTTPMethod:@"POST"];
    [request setHTTPBody:UIImageJPEGRepresentation(picture, 0.5)];
    [request setValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"myPhotoTitle" forHTTPHeaderField:@"Photo-Title"];
    [request setValue:@"myPhotoDescription" forHTTPHeaderField:@"Photo-Description"];

    NSURLResponse *response;

    NSError *error;

    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

    在服务器端,我使用以下代码接收照片:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    InputStream is = request.inputStream

    def receivedPhotoFile = (IOUtils.toByteArray(is))

    def photo = new Photo()
    photo.photoFile = receivedPhotoFile //photoFile is a transient attribute
    photo.title = request.getHeader("Photo-Title")
    photo.description = request.getHeader("Photo-Description")
    photo.imageURL ="temp"    

    if (photo.save()) {    

        File saveLocation = grailsAttributes.getApplicationContext().getResource(File.separator +"images").getFile()
        saveLocation.mkdirs()

        File tempFile = File.createTempFile("photo",".jpg", saveLocation)

        photo.imageURL = saveLocation.getName() +"/" + tempFile.getName()

        tempFile.append(photo.photoFile);

    } else {

        println("Error")

    }

    我不知道将来是否有问题,但现在在生产环境中工作得很好。


    这里是我的方法API(我使用示例)-正如您所看到的,您在API中没有使用任何文件标识(服务器中上载的文件标识符):

    1.在服务器上创建"photo"对象:

    1
    2
    3
    POST: /projects/{project_id}/photos  
    params in: {name:some_schema.jpg, comment:blah}
    return: photo_id

    2.上传文件(请注意,"文件"是单数形式,因为每张照片只有一个文件):

    1
    2
    3
    POST: /projects/{project_id}/photos/{photo_id}/file
    params in: file to upload
    return: -

    例如:

    3.阅读照片列表

    1
    2
    3
    GET: /projects/{project_id}/photos
    params in: -
    return: array of objects: [ photo, photo, photo, ... ]

    4.阅读一些照片详细信息

    1
    2
    3
    GET: /projects/{project_id}/photos/{photo_id}
    params in: -
    return: photo = { id: 666, name:'some_schema.jpg', comment:'blah'}

    5.读取照片文件

    1
    2
    3
    GET: /projects/{project_id}/photos/{photo_id}/file
    params in: -
    return: file content

    所以得出的结论是,首先通过post创建对象(photo),然后通过file发送第二个请求(再次post)。


    由于唯一缺少的示例是Android示例,我将添加它。此技术使用的自定义异步任务应在Activity类中声明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    private class UploadFile extends AsyncTask<Void, Integer, String> {
        @Override
        protected void onPreExecute() {
            // set a status bar or show a dialog to the user here
            super.onPreExecute();
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            // progress[0] is the current status (e.g. 10%)
            // here you can update the user interface with the current status
        }

        @Override
        protected String doInBackground(Void... params) {
            return uploadFile();
        }

        private String uploadFile() {

            String responseString = null;
            HttpClient httpClient = new DefaultHttpClient();
            HttpPost httpPost = new HttpPost("http://example.com/upload-file");

            try {
                AndroidMultiPartEntity ampEntity = new AndroidMultiPartEntity(
                    new ProgressListener() {
                        @Override
                            public void transferred(long num) {
                                // this trigger the progressUpdate event
                                publishProgress((int) ((num / (float) totalSize) * 100));
                            }
                });

                File myFile = new File("/my/image/path/example.jpg");

                ampEntity.addPart("fileFieldName", new FileBody(myFile));

                totalSize = ampEntity.getContentLength();
                httpPost.setEntity(ampEntity);

                // Making server call
                HttpResponse httpResponse = httpClient.execute(httpPost);
                HttpEntity httpEntity = httpResponse.getEntity();

                int statusCode = httpResponse.getStatusLine().getStatusCode();
                if (statusCode == 200) {
                    responseString = EntityUtils.toString(httpEntity);
                } else {
                    responseString ="Error, http status:"
                            + statusCode;
                }

            } catch (Exception e) {
                responseString = e.getMessage();
            }
            return responseString;
        }

        @Override
        protected void onPostExecute(String result) {
            // if you want update the user interface with upload result
            super.onPostExecute(result);
        }

    }

    因此,当您想上传文件时,只需拨打:

    1
    new UploadFile().execute();


    FormData对象:使用Ajax上载文件

    XMLHttpRequestLevel2添加了对新FormData接口的支持。formdata对象提供了一种方法,可以轻松地构造一组表示表单字段及其值的键/值对,然后可以使用xmlhttprequest send()方法轻松发送这些键/值对。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function AjaxFileUpload() {
        var file = document.getElementById("files");
        //var file = fileInput;
        var fd = new FormData();
        fd.append("imageFileData", file);
        var xhr = new XMLHttpRequest();
        xhr.open("POST", '/ws/fileUpload.do');
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                 alert('success');
            }
            else if (uploadResult == 'success')
                 alert('error');
        };
        xhr.send(fd);
    }

    https://developer.mozilla.org/en-us/docs/web/api/formdata


    我想向后端服务器发送一些字符串。我没有在多部分中使用JSON,我使用了请求参数。

    1
    2
    3
    4
    5
    @RequestMapping(value ="/upload", method = RequestMethod.POST)
    public void uploadFile(HttpServletRequest request,
            HttpServletResponse response, @RequestParam("uuid") String uuid,
            @RequestParam("type") DocType type,
            @RequestParam("file") MultipartFile uploadfile)

    URL看起来像

    1
    http://localhost:8080/file/upload?uuid=46f073d0&type=PASSPORT

    我正在传递两个参数(uuid和type)以及文件上传。希望这能帮助那些没有复杂JSON数据的人。


    1
    2
    3
    4
    @RequestMapping(value ="/uploadImageJson", method = RequestMethod.POST)
        public @ResponseBody Object jsongStrImage(@RequestParam(value="image") MultipartFile image, @RequestParam String jsonStr) {
    -- use  com.fasterxml.jackson.databind.ObjectMapper convert Json String to Object
    }

    请确保您有以下导入。当然,其他标准进口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import org.springframework.core.io.FileSystemResource


        void uploadzipFiles(String token) {

            RestBuilder rest = new RestBuilder(connectTimeout:10000, readTimeout:20000)

            def zipFile = new File("testdata.zip")
            def Id ="001G00000"
            MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>()
            form.add("id", id)
            form.add('file',new FileSystemResource(zipFile))
            def urld ='''http://URL''';
            def resp = rest.post(urld) {
                header('X-Auth-Token', clientSecret)
                contentType"multipart/form-data"
                body(form)
            }
            println"resp::"+resp
            println"resp::"+resp.text
            println"resp::"+resp.headers
            println"resp::"+resp.body
            println"resp::"+resp.status
        }