Публикация файла и связанных данных на RESTful WebService предпочтительно в виде JSON

Вероятно, это будет глупый вопрос, но у меня одна из тех ночей. В приложении я разрабатываю RESTful API, и мы хотим, чтобы клиент отправлял данные в формате JSON. Часть этого приложения требует, чтобы клиент загружал файл (обычно изображение), а также информацию об изображении.

Мне трудно отследить, как это происходит в одном запросе. Можно ли Base64 данных файла в строку JSON? Я собираюсь выполнить 2 сообщения на сервере? Я не должен использовать JSON для этого?

В качестве дополнительного примечания мы используем Grails на бэкэнде, и к этим службам обращаются нативные мобильные клиенты (iPhone, Android и т. Д.), Если что-то из этого имеет значение.

11 ответов

Решение

Я задал подобный вопрос здесь:

Как загрузить файл с метаданными с помощью веб-службы REST?

У вас есть три варианта:

  1. Base64 кодирует файл за счет увеличения размера данных примерно на 33%.
  2. Отправьте файл первым в multipart/form-data POST и верните ID клиенту. Затем клиент отправляет метаданные с идентификатором, а сервер повторно связывает файл и метаданные.
  3. Сначала отправьте метаданные и верните идентификатор клиенту. Затем клиент отправляет файл с идентификатором, а сервер повторно связывает файл и метаданные.

Вы можете отправить файл и данные за один запрос, используя тип содержимого multipart/form-data:

Во многих приложениях пользователю может быть предоставлена ​​форма. Пользователь заполняет форму, включая информацию, которая вводится, генерируется с помощью ввода пользователя или включается из файлов, выбранных пользователем. Когда форма заполнена, данные из формы отправляются от пользователя к получающему приложению.

Определение MultiPart/Form-Data получено из одного из этих приложений...

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

"multipart/form-data" содержит ряд частей. Ожидается, что каждая часть будет содержать заголовок размещения содержимого [RFC 2183], где тип расположения - "данные формы", а расположение - (дополнительный) параметр "name", где значение этого параметра является исходным Имя поля в форме. Например, часть может содержать заголовок:

Content-Disposition: форма-данные; имя = "пользователь"

со значением, соответствующим записи в поле "пользователь".

Вы можете включить информацию о файле или информацию о поле в каждый раздел между границами. Я успешно реализовал сервис RESTful, который требовал от пользователя отправки как данных, так и формы, и multipart/form-data работали отлично. Служба была построена с использованием Java/Spring, а клиент использовал C#, поэтому, к сожалению, у меня нет примеров Grails, чтобы дать вам информацию о том, как настроить службу. Вам не нужно использовать JSON в этом случае, так как каждый раздел "form-data" предоставляет вам место для указания имени параметра и его значения.

Преимущество использования multipart/form-data заключается в том, что вы используете HTTP-определенные заголовки, поэтому вы придерживаетесь философии REST об использовании существующих инструментов HTTP для создания вашего сервиса.

Я знаю, что эта ветка довольно старая, но мне здесь не хватает одного варианта. Если у вас есть метаданные (в любом формате), которые вы хотите отправить вместе с данными для загрузки, вы можете сделать один 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--

Вот мой подход API (я использую пример) - как вы можете видеть, вы не используете file_id (идентификатор загруженного файла на сервере) в API:

1. Создайте объект 'photo' на сервере:

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

2. Загрузите файл (обратите внимание, что "файл" находится в единственном числе, потому что он только один на фотографию):

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

А потом, например:

3. Читайте список фотографий

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

4. Прочитайте некоторые детали фотографии

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

5. Читайте фото файл

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

Итак, вывод таков: сначала вы создаете объект (фотографию) по POST, а затем отправляете запрос secod с файлом (снова POST).

Я знаю, что этот вопрос старый, но в последние дни я искал всю сеть, чтобы решить тот же вопрос. У меня есть веб-сервисы Grails REST и 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];

На стороне сервера я получаю фото, используя код:

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

}

Я не знаю, возникнут ли у меня проблемы в будущем, но сейчас работает нормально в производственной среде.

Объекты 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);
}

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

Поскольку единственным отсутствующим примером является пример ANDROID, я добавлю его. В этом методе используется пользовательский AsyncTask, который должен быть объявлен внутри вашего класса Activity.

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

Я хотел отправить несколько строк на бэкэнд-сервер. Я не использовал json с multipart, я использовал параметры запроса.

@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 для отправки.

Вы можете попробовать использовать библиотеку 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());
@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
}

Пожалуйста, убедитесь, что у вас есть следующий импорт. Конечно, другой стандартный импорт

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. Пусть клиент выставит файл через HTTP
  2. Затем клиент может отправить URL с вашими данными JSON, например, файл изображения {"file_url":"http://cockwombles.com/blah.jpg"}
  3. Затем сервер может загрузить файл.
Другие вопросы по тегам