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

757

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

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

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

Gregg
источник
1
Итак, каков наилучший способ сделать это?
James111
3
Отправьте метаданные в строке запроса URL вместо JSON.
17

Ответы:

632

Я задал похожий вопрос здесь:

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

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

  1. Base64 кодирует файл за счет увеличения размера данных примерно на 33% и добавляет накладные расходы на обработку как на сервере, так и на клиенте для кодирования / декодирования.
  2. Сначала отправьте файл в multipart/form-dataPOST и верните идентификатор клиенту. Затем клиент отправляет метаданные с идентификатором, а сервер повторно связывает файл и метаданные.
  3. Сначала отправьте метаданные и верните идентификатор клиенту. Затем клиент отправляет файл с идентификатором, а сервер повторно связывает файл и метаданные.
Даниэль Т.
источник
29
Если я выбрал вариант 1, нужно ли просто включать содержимое Base64 в строку JSON? {file: '234JKFDS # $ @ # $ MFDDMS ....', name: 'somename' ...} Или есть что-то еще?
Грегг
15
Грегг, именно так, как вы сказали, вы бы просто включили его в качестве свойства, а значением была бы строка в кодировке base64. Вероятно, это самый простой способ, но он может быть непрактичным в зависимости от размера файла. Например, для нашего приложения нам нужно отправить изображения iPhone размером 2-3 МБ каждый. Увеличение на 33% недопустимо. Если вы отправляете только небольшие изображения размером 20 КБ, эти издержки могут быть более приемлемыми.
Даниэль Т.
19
Следует также отметить, что кодирование / декодирование base64 также займет некоторое время обработки. Это может быть проще всего сделать, но это, конечно, не самое лучшее.
Даниэль Т.
8
JSON с Base64? хм .. я думаю о том, чтобы придерживаться multipart / формы
вездесущий
12
Почему запрещено использовать multipart / form-data в одном запросе?
1nstinct
107

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

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

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

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

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

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

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

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

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

McStretch
источник
1
Спасибо, но мой вопрос был сосредоточен на желании использовать JSON для запроса и, если это было возможно. Я уже знаю, что могу отправить это так, как вы предлагаете.
Грегг
15
Да, по сути, это мой ответ на вопрос «Разве я не должен использовать JSON для этого?» Есть ли конкретная причина, по которой вы хотите, чтобы клиент использовал JSON?
McStretch
3
Скорее всего, деловое требование или соблюдение последовательности. Конечно, в идеале нужно принять оба (данные формы и ответ JSON) на основе HTTP-заголовка Content-Type.
Даниэль Т.
2
Выбор JSON приводит к гораздо более элегантному коду как на стороне клиента, так и на стороне сервера, что приводит к уменьшению потенциальных ошибок. Данные формы так вчера.
superarts.org
5
Я прошу прощения за то, что я сказал, если это задевает чувства некоторых разработчиков .Net. Хотя английский не является моим родным языком, я не могу сказать, что могу сказать что-то грубое о самой технологии. Использование данных формы - это здорово, и если вы продолжите использовать их, вы тоже будете еще лучше!
superarts.org
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--
pgiecek
источник
Мне больше всего понравилось ваше решение. К сожалению, в браузере нет возможности создавать взаимные запросы или связанные запросы.
Петр Бодис
есть ли у вас опыт привлечения клиентов (особенно JS) для взаимодействия с API таким способом
pvgoddijn
к сожалению, в настоящее время нет читателя для таких данных на php (7.2.1), и вам придется создать свой собственный парсер
dewd
Печально, что серверы и клиенты не имеют хорошей поддержки для этого.
Надер
14

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

}

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

Rscorreia
источник
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
Это кажется более «ОТЛИЧНЫМ» способом достижения этого.
Джеймс Вебстер
Операция POST для вновь создаваемых ресурсов, должна возвращать идентификатор местоположения, в простой версии детали объекта
Иван Проскуряков
@ivanproskuryakov почему "должен"? В приведенном выше примере (POST в пункте 2) идентификатор файла бесполезен. Второй аргумент (для POST в пункте 2) я использую форму единственного числа '/ file' (не '/ files'), поэтому идентификатор не требуется, поскольку путь: / projects / 2 / photos / 3 / file предоставляет полную информацию для файла фотографии личности.
Камиль Келчевски
Из спецификации протокола HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Created «На вновь созданный ресурс могут ссылаться URI, возвращенные в объекте ответа, с наиболее конкретным URI для ресурса, заданного поле заголовка местоположения. " @ KamilKiełczewski (один) и (два) можно объединить в одну операцию POST. POST: / projects / {project_id} / photos Возвращает заголовок местоположения, который можно использовать для операции GET с одной фотографией (ресурс *). GET: получить одиночное фото со всеми подробностями CGET: получить всю коллекцию фотографий
Иван Проскуряков
1
Если метаданные и выгрузка являются отдельными операциями, то у конечных точек возникают следующие проблемы: Для выгрузки файлов используется операция POST - POST не идемпотентен. PUT (идемпотент) должен использоваться, так как вы меняете ресурс, не создавая новый. REST работает с объектами, которые называются ресурсами . POST: «../photos/« PUT: «../photos/ enjphoto_id}» GET: «../photos/« GET: «../photos/ndomphoto_id}» PS. Разделение загрузки на отдельную конечную точку может привести к непредсказуемому поведению. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Иван Проскуряков
6

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

lakhan_Ideavate
источник
6

Поскольку единственный отсутствующий пример - пример 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();
lifeisfoo
источник
Привет, что такое AndroidMultiPartEntity, пожалуйста, объясните ... и если я хочу загрузить файл pdf, word или xls, что мне нужно сделать, пожалуйста, дайте несколько советов ... я новичок в этом.
amit pandya
1
@amitpandya Я изменил код для загрузки общего файла, чтобы его было понятнее всем, кто его читает
lifeisfoo
2

Я хотел отправить несколько строк на бэкэнд-сервер. Я не использовал 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 для отправки.

Аслам Анвер
источник
1

Вы можете попробовать использовать https://square.github.io/okhttp/ библиотеку. Вы можете установить тело запроса multipart, а затем добавить объекты file и 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());
OneXer
источник
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
}
sunleo
источник
-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
Мариано Руис