magiclen / rocket-multipart-form-data

This crate provides a multipart parser for the Rocket framework.
MIT License
35 stars 14 forks source link

How can I send an array of photos? #1

Closed sinitcin closed 5 years ago

sinitcin commented 5 years ago

Hello!

I want to send array of some photos to my application using JS. For example:

function uploadFile(file, i) { 
  var xhr = new XMLHttpRequest()
  xhr.open('POST', '/upload_multipart', true)
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
  xhr.upload.addEventListener("progress", function(e) {
    updateProgress(i, (e.loaded * 100.0 / e.total) || 100)
  })
  xhr.addEventListener('readystatechange', function(e) {
    if (xhr.readyState == 4 && xhr.status == 200) {
      updateProgress(i, 100)
    }
    else if (xhr.readyState == 4 && xhr.status != 200) {
      // Error
    }
  })

  var formData = new FormData()
  formData.append('image', file)
  formData.append('image', file)
  xhr.send(formData)
}

And I expect the next Rust code to read the array as RawField::Multiple(raws):


#[post("/upload_multipart", data = "<data>")]
fn upload_multipart(content_type: &ContentType, data: Data) -> RawResponse {
    let mut options = MultipartFormDataOptions::new();
    options.allowed_fields.push(
        MultipartFormDataField::raw("image")
            .size_limit(32 * 1024 * 1024)
            .content_type_by_string(Some(mime::IMAGE_STAR))
            .unwrap(),
    );

    let mut multipart_form_data = match MultipartFormData::parse(content_type, data, options) {
        Ok(multipart_form_data) => multipart_form_data,
        Err(err) => match err {
            MultipartFormDataError::DataTooLargeError(_) => {
                return RawResponse::from_vec(
                    "Размер файла слишком большой."
                        .bytes()
                        .collect(),
                    "",
                    Some(mime::TEXT_PLAIN_UTF_8),
                )
            }
            MultipartFormDataError::DataTypeError(_) => {
                return RawResponse::from_vec(
                    "Вы отправили не изображение."
                        .bytes()
                        .collect(),
                    "",
                    Some(mime::TEXT_PLAIN_UTF_8),
                )
            }
            _ => {
                return RawResponse::from_vec(
                    "Произошла ошибка при загрузке файла."
                        .bytes()
                        .collect(),
                    "",
                    Some(mime::TEXT_PLAIN_UTF_8),
                )
            }
        },
    };

    let image = multipart_form_data.raw.remove(&"image".to_string());

    match image {
        Some(image) => match image {
            RawField::Single(raw) => {
                let content_type = raw.content_type;
                let file_name = raw.file_name.unwrap_or("Image".to_string());
                let data = raw.raw;

                let mut file =
                    File::create(Path::new("upload").join(&file_name).to_str().unwrap()).unwrap();
                file.write_all(&data).unwrap();

                RawResponse::from_vec(data, file_name, content_type)
            }
            RawField::Multiple(raws) => {
                for raw in raws {
                    //let content_type = raw.content_type;
                    let file_name = raw.file_name.unwrap_or("Image".to_string());
                    let data = raw.raw;

                    let mut file =
                        File::create(Path::new("upload").join(&file_name).to_str().unwrap())
                            .unwrap();
                    file.write_all(&data).unwrap();

                    //RawResponse::from_vec(data, file_name, content_type)
                }
                unreachable!()
            }
        },
        None => RawResponse::from_vec(
            "Please input a file.".bytes().collect(),
            "",
            Some(mime::TEXT_PLAIN_UTF_8),
        ),
    }
}

And after xhr.send i have response 'Please input a file.'. What am I doing wrong?

magiclen commented 5 years ago

If you allow your program to read multiple image, you need to reuse the push method of allowed_fields to store multiple MultipartFormDataField instances for multiple image.

Also, I don't recommend you to use XMLHttpRequest + FormData to send multipart/form-data requests because of the poor compatibility.

sinitcin commented 5 years ago

Big thank you! Do I understand correctly that the request formed in this way will not be correct?

  formData.append('image[]', file1)
  formData.append('image[]', file2)
magiclen commented 5 years ago
formData.append('image[]', file1)
formData.append('image[]', file2)

I think it is correct but you should also change MultipartFormDataField::raw("image") to MultipartFormDataField::raw("image[]") and change multipart_form_data.raw.remove(&"image".to_string()); to multipart_form_data.raw.remove(&"image[]".to_string());

sinitcin commented 5 years ago

Sorry, but no. No matter how many times I call this code, I always get the first file. let image = multipart_form_data.raw.remove(&"image[]".to_string());

magiclen commented 5 years ago

Did you input more than two MultipartFormDataField instances to allowed_fields?

sinitcin commented 5 years ago

No, I thought it was like as in README.md example

options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
...
    if let Some(array) = array {
        match array {
            TextField::Single(text) => {
                let _content_type = &text.content_type;
                let _file_name = &text.file_name;
                let _text = &text.text;
                // You can now deal with the text data.
            }
            TextField::Multiple(texts) => {
                // Because we put "array_max_length_3" field to the allowed_fields for three times, this arm will probably be matched.

                for text in texts { // The max length of the "texts" variable is 3
                    let _content_type = &text.content_type;
                    let _file_name = &text.file_name;
                    let _text = &text.text;
                    // You can now deal with the text data.
                }
            }
        }
magiclen commented 5 years ago

Yes, it was.

...
options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
options.allowed_fields.push(MultipartFormDataField::text("array_max_length_3"));
...
if let Some(array) = array {
    match array {
        TextField::Single(text) => {
            let _content_type = &text.content_type;
            let _file_name = &text.file_name;
            let _text = &text.text;
            // You can now deal with the text data.
        }
        TextField::Multiple(texts) => {
            // Because we put "array_max_length_3" field to the allowed_fields for three times, this arm will probably be matched.

            for text in texts { // The max length of the "texts" variable is 3
                let _content_type = &text.content_type;
                let _file_name = &text.file_name;
                let _text = &text.text;
                // You can now deal with the text data.
            }
        }
    }
}
...
sinitcin commented 5 years ago

I understood. But if I don't know the size of the array? Should I transfer an additional field with the number of elements? Or is it possible to find out in Rust the size of the array in multipart / data?

magiclen commented 5 years ago

You can rewrite your upload_multipart function to,

#[post("/upload_multipart/<img_count>", data = "<data>")]
fn upload_multipart(content_type: &ContentType, data: Data, img_count: u32) -> RawResponse {
    if img_count > MAX_IMG_COUNT {
        ...
    }
    ...
    for _ in 0..img_count {
        options.allowed_fields.push(...);
    }
    ...
}
sinitcin commented 5 years ago

God bless you. Thanks a lot. It completely solves my problem.