JSON과 같이 RESTful WebService에 파일 및 관련 데이터 게시


757

이것은 아마도 어리석은 질문 일 것입니다. 그러나 나는 그 밤 중 하나를 보내고 있습니다. 응용 프로그램에서 RESTful API를 개발 중이며 클라이언트가 데이터를 JSON으로 보내길 원합니다. 이 응용 프로그램의 일부로 클라이언트는 이미지에 대한 정보뿐만 아니라 파일 (일반적으로 이미지)을 업로드해야합니다.

단일 요청에서 이것이 어떻게 발생하는지 추적하는 데 어려움을 겪고 있습니다. 파일 데이터를 JSON 문자열로 Base64 할 수 있습니까? 서버에 2 개의 게시물을 수행해야합니까? 이것을 위해 JSON을 사용해서는 안됩니까?

참고로 백엔드에서 Grails를 사용하고 있으며 이러한 서비스는 기본 모바일 클라이언트 (iPhone, Android 등)에서 액세스하는 경우 차이가 있습니다.


1
그렇다면 가장 좋은 방법은 무엇입니까?
James111

3
JSON 대신 URL 쿼리 문자열로 메타 데이터를 보냅니다.
jrc

답변:


632

나는 비슷한 질문을했다 :

REST 웹 서비스를 사용하여 메타 데이터가있는 파일을 어떻게 업로드합니까?

기본적으로 세 가지 선택이 있습니다.

  1. Base64는 데이터 크기를 약 33 % 증가시키는 비용으로 파일을 인코딩하고 서버 / 클라이언트에서 인코딩 / 디코딩을 위해 처리 오버 헤드를 추가합니다.
  2. multipart/form-dataPOST 에서 파일을 먼저 보내고 클라이언트에 ID를 반환하십시오. 그런 다음 클라이언트는 메타 데이터를 ID와 함께 보내고 서버는 파일과 메타 데이터를 다시 연결합니다.
  3. 메타 데이터를 먼저 보내고 클라이언트에게 ID를 반환하십시오. 그런 다음 클라이언트는 파일을 ID로 보내면 서버는 파일과 메타 데이터를 다시 연결합니다.

29
옵션 1을 선택한 경우 JSON 문자열 내에 Base64 컨텐츠 만 포함합니까? {file : '234JKFDS # $ @ # $ MFDDMS ....', name : 'somename'...} 아니면 다른 것이 있습니까?
Gregg

15
그레그, 당신이 말한대로 속성으로 포함시키고 값은 base64로 인코딩 된 문자열이됩니다. 이 방법은 가장 쉬운 방법이지만 파일 크기에 따라 실용적이지 않을 수 있습니다. 예를 들어, 응용 프로그램의 경우 각각 2-3MB 인 iPhone 이미지를 보내야합니다. 33 %의 증가는 허용되지 않습니다. 작은 20KB 이미지 만 보내는 경우 해당 오버 헤드가 더 적합 할 수 있습니다.
Daniel T.

19
또한 base64 인코딩 / 디코딩에도 약간의 처리 시간이 필요하다는 점도 언급해야합니다. 가장 쉬운 방법 일 수도 있지만 최선은 아닙니다.
Daniel T.

8
base64와 JSON? 흠 .. 나는 multipart / form을 고집하려고 생각하고있다
Omnipresent

12
한 번의 요청으로 multipart / form-data를 사용하지 않는 이유는 무엇입니까?
1nstinct

107

multipart / form-data 컨텐츠 유형을 사용하여 한 번의 요청으로 파일 및 데이터를 전송할 수 있습니다 .

많은 응용 프로그램에서 사용자에게 양식을 제공 할 수 있습니다. 사용자는 입력하거나 사용자 입력으로 생성하거나 사용자가 선택한 파일에 포함 된 정보를 포함하여 양식을 작성합니다. 양식이 작성되면 양식의 데이터가 사용자에서 수신 애플리케이션으로 전송됩니다.

MultiPart / Form-Data의 정의는 해당 응용 프로그램 중 하나에서 파생됩니다 ...

에서 http://www.faqs.org/rfcs/rfc2388.html :

"multipart / form-data"에는 일련의 파트가 포함됩니다. 각 부분에는 처리 유형이 "form-data"이고 내용에 "name"의 (추가) 매개 변수가 포함 된 콘텐츠 처리 헤더 [RFC 2183]가 있어야합니다. 여기서 해당 매개 변수의 값은 원본입니다. 양식의 필드 이름. 예를 들어 부품에 헤더가 포함될 수 있습니다.

내용 처리 : 양식 데이터; name = "user"

"사용자"필드의 입력에 해당하는 값으로.

경계 사이의 각 섹션 내에 파일 정보 또는 필드 정보를 포함 할 수 있습니다. 사용자가 데이터와 양식을 모두 제출 해야하는 RESTful 서비스를 성공적으로 구현했으며 multipart / form-data가 완벽하게 작동했습니다. 이 서비스는 Java / Spring을 사용하여 구축되었으며 클라이언트는 C #을 사용하고 있었으므로 불행히도 서비스 설정 방법에 관한 Grails 예제가 없습니다. 이 경우 각 "form-data"섹션에 매개 변수 이름과 값을 지정할 수있는 위치가 제공되므로 JSON을 사용할 필요가 없습니다.

multipart / form-data 사용에 대한 좋은 점은 HTTP 정의 헤더를 사용한다는 것입니다. 따라서 기존 HTTP 도구를 사용하여 서비스를 작성한다는 REST 철학을 고수하고 있습니다.


1
감사하지만 내 질문은 요청에 JSON을 사용하고 가능한 경우에 초점을 맞추 었습니다. 나는 당신이 제안한대로 그것을 보낼 수 있다는 것을 이미 알고 있습니다.
Gregg

15
네, 본질적으로 "JSON을 사용하지 않아야합니까?" 클라이언트가 JSON을 사용하도록하려는 특별한 이유가 있습니까?
McStretch

3
비즈니스 요구 사항 또는 일관성 유지. 물론 이상적인 유형은 Content-Type HTTP 헤더를 기반으로 양식 데이터와 JSON 응답을 모두 수락하는 것입니다.
Daniel T.

2
JSON을 선택하면 클라이언트와 서버 측 모두에서 훨씬 더 우아한 코드가 생성되므로 잠재적 인 버그가 줄어 듭니다. 어제 양식 데이터가 너무 큽니다.
superarts.org

5
.Net 개발자의 감정이 상했다면 내가 말한 것에 대해 사과드립니다. 영어는 모국어가 아니지만 기술 자체에 대해 무례한 말을하는 것은 핑계가 아닙니다. 양식 데이터를 사용하는 것이 좋습니다. 계속 사용하면 훨씬 더 훌륭해집니다!
superarts.org 2016 년

53

이 스레드가 상당히 오래되었다는 것을 알고 있지만 여기에 한 가지 옵션이 없습니다. 업로드 할 데이터와 함께 전송하려는 메타 데이터 (모든 형식)가있는 경우 단일 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--

나는 당신의 솔루션을 가장 좋아했습니다. 불행히도, 브라우저에서 다중 / 관련 요청을 생성 할 방법이없는 것 같습니다.
Petr Baudis

클라이언트가 이런 방식으로 API와 통신하도록하는 경험이
있습니까?

불행히도, 현재 PHP (7.2.1)에 대한 이런 종류의 데이터에 대한 독자는 없으며 자신 만의 파서를 작성해야합니다
dewed

서버와 클라이언트가 이것을 잘 지원하지 못한다는 것은 슬픈 일입니다.
Nader Ghanbari

14

나는이 질문이 오래되었다는 것을 알고 있지만 마지막 날 에이 동일한 질문을 해결하기 위해 전체 웹을 검색했습니다. 그림, 제목 및 설명을 보내는 REST 웹 서비스 및 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 Campbell

14

다음은 내 접근 API입니다 (예 : 사용 예). 보시 file_id다시피 API에서 (업로드 된 파일 식별자)를 사용하지 않습니다 .

  1. photo서버에서 객체를 만듭니다 .

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. 파일 업로드 ( file사진 당 하나만 있기 때문에 단수형입니다) :

    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)과 함께 두 번째 요청을 보냅니다.


3
이것은 이것을 달성하는보다 '안정적인'방법처럼 보입니다.
James Webster

새로 작성된 자원에 대한 POST 조작은 오브젝트의 간단한 버전 세부 사항으로 위치 ID를 리턴해야합니다.
Ivan Proskuryakov

@ivanproskuryakov 왜 "필수"? 위의 예에서 (포인트 2의 POST) 파일 ID는 쓸모가 없습니다. 두 번째 인수 (포인트 2의 POST) 나는 단일 형식 '/ file'( '/ files'아님)을 사용하므로 path : / projects / 2 / photos / 3 / file은 신원 사진 파일에 전체 정보를 제공하기 때문에 ID가 필요하지 않습니다.
Kamil Kiełczewski

HTTP 프로토콜 사양에서. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Created "새로 생성 된 리소스는 응답 엔티티에 반환 된 URI에 의해 참조 될 수 있습니다. 위치 헤더 필드 " @ KamilKiełczewski (1)과 (2)는 하나의 POST 작업 POST로 결합 될 수 있습니다. 모든 세부 사항이 담긴 단일 사진 CGET : 모든 사진 모음
Ivan Proskuryakov

1
메타 데이터와 업로드가 별도의 작업 인 경우 엔드 포인트에 다음과 같은 문제가 있습니다. 파일 업로드 POST 작업 사용-POST가 dem 등성이 아닙니다. 리소스를 새로 만들지 않고 리소스를 변경하므로 PUT (idempotent)을 사용해야합니다. REST는 resources 라는 객체와 함께 작동 합니다 . 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 코드를 일반 파일 업로드로 변경하여 코드를 읽는 사람에게 더 명확
해졌

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

파일 업로드와 함께 두 개의 매개 변수 (uuid 및 type)를 전달하고 있습니다. 이것이 복잡한 json 데이터를 보내지 못하는 사람에게 도움이되기를 바랍니다.


1

https://square.github.io/okhttp/ 라이브러리를 사용해보십시오 . 요청 본문을 multipart로 설정 한 다음 파일과 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.