ファイルと関連データをRESTful WebServiceに、できればJSONとして投稿する


757

これはおそらく愚かな質問になるでしょうが、私はそれらの夜の1つを持っています。アプリケーションでは、RESTful APIを開発しており、クライアントがデータをJSONとして送信することを望んでいます。このアプリケーションの一部では、クライアントはファイル(通常は画像)と画像に関する情報をアップロードする必要があります。

これが単一のリクエストでどのように発生するかを追跡するのに苦労しています。ファイルデータをJSON文字列にBase64することは可能ですか?サーバーへの2つの投稿を実行する必要がありますか?これにはJSONを使用しないでください。

ちなみに、私たちはバックエンドでGrailsを使用しています。これらのサービスは、ネイティブモバイルクライアント(iPhone、Androidなど)によってアクセスされます。


1
それで、これを行うための最良の方法は何ですか?
James111

3
JSONではなく、URLクエリ文字列でメタデータを送信します。
jrc

回答:


632

私はここで同様の質問をしました:

REST Webサービスを使用してメタデータを含むファイルをアップロードするにはどうすればよいですか?

基本的に3つの選択肢があります。

  1. Base64はファイルをエンコードしますが、データサイズが約33%増加しますが、サーバーとクライアントの両方でエンコード/デコードの処理オーバーヘッドが追加されます。
  2. 最初にmultipart/form-dataPOSTでファイルを送信し、IDをクライアントに返します。次にクライアントはIDとともにメタデータを送信し、サーバーはファイルとメタデータを再度関連付けます。
  3. 最初にメタデータを送信し、IDをクライアントに返します。次に、クライアントはIDとともにファイルを送信し、サーバーはファイルとメタデータを再度関連付けます。

29
オプション1を選択した場合、Base64コンテンツをJSON文字列内に含めるだけですか?{file: '234JKFDS#$ @#$ MFDDMS ....'、name: 'somename' ...}または、他に何かありますか?
Gregg

15
Greggは、あなたが言ったとおり、それをプロパティとして含めるだけで、値はbase64でエンコードされた文字列になります。これはおそらく最も簡単な方法ですが、ファイルサイズによっては実用的でない場合があります。たとえば、このアプリケーションでは、それぞれ2〜3 MBのiPhoneイメージを送信する必要があります。33%の増加は許容できません。小さな20KBの画像のみを送信する場合は、そのオーバーヘッドが許容範囲内となる場合があります。
ダニエルT.

19
また、base64エンコード/デコードにも処理時間がかかることを述べておきます。それは最も簡単なことかもしれませんが、確かに最善ではありません。
ダニエルT.

8
base64のjson?うーん.. multipart / formに固執することを考えています
Omnipresent

12
1つのリクエストでmultipart / form-dataを使用することが拒否されるのはなぜですか?
2015

107

multipart / form-data コンテンツタイプを使用して、1つのリクエストでファイルとデータを送信できます。

多くのアプリケーションでは、ユーザーにフォームが表示される可能性があります。ユーザーは、入力した情報、ユーザー入力によって生成された情報、またはユーザーが選択したファイルから含まれる情報を含め、フォームに入力します。フォームに入力すると、フォームのデータがユーザーから受信アプリケーションに送信されます。

MultiPart / Form-Dataの定義は、これらのアプリケーションの1つから派生しています...

http://www.faqs.org/rfcs/rfc2388.htmlから:

「multipart / form-data」には一連のパーツが含まれています。各部分には、コンテンツタイプヘッダー[RFC 2183]が含まれていることが想定されています。ここで、ディスポジションタイプは「フォームデータ」であり、ディスポジションには「名前」の(追加の)パラメーターが含まれています。このパラメーターの値は元の値です。フォームのフィールド名。たとえば、パーツにヘッダーが含まれている場合があります。

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

「ユーザー」フィールドのエントリに対応する値。

境界間の各セクション内にファイル情報またはフィールド情報を含めることができます。ユーザーがデータとフォームの両方を送信する必要があるRESTfulサービスを正常に実装し、multipart / form-dataは完全に機能しました。サービスはJava / Springを使用して構築され、クライアントはC#を使用していたため、残念ながら、サービスの設定方法を説明するGrailsの例はありません。この場合、JSONを使用する必要はありません。各 "form-data"セクションには、パラメーターの名前とその値を指定する場所が用意されているためです。

multipart / form-dataを使用することの良い点は、HTTP定義のヘッダーを使用しているため、既存のHTTPツールを使用してサービスを作成するというRESTの考え方に固執していることです。


1
おかげで、私の質問は、リクエストにJSONを使用することと、それが可能かどうかに焦点を当てていました。私はあなたが提案する方法でそれを送ることができることをすでに知っています。
グレッグ、

15
ええ、それは本質的に「これにJSONを使用すべきではないのか?」に対する私の応答です。クライアントでJSONを使用する特定の理由はありますか?
McStretch 2010年

3
ほとんどの場合、ビジネス要件または一貫性を保ちます。もちろん、行うのに理想的なのは、Content-Type HTTPヘッダーに基づいて両方(フォームデータとJSON応答)を受け入れることです。
ダニエルT.

2
JSONを選択すると、クライアント側とサーバー側の両方ではるかに洗練されたコードが生成され、潜在的なバグが減少します。フォームデータは昨日そうです。
superarts.org

5
.Net開発者の気持ちを害した場合は、申し訳ありません。英語は私の母国語ではありませんが、テクノロジー自体について失礼なことを言うのに有効な言い訳にはなりません。フォームデータの使用は素晴らしいです。それを使い続けると、さらに素晴らしいものになります。
superarts.org 2016年

53

このスレッドはかなり古いものですが、ここでは1つのオプションがありません。アップロードするデータと一緒に送信したいメタデータ(任意の形式)がある場合は、1つのmultipart/relatedリクエストを行うことができます。

Multipart / Relatedメディアタイプは、相互に関連する複数のボディパーツで構成される複合オブジェクトを対象としています。

RFC 2387仕様で詳細を確認できます

基本的に、そのようなリクエストの各部分は異なるタイプのコンテンツを持つことができ、すべての部分は何らかの形で関連しています(たとえば、画像とメタデータ)。パーツは境界文字列で識別され、最終的な境界文字列の後には2つのハイフンが続きます。

例:

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

私はあなたの解決策を断然最高でした。残念ながら、ブラウザでマルチパート/関連リクエストを作成する方法はないようです。
Petr Baudis、2016年

この方法でAPI(特にJSのもの)にAPIと通信させる経験はありますか
pvgoddijn

残念ながら、現在、php(7.2.1)にはこの種のデータのリーダーはなく、独自のパーサーを作成する必要があります
dewd

サーバーとクライアントがこれをうまくサポートしていないのは残念です。
Nader Ghanbari、

14

私はこの質問が古いことを知っていますが、最後の日に私はこの同じ質問を解決するためにウェブ全体を検索しました。写真、タイトル、説明を送信するgrails REST WebサービスとiPhoneクライアントがあります。

私のアプローチが最高かどうかはわかりませんが、とても簡単でシンプルです。

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];

サーバー側では、コードを使用して写真を受け取ります。

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

}

将来問題が発生するかどうかはわかりませんが、現在は運用環境で問題なく動作しています。


1
httpヘッダーを使用するこのオプションが好きです。これは、メタデータと標準のhttpヘッダーに対称性がある場合に特に効果的ですが、独自に作成することもできます。
EJキャンベル

14

これが私のアプローチAPIです(私は例を使用しています)-ご覧のfile_idとおり、API では何も(サーバーにアップロードされたファイル識別子)を使用していません。

  1. photoサーバー上にオブジェクトを作成します。

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. ファイルをアップロード(file写真ごとに1つしかないため、単数形であることに注意してください):

    POST: /projects/{project_id}/photos/{photo_id}/file
    body: file to upload
    response: -

そして、例えば:

  1. 写真リストを読む

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. 写真の詳細を読む

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. 写真ファイルを読む

    GET: /projects/{project_id}/photos/{photo_id}/file
    response: file content

したがって、結論は、最初にPOSTによってオブジェクト(写真)を作成し、次にファイルを使用して(もう一度POSTで)2番目のリクエストを送信することです。


3
これは、これを実現するためのより「RESTFUL」な方法のようです。
James Webster、

新しく作成されたリソースのPOST操作は、オブジェクトの単純なバージョン詳細でロケーションIDを返す必要があります
Ivan Proskuryakov

@ivanproskuryakovなぜ「必須」なのですか?上記の例(ポイント2のPOST)では、ファイルIDは役に立ちません。2番目の引数(ポイント2のPOSTの場合)私は単数形「/ file」(「/ files」ではない)を使用しているため、パス:/ projects / 2 / photos / 3 / fileはID写真ファイルに完全な情報を提供するため、IDは必要ありません。
KamilKiełczewski2017年

HTTPプロトコル仕様から。w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Created "新しく作成されたリソースは、応答のエンティティで返されたURIによって参照できます。 Locationヘッダーフィールド。」@KamilKiełczewski(1つ)と(2つ)を1つのPOST操作に組み合わせることができますPOST:/ projects / {project_id} / photos GET単一の写真(resource *)操作GETに使用できる場所ヘッダーを返しますGET: 1枚の写真にすべての詳細CGET:写真のすべてのコレクションを取得
Ivan Proskuryakov

1
メタデータとアップロードが別々の操作である場合、エンドポイントには次の問題があります。ファイルアップロードの場合POST操作が使用されます-POSTはべき等ではありません。新しいリソースを作成せずにリソースを変更するため、PUT(べき等)を使用する必要があります。RESTは、リソースと呼ばれるオブジェクトを処理します。POST:「../photos/」PUT:「../photos/{photo_id}」GET:「../photos/」GET:「../photos/{photo_id}」PS。アップロードを個別のエンドポイントに分離すると、予期しない動作が発生する可能性があります。restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov

6

FormDataオブジェクト:Ajaxを使用してファイルをアップロードする

XMLHttpRequestレベル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);
}

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


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

}

したがって、ファイルをアップロードする場合は、次のように呼び出します。

new UploadFile().execute();

こんにちは、AndroidMultiPartEntityとは何ですか?説明してください。PDF、Word、XLSファイルをアップロードする場合は、何をすべきかを説明してください...これは初めてです。
amit pandya

1
@amitpandya私はコードを一般的なファイルのアップロードに変更したので、それを読んでいる誰にとってもより明確になります
lifeisfoo

2

文字列をバックエンドサーバーに送信する必要がありました。マルチパートでjsonを使用せず、リクエストパラメータを使用しました。

@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は次のようになります

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

ファイルのアップロードと一緒に2つのパラメーター(uuidとtype)を渡します。これが送信する複雑なjsonデータを持っていない人を助けることを願っています。


1

https://square.github.io/okhttp/ライブラリを使用してみてください。リクエストの本文をマルチパートに設定し、次のようにファイルとjsonオブジェクトを個別に追加できます。

MultipartBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("uploadFile", uploadFile.getName(), okhttp3.RequestBody.create(uploadFile, MediaType.parse("image/png")))
                .addFormDataPart("file metadata", json)
                .build();

        Request request = new Request.Builder()
                .url("https://uploadurl.com/uploadFile")
                .post(requestBody)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            logger.info(response.body().string());

0
@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
}

-5

次のインポートがあることを確認してください。もちろん他の標準輸入品

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
    }

1
この取得java.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.