如何使用multipart/form-data和hyper发布图片?

8

我正在尝试使用超文本传输协议(HTTP)来上传图片文件,类似于cURL的方式:

curl -F smfile=@11.jpg https://httpbin.org/post --trace-ascii -

结果如下:
{
  "args": {},
  "data": "",
  "files": {
    "smfile": "data:image/jpeg;base64,..."
  },
  "form": {},
  "headers": {
    "Accept": "/",
    "Connection": "close",
    "Content-Length": "1709",
    "Content-Type": "multipart/form-data; boundary=------------------------58370e136081470e",
    "Expect": "100-continue",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.59.0"
  },
  "json": null,
  "origin": "myip",
  "url": "https://httpbin.org/post"
}

我了解到 Content-Type 应该设置为带有边界标记的 multipart/form-data。这是我的代码:
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate tokio;

use futures::{future, Future};
use hyper::header::CONTENT_TYPE;
use hyper::rt::Stream;
use hyper::{Body, Client, Method, Request};
use hyper_tls::HttpsConnector;
use std::fs::File;
use std::io::prelude::*;
use std::io::{self, Write};

const BOUNDARY: &'static str = "------------------------ea3bbcf87c101592";

fn main() {
    tokio::run(future::lazy(|| {
        let https = HttpsConnector::new(4).unwrap();
        let client = Client::builder().build::<_, hyper::Body>(https);

        let mut req = Request::new(Body::from(image_data()));

        req.headers_mut().insert(
            CONTENT_TYPE,
            format!("multipart/form-data; boundary={}", BOUNDARY)
                .parse()
                .unwrap(),
        );
        *req.method_mut() = Method::POST;
        *req.uri_mut() = "https://httpbin.org/post".parse().unwrap();

        client
            .request(req)
            .and_then(|res| {
                println!("status: {}", res.status());

                res.into_body().for_each(|chunk| {
                    io::stdout()
                        .write_all(&chunk)
                        .map_err(|e| panic!("stdout error: {}", e))
                })
            })
            .map_err(|e| println!("request error: {}", e))
    }));
}

fn image_data() -> Vec<u8> {
    let mut result: Vec<u8> = Vec::new();
    result.extend_from_slice(format!("--{}\r\n", BOUNDARY).as_bytes());
    result
        .extend_from_slice(format!("Content-Disposition: form-data; name=\"text\"\r\n").as_bytes());
    result.extend_from_slice("title\r\n".as_bytes());
    result.extend_from_slice(format!("--{}\r\n", BOUNDARY).as_bytes());
    result.extend_from_slice(
        format!("Content-Disposition: form-data; name=\"smfile\"; filename=\"11.jpg\"\r\n")
            .as_bytes(),
    );
    result.extend_from_slice("Content-Type: image/jpeg\r\n\r\n".as_bytes());

    let mut f = File::open("11.jpg").unwrap();
    let mut file_data = Vec::new();
    f.read_to_end(&mut file_data).unwrap();

    result.append(&mut file_data);

    result.extend_from_slice(format!("--{}--\r\n", BOUNDARY).as_bytes());
    result
}

注意:运行此代码需要一个名为11.jpg的JPEG文件,该文件可以是任何JPEG文件。

httpbin显示我没有发布任何内容:

(完整代码)

{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Connection": "close",
    "Content-Length": "1803",
    "Content-Type": "multipart/form-data; boundary=------------------------ea3bbcf87c101592",
    "Host": "httpbin.org"
  },
  "json": null,
  "origin": "myip",
  "url": "https://httpbin.org/post"
}

我不知道如何解决这个问题。


@E_net4 感谢您的评论,我已添加了代码。Rust playground无法工作,可能是由于我使用的额外crate导致的,所以我发布了gist链接。 - user2986683
2个回答

8

你没有在最后一个边界之前正确地放置换行符/回车符对。

这是我会写的生成正文的代码,需要更少的分配:

fn image_data() -> io::Result<Vec<u8>> {
    let mut data = Vec::new();
    write!(data, "--{}\r\n", BOUNDARY)?;
    write!(data, "Content-Disposition: form-data; name=\"smfile\"; filename=\"11.jpg\"\r\n")?;
    write!(data, "Content-Type: image/jpeg\r\n")?;
    write!(data, "\r\n")?;

    let mut f = File::open("11.jpg")?;
    f.read_to_end(&mut data)?;

    write!(data, "\r\n")?; // The key thing you are missing
    write!(data, "--{}--\r\n", BOUNDARY)?;

    Ok(data)
}

调用此代码也可以简化:

fn main() {
    let https = HttpsConnector::new(4).unwrap();
    let client = Client::builder().build::<_, hyper::Body>(https);

    let data = image_data().unwrap();
    let req = Request::post("https://httpbin.org/post")
        .header(CONTENT_TYPE, &*format!("multipart/form-data; boundary={}", BOUNDARY))
        .body(data.into())
        .unwrap();

    tokio::run(future::lazy(move || {
        client
            .request(req)
            .and_then(|res| {
                println!("status: {}", res.status());

                res.into_body().for_each(|chunk| {
                    io::stdout()
                        .write_all(&chunk)
                        .map_err(|e| panic!("stdout error: {}", e))
                })
            })
            .map_err(|e| println!("request error: {}", e))
    }));
}

谢谢您的回答,但我还有一个问题:我发现您的代码删除了 text 字段,我将我的代码更改为 result.extend_from_slice("title\r\n\r\n".as_bytes());(添加一个额外的 '\r\n'),但是 httpbin 返回了 "form":{"text":""},是否有一种方法可以为 text 赋值? - user2986683
我已经发现:在标题前添加“\r\n”,再次感谢 :) - user2986683

0

有一个板条箱可以为您完成这项任务,streamer 带有hyper 功能。 您只需编写以下内容:

use hyper::{Body, Request}:
let file = File::open("info").unwrap();
let mut streaming = Streamer::new(file)
streaming.meta.set_name("smfile"); // field name 
streaming.meta.set_filename("11.jpg"); // file name
let body: Body = streaming.streaming();
// build a request 
let request: Request<Body> = Request::post("<uri-here>").body(body).expect("failed to build a request");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接