将文件和关联数据发布到RESTful WebService,最好是JSON

Gre*_*egg 687 rest grails json file-upload

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

我很难跟踪单个请求中如何发生这种情况.是否可以将文件数据Base64转换为JSON字符串?我是否需要向服务器发送2个帖子?我不应该为此使用JSON吗?

作为旁注,我们在后端使用Grails,这些服务由本机移动客户端(iPhone,Android等)访问,如果其中任何一个有所不同.

Dan*_* T. 575

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

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

你基本上有三个选择:

  1. Base64对文件进行编码,代价是将数据大小增加约33%.
  2. 首先在multipart/form-dataPOST中发送文件,然后将ID返回给客户端.然后,客户端发送带有ID的元数据,服务器将文件和元数据重新关联.
  3. 首先发送元数据,然后将ID返回给客户端.然后,客户端发送带有ID的文件,服务器将文件和元数据重新关联.

  • 如果我选择了选项1,我是否只在JSON字符串中包含Base64内容?{file:'234JKFDS#$ @#$ MFDDMS ....',名称:'somename'...}或者还有更多内容吗? (24认同)
  • 正如你所说的那样,Gregg只是将它作为属性包含在内,而值将是base64编码的字符串.这可能是最简单的方法,但根据文件大小可能不实用.例如,对于我们的应用程序,我们需要发送每个2-3 MB的iPhone映像.增加33%是不可接受的.如果您只发送小的20KB图像,那么开销可能更容易接受. (13认同)
  • 我还要提一下,base64编码/解码也需要一些处理时间.这可能是最简单的事情,但它肯定不是最好的. (13认同)
  • 为什么拒绝在一个请求中使用multipart/form-data? (9认同)
  • json与base64?嗯..我正在考虑坚持使用multipart/form (6认同)
  • @Socrates我想说选项2更安全。假设您正在管理带有一些附件的表单。您上传文件并从服务器接收一个 ID。如果文件上传成功,您可以使用文件 ID 更新表单并发送表单的保存请求。这样,如果文件失败,您就永远不会更新表单。相反,您可能会面临更新表单然后无法上传文件的风险。 (3认同)
  • 如果放弃 JSON 部分,如何将所有内容作为“multipart/form-data”发送作为解决方案?我问这个问题是因为我不明白为什么此时应该使用 JSON(除非我用 JSON+Base64 对文件进行编码)。 (2认同)
  • 选项4:修改您的WebService API,以接受JSON或multipart / form-data输入。 (2认同)
  • 我想在此处添加第4个选项-消除对选项#2和#3的发送顺序的需要:在客户端上创建UUID。使用此UUID发送文件/使用此UUID发送元数据(以任何顺序)。这种方法的“功能”是客户端负责创建UUID-但是无论如何通常都需要使用它来帮助同步。 (2认同)
  • 对我来说,第四个选项是,在multipart/form-data中添加另一个类型为'TEXT'的表单数据并使用字符串化的元数据. (2认同)
  • 有趣的。为什么要先发送可能很大的文件?如果元数据从未到达怎么办?您将最无用的文件存储在服务器上并等待元数据多长时间?如果您先发送元数据,则会将文件(或数据对象)标记为不完整,直到有效负载到达。这样您就可以使用垃圾收集器并删除不完整的文件,无论如何,这些文件不会占用太多空间。您还拥有其余的表单数据,并且可以要求用户稍后提供有效负载。(假设您要上传复杂的表单,而不仅仅是单个文件)。 (2认同)

McS*_*tch 97

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

在许多应用中,可以向用户呈现表格.用户将填写表单,包括键入的信息,由用户输入生成的信息,或者包含在用户选择的文件中的信息.填写表单后,表单中的数据将从用户发送到接收应用程序.

MultiPart/Form-Data的定义来自其中一个应用程序......

来自http://www.faqs.org/rfcs/rfc2388.html:

"multipart/form-data"包含一系列部分.每个部分都应包含内容处置标题[RFC 2183],其中处置类型为"form-data",并且处置包含"name"的(附加)参数,其中该参数的值为原始值表单中的字段名称.例如,部件可能包含标头:

内容处理:表格数据; 名称="用户"

具有与"user"字段的条目对应的值.

您可以在边界之间的每个部分中包含文件信息或字段信息.我已经成功实现了RESTful服务,该服务要求用户提交数据和表单,并且multipart/form-data完美地工作.该服务是使用Java/Spring构建的,客户端使用的是C#,所以很遗憾,我没有任何Grails示例可以为您提供有关如何设置服务的信息.在这种情况下,您不需要使用JSON,因为每个"form-data"部分都为您提供了指定参数名称及其值的位置.

使用multipart/form-data的好处在于您使用的是HTTP定义的头文件,因此您坚持使用现有HTTP工具创建服务的REST理念.

  • 是的,这基本上是我的答案"我不应该为此使用JSON吗?" 是否有特定原因要求客户端使用JSON? (13认同)
  • 谢谢,但我的问题集中在想要使用 JSON 进行请求以及是否可能。我已经知道我可以按照您建议的方式发送。 (4认同)
  • 如果它伤害了某些.Net开发人员的感觉,我为我所说的道歉.虽然英语不是我的母语,但对于我说这项技术本身的粗鲁行为并不是一个有效的借口.使用表单数据非常棒,如果你继续使用它,你也会更加棒极了! (4认同)
  • 为什么一个表单字段不能是 json,第二个字段不能是多部分文件?服务器只需要解析 json 并继续生活。 (4认同)
  • 最有可能是业务要求或保持一致性.当然,理想的做法是基于Content-Type HTTP标头接受(表单数据和JSON响应). (3认同)
  • 选择JSON会在客户端和服务器端产生更优雅的代码,从而减少潜在的错误.表格数据是昨天的. (2认同)
  • 但在这种情况下,如何在客户端获取文本数据和图像,因为它们都有一个端点? (2认同)

pgi*_*cek 44

我知道这个帖子很老了,但是,我在这里缺少一个选项.如果您要将要发送的元数据(以任何格式)与要上载的数据一起发送,则可以发出单个multipart/related请求.

Multipart/Related媒体类型适用于由多个相互关联的身体部位组成的复合对象.

您可以查看RFC 2387规范以获取更深入的详细信息.

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

例:

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--
Run Code Online (Sandbox Code Playgroud)

  • 到目前为止,我最喜欢你的解决方案。不幸的是,似乎无法在浏览器中创建多部分/相关请求。 (2认同)
  • 您是否有让客户端(尤其是 JS 客户端)以这种方式与 api 进行通信的经验 (2认同)
  • 遗憾的是服务器和客户端对此没有良好的支持。 (2认同)

小智 14

我知道这个问题已经过时了,但在最后几天我搜索了整个网络以解决同样的问题.我有grails REST webservices和iPhone Client发送图片,标题和描述.

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

我使用UIImagePickerController拍照并使用请求的标头标签向服务器发送NSData以发送图片的数据.

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];
Run Code Online (Sandbox Code Playgroud)

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

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")

}
Run Code Online (Sandbox Code Playgroud)

我不知道将来是否有问题,但现在在生产环境中工作正常.

  • 我喜欢这个使用 http 标头的选项。当元数据和标准 http 标头之间存在某种对称性时,这尤其有效,但您显然可以自己发明。 (2认同)

Kam*_*ski 9

这是我的方法API(我使用示例) - 正如您所看到的,您在API中不使用任何file_id(在服务器中上传的文件identyicator):

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

POST: /projects/{project_id}/photos   
params in: {name:some_schema.jpg, comment:blah}
return: photo_id
Run Code Online (Sandbox Code Playgroud)

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

POST: /projects/{project_id}/photos/{photo_id}/file
params in: file to upload
return: -
Run Code Online (Sandbox Code Playgroud)

然后例如:

3.阅读照片列表

GET: /projects/{project_id}/photos
params in: -
return: array of objects: [ photo, photo, photo, ... ]
Run Code Online (Sandbox Code Playgroud)

4.阅读一些照片细节

GET: /projects/{project_id}/photos/{photo_id}
params in: -
return: photo = { id: 666, name:'some_schema.jpg', comment:'blah'}
Run Code Online (Sandbox Code Playgroud)

5.阅读照片文件

GET: /projects/{project_id}/photos/{photo_id}/file
params in: -
return: file content
Run Code Online (Sandbox Code Playgroud)

所以结论是,首先你通过POST创建对象(照片),然后你发送带文件的secod请求(再次POST).

  • 如果元数据和上传是单独的操作,则端点具有以下问题: 对于使用的文件上传 POST 操作 - POST 不是幂等的。必须使用 PUT(idempotent),因为您要更改资源而不创建新资源。REST 处理名为 *resources* 的对象。POST:“../photos/” PUT:“../photos/{photo_id}” GET:“../photos/” GET:“../photos/{photo_id}” PS。将上传分离到单独的端点可能会导致不可预测的行为。http://www.restapitutorial.com/lessons/idempotency.html http://restful-api-design.readthedocs.io/en/latest/resources.html (3认同)
  • 这似乎是实现这一目标的更"'RESTFUL'方式. (2认同)

lif*_*foo 6

由于唯一缺少的例子是ANDROID示例,我将添加它.此技术使用应在Activity类中声明的自定义AsyncTask.

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);
    }

}
Run Code Online (Sandbox Code Playgroud)

所以,当你想上传你的文件时,只需致电:

new UploadFile().execute();
Run Code Online (Sandbox Code Playgroud)


lak*_*ate 5

FormData对象:使用Ajax上传文件

XMLHttpRequest Level 2增加了对新FormData接口的支持.FormData对象提供了一种方法,可以轻松构造一组表示表单字段及其值的键/值对,然后可以使用XMLHttpRequest send()方法轻松发送.

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);
}
Run Code Online (Sandbox Code Playgroud)

https://developer.mozilla.org/en-US/docs/Web/API/FormData