RustのHTTPクライアントのureqでmultipartを送信してみる

# 目的
[ureq](https://crates.io/crates/ureq)はcrateの依存関係が少ない軽量なhttpclientなんですが、まだmultipartをサポートしていません。そこで自前でmultipartを作って送信することにしました。自分はTwitterAPIを呼び出したかったので、最低限それができる程度にしか作っていません。

# コード
```rust:Cargo.toml
[package]
name = "test"
version = "0.1.0"
edition = "2018"

[dependencies]
bytebuffer = "^0.2"
rand = "^0.7"
twapi-oauth = "^0.1"
ureq = { version = "^0.11", features = ["json"] }
```

```rust:main.rs
use bytebuffer::ByteBuffer;
use rand::seq::SliceRandom;
use std::env;
use std::io::{BufReader, Cursor, Read};
use twapi_oauth::oauth1_authorization_header;

pub struct MultiPart {
    pub boundary: String,
    buffer: ByteBuffer,
    before_flag: bool,
}

impl MultiPart {
    pub fn new() -> Self {
        let mut rng = &mut rand::thread_rng();
        let postfix = String::from_utf8(
            "-_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
                .as_bytes()
                .choose_multiple(&mut rng, 30)
                .cloned()
                .collect(),
        )
        .unwrap();
        Self {
            boundary: format!("---------------------------{}", postfix),
            buffer: ByteBuffer::new(),
            before_flag: true,
        }
    }

    pub fn to_bytes(&mut self) -> Vec<u8> {
        if self.before_flag {
            self.write_boundary(true);
            self.before_flag = false;
        }
        self.buffer.to_bytes()
    }

    pub fn add_string(&mut self, name: &str, data: &str) {
        self.write_boundary(false);
        self.write_header(name);
        self.buffer.write_bytes(&format!("{}\r\n", data).as_bytes());
    }

    pub fn add_cursor(&mut self, name: &str, cursor: Cursor<Vec<u8>>) {
        self.write_boundary(false);
        self.write_header(name);
        self.buffer.write_bytes(cursor.get_ref());
        self.buffer.write_bytes("\r\n".as_bytes());
    }

    fn write_header(&mut self, name: &str) {
        self.buffer.write_bytes(
            &format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", name).as_bytes(),
        );
    }

    fn write_boundary(&mut self, last_flag: bool) {
        self.buffer.write_bytes("--".as_bytes());
        self.buffer.write_bytes(&self.boundary.as_bytes());
        if last_flag {
            self.buffer.write_bytes("--".as_bytes());
        } else {
            self.buffer.write_bytes("\r\n".as_bytes());
        }
    }
}

fn main() {
    let consumer_key = env::var("CONSUMER_KEY").unwrap();
    let consumer_secret = env::var("CONSUMER_SECRET").unwrap();
    let access_key = env::var("ACCESS_KEY").unwrap();
    let access_secret = env::var("ACCESS_SECRET").unwrap();

    let metadata = std::fs::metadata("test.jpg").unwrap();
    let file_size = metadata.len();
    let f = std::fs::File::open("test.jpg").unwrap();
    let mut cursor = Cursor::new(vec![0; file_size as usize]);
    let mut reader = BufReader::new(f);
    reader.read(cursor.get_mut()).unwrap();

    let mut data = MultiPart::new();
    data.add_cursor("media", cursor);

    let url = "https://upload.twitter.com/1.1/media/upload.json";
    let authorization = oauth1_authorization_header(
        &consumer_key,
        &consumer_secret,
        &access_key,
        &access_secret,
        "POST",
        url,
        &vec![],
    );

    let response = ureq::post(url)
        .set("Authorization", &authorization)
        .set(
            "Content-Type",
            &format!("multipart/form-data; boundary={}", data.boundary),
        )
        .send_bytes(&data.to_bytes());
    println!("{:?}", response.into_json());
}
```

# さいごに
ureqでTwitterAPIを呼び出すライブラリを作成しました。
[twapi-ureq](https://crates.io/crates/twapi-ureq)

# 参考
[[Rust] ureq - reqwest の軽量な代替クレート](https://qiita.com/osanshouo/items/adadfe6bbcbb201cbaed)
[multipart/form-dataのリクエストで地味にハマったメモ](https://qiita.com/Zaki_Tk/items/073f597d52f6fd8e3dcd)
[Rustでランダム文字列を作成する](https://qiita.com/aoyagikouhei/items/b796632ff6581197737c)