Опублікування файлу та пов’язаних даних на RESTful WebService, бажано, як JSON


757

Це, мабуть, буде дурним питанням, але я маю одну з цих ночей. У програмі я розробляю RESTful API і ми хочемо, щоб клієнт надсилав дані як JSON. Частина цієї програми вимагає від клієнта завантаження файлу (як правило, зображення), а також інформації про зображення.

Мені важко відстежувати, як це відбувається в одному запиті. Чи можливо Base64 дані файлу в рядок JSON? Чи потрібно мені виконати 2 повідомлення на сервері? Чи не слід використовувати для цього JSON?

В якості додаткової примітки ми використовуємо Grails в інший час, і до цих служб звертаються рідні мобільні клієнти (iPhone, Android тощо), якщо щось із цього має значення.


1
Отже, який найкращий спосіб зробити це?
James111

3
Надішліть метадані в рядок запиту URL, а не JSON.
jrc

Відповіді:


632

Я задав подібне запитання тут:

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

У вас є три варіанти:

  1. Base64 кодує файл за рахунок збільшення розміру даних приблизно на 33% і додає накладні витрати як на сервері, так і на клієнті для кодування / декодування.
  2. Надішліть файл спочатку в multipart/form-dataпошті та поверніть ідентифікатор клієнту. Потім клієнт надсилає метадані з ідентифікатором, а сервер повторно пов'язує файл і метадані.
  3. Спочатку надішліть метадані та поверніть ідентифікатор клієнту. Потім клієнт надсилає файл з ідентифікатором, а сервер повторно пов'язує файл і метадані.

29
Якщо я вибрав варіант 1, чи просто я включати вміст Base64 всередині рядка JSON? {file: '234JKFDS # $ @ # $ MFDDMS ....', name: 'somename' ...} Або є щось більше?
Грегг

15
Грегг, точно так, як ви сказали, ви просто включите його як властивість, а значенням буде рядок, кодований base64. Це, мабуть, найпростіший метод, але може бути не практичним, залежно від розміру файлу. Наприклад, для нашого додатку нам потрібно надіслати зображення iPhone, які мають по 2-3 МБ кожен. Збільшення на 33% неприпустимо. Якщо ви надсилаєте лише невеликі зображення в 20 КБ, це накладні витрати можуть бути більш прийнятними.
Даніель Т.

19
Я також повинен зазначити, що кодування / декодування base64 також потребуватиме певного часу на обробку. Це може бути найпростіше зробити, але це, звичайно, не найкраще.
Даніель Т.

8
json з base64? хм .. Я замислююся над дотриманням багатопартійності / форми
всюдисущий

12
Чому заперечувати використання багаточастинних / форм-даних в одному запиті?
інстинкт

107

Ви можете надсилати файл і дані в одному запиті, використовуючи тип вмісту мультичасткових / форм-даних :

У багатьох додатках користувачеві може бути представлена ​​форма. Користувач заповнить форму, включаючи інформацію, яка вводиться, генерується за допомогою введення користувача або включається з файлів, обраних користувачем. Коли форма заповнена, дані з форми надсилаються від користувача до приймаючої програми.

Визначення MultiPart / Form-Data походить від одного з цих застосунків ...

Від http://www.faqs.org/rfcs/rfc2388.html :

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

Зміст-диспозиція: форми-дані; name = "користувач"

зі значенням, що відповідає запису поля "користувач".

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

Хороша річ у використанні мультичасткових / форм-даних - це те, що ви використовуєте заголовки, визначені HTTP, тому ви дотримуєтесь філософії REST щодо використання існуючих інструментів HTTP для створення вашої послуги.


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

Мені найбільше сподобалось ваше рішення. На жаль, видається, що у веб-переглядачі неможливо створити взаємні запити / пов'язані запити.
Петро Бодіс

Чи маєте ви досвід змусити клієнтів (особливо JS) спілкуватися з api таким чином
pvgoddijn

на жаль, наразі немає такого читача для подібних даних про php (7.2.1), і вам доведеться створити власний аналізатор
dewd

Прикро, що сервери та клієнти не мають для цього гарної підтримки.
Надер Ганбарі

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
Це здається більш "швидким" способом цього досягти.
Джеймс Вебстер

Операція POST для новостворених ресурсів повинна повернути ідентифікатор місцезнаходження в простому варіанті деталі об'єкта
Іван Проскуряков

@ivanproskuryakov чому "повинен"? У наведеному вище прикладі (POST у пункті 2) ідентифікатор файлу марний. Другий аргумент (для POST у пункті 2) я використовую форму однини '/ файл' (не '/ файли'), тому ідентифікатор не потрібен, оскільки шлях: / Проекти / 2 / фотографії / 3 / файл дають ПОВНУЮ інформацію в файл фотографії особи.
Kamil Kiełczewski

З специфікації протоколу HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Створено "Новостворений ресурс може бути посилається на URI, повернуті в об'єкті відповіді, з найбільш конкретним URI для ресурсу, заданим поле заголовка місцеположення. " @ KamilKiełczewski (один) та (два) можна об’єднати в одну операцію POST POST: / projects / {project_id} / photos поверне вам заголовок розташування, який може бути використаний для отримання однієї фотографії (ресурсу *) операції GET: щоб отримати єдине фото з усіма подробицями CGET: щоб отримати всю колекцію фотографій
Іван Проскуряков

1
Якщо метадані та завантаження є окремими операціями, то в кінцевих точках є такі проблеми: Для завантаження файлів використовується операція POST - POST не є ідентичним. PUT (idempotent) потрібно використовувати, оскільки ви змінюєте ресурс, не створюючи нового. 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
Іван Проскуряков

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 , я додам його. У цій техніці використовується спеціальний 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 з 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/ library. Ви можете встановити тіло запиту на багаточастинні, а потім додати файли та об'єкти 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
Маріано Руїз
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.