Опублікування файлу та пов’язаних даних на 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.