Image decay as a service

栏目: IT技术 · 发布时间: 4年前

内容简介:Since I write a lot ofarticles about Rust, I tend to get a lot of questions about specific crates: "Amos, what do you think ofAnd most of the time, I'm not sure what to responds. There's aNow, I personally think having so many crates available is a good th

Since I write a lot ofarticles about Rust, I tend to get a lot of questions about specific crates: "Amos, what do you think of oauth2-simd ? Is it better than openid-sse4 ? I think the latter has a lot of boilerplate."

And most of the time, I'm not sure what to responds. There's a lot of crates out there. I could probably review one crate a day until I retire!

Now, I personally think having so many crates available is a good th...

Cool bear's hot tip

Shhhhhh. Drop it and move on.

O..kay then.

Well, I recently relaunched my website as a completely custom-made web server on top of tide . And a week later, mostly out of curiosity (but not exclusively), I ported it over to warp .

So these I can review. And let's do so now.

The tide is rising (at its own pace)

I'll start with tide , as it's my personal favorite.

We'll build a small web app with it.

Cool bear's hot tip

Before proceeding - you're going to want a recent version of Rust.

If you're picking it up again after some time, make sure to run rustup update or equivalent, so that you have at least rustc 1.44.1 .

Also, the samples in this article are run on Linux.

Shell session

$ cargo new more-jpeg Created binary (application) `more-jpeg` package $ cd more-jpeg $ cargo add tide Adding tide v0.11.0 to dependencies

Now, the thing with Rust http servers, is that you're not choosing a single crate . A single decision will determine a lot of the other crates you depend on.

You should know that there efforts to bridge that gap are underway, and there's often solutions you can use to pick your favorites from either ecosystem.

Your mileage may vary. I had no problem using tokio::sync::broadcast inside my original tide-powered app. However, I wasn't able to use reqwest . This is a known issue , and I expect it'll be fixed over time.

More than anything else, I'm interested in showing you both approaches, and talk about their respective strengths. Think of it as two very good restaurants. The chefs may have their own take on a lot of things, but either way, you're getting a delicious meal.

So!

On tide's side, we're going to go with async-std , just like the README recommends:

Shell session

$ cargo add async-std

Actually, we'll also want to opt into the attributes feature of async-std , so let's edit our Cargo.toml a bit:

TOML markup

[dependencies] tide = "0.11.0" async-std = { version = "1.6.2", features = ["attributes"] }

Thanks to that, we can declare our main function as async :

Rust code

#[async_std::main] async fn main() { println!("Hello from async rust! (well, sort of)"); }

Shell session

$ cargo run --quiet Hello from async rust! (well, sort of)

Of course we're not actually doing any asynchronous work yet.

But we could!

Rust code

use async_std::{fs::File, io::prelude::*}; use std::{collections::hash_map::DefaultHasher, error::Error, hash::Hasher}; #[async_std::main] async fn main() -> Result<(), Box<dyn Error>> { let path = "./target/debug/more-jpeg"; let mut f = File::open(path).await?; let mut hasher = DefaultHasher::new(); let mut buf = vec![0u8; 1024]; loop { match f.read(&mut buf).await? { 0 => break, n => hasher.write(&buf[..n]), } } println!("{}: {:08x}", path, hasher.finish()); Ok(()) }

Shell session

$ cargo run --quiet ./target/debug/more-jpeg: b0d272206e97a665

Cool bear's hot tip

Two things not to do in this code sample:

  • Don't use DefaultHasher - the internal algorithm is not specified. It was used here to avoid adding a dependency just for a digression.
  • Don't use a 1KiB buffer. Also, in some cases, async_std::fs::read is a better idea.

Okay. Cool! That's not a web server though.

Let's serve up some text:

Rust code

use std::error::Error; #[async_std::main] async fn main() -> Result<(), Box<dyn Error>> { // Make a new tide app let mut app = tide::new(); // Handle the `/` route. // Note that async closures are still unstable - this // is a regular closure with an async block in it. // The argument we're discarding (with `_`) is the request. app.at("/").get(|_| async { Ok("Hello from tide") }); // The argument to `listen` is an `impl ToSocketAddrs`, // but it's async-std's `ToSocketAddrs`, so it accepts strings: app.listen("localhost:3000").await?; Ok(()) }

Shell session

$ cargo run --quiet & [1] 464865 $ curl http://localhost:3000 Hello from tide% $ kill %1 [1] + 464865 terminated cargo run --quiet

Cool bear's hot tip

The final % is not a typo - it's just zsh 's way of saying the command's output did not finish with a new line - but it inserted one anyway, otherwise our command prompt would be misaligned.

Image decay as a service

The prompt shown here is starship , by the way.

Text is cool and all, but how about some HTML?

Let's get fancy immediately and use liquid for templating:

html

<!-- in `templates/index.html.liquid` --> <!DOCTYPE html> <html lang="en"> <head> <title>More JPEG!</title> </head> <body> <p>Hello from <em>Tide</em>.</p> </body> </html>

Rust code

// in `src/main.rs` use async_std::fs::read_to_string; use liquid::Object; use std::error::Error; use tide::{Response, StatusCode}; #[async_std::main] async fn main() -> Result<(), Box<dyn Error>> { let mut app = tide::new(); app.at("/").get(|_| async { let path = "./templates/index.html.liquid"; let source = read_to_string(path).await.unwrap(); let compiler = liquid::ParserBuilder::with_stdlib().build().unwrap(); let template = compiler.parse(&source).unwrap(); let globals: Object = Default::default(); let markup = template.render(&globals).unwrap(); let mut res = Response::new(StatusCode::Ok); res.set_body(markup); Ok(res) }); app.listen("localhost:3000").await?; Ok(()) }

This code isn't good:

  • We're reading and compiling the template for every request
  • We unwrap() a lot - our app will panic if anything goes wrong

...but let's try it anyway.

Image decay as a service

Mhh.

We forgot something! In order to get a browser to render HTML, we have to set the content-type header:

Rust code

// new: `Mime` import use tide::{http::Mime, Response, StatusCode}; // new: `FromStr` import use std::{error::Error, str::FromStr}; #[async_std::main] async fn main() -> Result<(), Box<dyn Error>> { let mut app = tide::new(); app.at("/").get(|_| async { // omitted: everything up until `let markup` let mut res = Response::new(StatusCode::Ok); res.set_content_type(Mime::from_str("text/html; charset=utf-8").unwrap()); res.set_body(markup); Ok(res) }); app.listen("localhost:3000").await?; Ok(()) }

That should be better

Image decay as a service

Woo!

And it is!

Now let's look at how our server behaves:

Shell session

$ curl http://localhost:3000 <!DOCTYPE html> <html lang="en"> <head> <title>More JPEG!</title> </head> <body> <p>Hello from <em>Tide</em>.</p> </body> </html>%

So far so good.

Shell session

$ curl -I http://localhost:3000 HTTP/1.1 200 OK content-length: 162 date: Wed, 01 Jul 2020 11:45:25 GMT content-type: text/html;charset=utf-8

Seems okay.

Shell session

$ curl -X HEAD http://localhost:3000 Warning: Setting custom HTTP method to HEAD with -X/--request may not work the Warning: way you want. Consider using -I/--head instead. <!DOCTYPE html> <html lang="en"> <head> <title>More JPEG!</title> </head> <body> <p>Hello from <em>Tide</em>.</p> </body> </html>%

Woops, that's wrong! For http HEAD requests, a server should set the content-length header, but it "must not" actually send the body.

This is a bug in async-h1 , and there's already a fix in the works.

Shell session

$ curl -v -d 'a=b' http://localhost:3000 * Trying ::1:3000... * Connected to localhost (::1) port 3000 (#0) > POST / HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/7.70.0 > Accept: */* > Content-Length: 3 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 3 out of 3 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 405 Method Not Allowed < content-length: 0 < date: Wed, 01 Jul 2020 11:47:28 GMT < * Connection #0 to host localhost left intact

That is correct. We specified a GET handler, and it refuses to reply to POST .

Finally, let's request a route that doesn't exist:

Shell session

$ curl -v http://localhost:3000/nope * Trying ::1:3000... * Connected to localhost (::1) port 3000 (#0) > GET /nope HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/7.70.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 404 Not Found < content-length: 0 < date: Wed, 01 Jul 2020 11:58:37 GMT < * Connection #0 to host localhost left intact

Wonderful.

Let's look at this line:

Rust code

let mut res = Response::new(StatusCode::Ok);

Here we used a variant from the StatusCode enum - but we don't have to, we could also just use 200 , and it would work, because Response::new takes an <S: TryInto<StatusCode>> .

Now this line:

Rust code

res.set_content_type(Mime::from_str("text/html; charset=utf-8").unwrap());

Response::set_content_type takes an impl Into<Mime> . Before setting the content-type header, it'll check that the value we're setting it to is a valid mime type .

This is actually one of the few places I'm going to allow an unwrap() - you really should never be sending invalid mime types.

Okay - what about error handling?

Cool bear's hot tip

What about it?

Well, we don't actually want our server to crash. We want it to gracefully reply with an HTTP 500 .

Let's try refactoring our code to make our template serving code re-usable:

Rust code

use async_std::fs::read_to_string; use liquid::Object; use std::{error::Error, str::FromStr}; use tide::{http::Mime, Response, StatusCode}; #[async_std::main] async fn main() -> Result<(), Box<dyn Error>> { let mut app = tide::new(); app.at("/").get(|_| async { let path = "./templates/index.html.liquid"; serve_template(path).await }); app.listen("localhost:3000").await?; Ok(()) } async fn serve_template(path: &str) -> Result<Response, Box<dyn Error>> { let source = read_to_string(path).await?; let compiler = liquid::ParserBuilder::with_stdlib().build()?; let template = compiler.parse(&source)?; let globals: Object = Default::default(); let markup = template.render(&globals)?; let mut res = Response::new(StatusCode::Ok); res.set_content_type(Mime::from_str("text/html; charset=utf-8").unwrap()); res.set_body(markup); Ok(res) }

Shell session

$ cargo check Checking more-jpeg v0.1.0 (/home/amos/ftl/more-jpeg) error[E0271]: type mismatch resolving `<impl std::future::Future as std::future::Future>::Output == std::result::Result<_, http_types::error::Error>` --> src/main.rs:9:17 | 9 | app.at("/").get(|_| async { | ^^^ expected struct `std::boxed::Box`, found struct `http_types::error::Error` | = note: expected enum `std::result::Result<tide::response::Response, std::boxed::Box<dyn std::error::Error>>` found enum `std::result::Result<_, http_types::error::Error>` = note: required because of the requirements on the impl of `tide::endpoint::Endpoint<()>` for `[closure@src/main.rs:9:21: 12:6]`

Ah, that doesn't compile.

It looks like squints returning a Result is correct, but the Error type is wrong. Luckily, tide ship with its own Error type , so we can just map our error to it.

Let's even add a little bit of logging:

Shell session

$ cargo add log Updating 'https://github.com/rust-lang/crates.io-index' index Adding log v0.4.8 to dependencies $ cargo add pretty_env_logger Updating 'https://github.com/rust-lang/crates.io-index' index Adding pretty_env_logger v0.4.0 to dependencies

That way, we can log the error on the server, without showing visitors sensitive information:

Rust code

#[async_std::main] async fn main() -> Result<(), Box<dyn Error>> { if std::env::var_os("RUST_LOG").is_none() { std::env::set_var("RUST_LOG", "info"); } pretty_env_logger::init(); let mut app = tide::new(); app.at("/").get(|_| async { log::info!("Serving /"); let path = "./templates/index.html.liquid-notfound"; serve_template(path).await.map_err(|e| { log::error!("While serving template: {}", e); tide::Error::from_str( StatusCode::InternalServerError, "Something went wrong, sorry!", ) }) }); app.listen("localhost:3000").await?; Ok(()) }

Image decay as a service

I'm not super fond of the empty body there - Firefox just display a blank page, whereas Chromium shows its own 500 page. But we could always have our own error handling middleware! It's fixable.

Next up - what would we do if we wanted to parse templates at server startup? Instead of doing it on every request?

Shell session

$ cargo add thiserror Updating 'https://github.com/rust-lang/crates.io-index' index Adding thiserror v1.0.20 to dependencies

First, let's make a function to compile a bunch of templates:

Rust code

// new: `Template` import use liquid::{Object, Template}; // new: `HashMap` import use std::{error::Error, str::FromStr, collections::HashMap}; pub type TemplateMap = HashMap<String, Template>; #[derive(Debug, thiserror::Error)] enum TemplateError { #[error("invalid template path: {0}")] InvalidTemplatePath(String), } async fn compile_templates(paths: &[&str]) -> Result<TemplateMap, Box<dyn Error>> { let compiler = liquid::ParserBuilder::with_stdlib().build()?; let mut map = TemplateMap::new(); for path in paths { let name = path .split('/') .last() .map(|name| name.trim_end_matches(".liquid")) .ok_or_else(|| TemplateError::InvalidTemplatePath(path.to_string()))?; let source = read_to_string(path).await?; let template = compiler.parse(&source)?; map.insert(name.to_string(), template); } Ok(map) }

Next up, we can call it from main :

Rust code

async fn main() -> Result<(), Box<dyn Error>> { // (cut) let templates = compile_templates(&["./templates/index.html.liquid"]).await?; log::info!("{} templates compiled", templates.len()); // etc. }

This works well enough:

sh

Running `target/debug/more-jpeg` INFO more_jpeg > 1 templates compiled

But how do we use it from our handler? First we'll want to change our serve_template function:

Rust code

#[derive(Debug, thiserror::Error)] enum TemplateError { #[error("invalid template path: {0}")] InvalidTemplatePath(String), // new #[error("template not found: {0}")] TemplateNotFound(String), } async fn serve_template(templates: &TemplateMap, name: &str) -> Result<Response, Box<dyn Error>> { let template = templates .get(name) .ok_or_else(|| TemplateError::TemplateNotFound(name.to_string()))?; let globals: Object = Default::default(); let markup = template.render(&globals)?; let mut res = Response::new(StatusCode::Ok); res.set_content_type(Mime::from_str("text/html; charset=utf-8").unwrap()); res.set_body(markup); Ok(res) }

And adjust our route handler accordingly:

Rust code

app.at("/").get(|_| async { log::info!("Serving /"); let name = "index.html"; serve_template(&templates, name).await.map_err(|e| { log::error!("While serving template: {}", e); tide::Error::from_str( StatusCode::InternalServerError, "Something went wrong, sorry!", ) }) });

Right?

Shell session

$ cargo check Checking more-jpeg v0.1.0 (/home/amos/ftl/more-jpeg) error[E0373]: closure may outlive the current function, but it borrows `templates`, which is owned by the current function --> src/main.rs:17:21 | 17 | app.at("/").get(|_| async { | ^^^ may outlive borrowed value `templates` ... 20 | serve_template(&templates, name).await.map_err(|e| { | --------- `templates` is borrowed here | note: function requires argument type to outlive `'static` --> src/main.rs:17:5 | 17 | / app.at("/").get(|_| async { 18 | | log::info!("Serving /"); 19 | | let name = "index.html"; 20 | | serve_template(&templates, name).await.map_err(|e| { ... | 26 | | }) 27 | | }); | |______^ help: to force the closure to take ownership of `templates` (and any other referenced variables), use the `move` keyword | 17 | app.at("/").get(move |_| async { | ^^^^^^^^

Okay, uh, let's try move , if you say so rustc:

Rust code

app.at("/").get(move |_| async { // etc. });

Shell session

$ cargo check Checking more-jpeg v0.1.0 (/home/amos/ftl/more-jpeg) error: lifetime may not live long enough --> src/main.rs:17:30 | 17 | app.at("/").get(move |_| async { | _____________________--------_^ | | | | | | | return type of closure is impl std::future::Future | | lifetime `'1` represents this closure's body 18 | | log::info!("Serving /"); 19 | | let name = "index.html"; 20 | | serve_template(&templates, name).await.map_err(|e| { ... | 26 | | }) 27 | | }); | |_____^ returning this value requires that `'1` must outlive `'2` | = note: closure implements `Fn`, so references to captured variables can't escape the closure

Mhhhhh not quite.

What if we try to move move elsewhere? To our async block?

Rust code

app.at("/").get(|_| async move { // etc. });

Shell session

$ cargo check Checking more-jpeg v0.1.0 (/home/amos/ftl/more-jpeg) error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce` --> src/main.rs:17:21 | 17 | app.at("/").get(|_| async move { | _________________---_^^^^^^^^^^^^^^_- | | | | | | | this closure implements `FnOnce`, not `Fn` | | the requirement to implement `Fn` derives from here 18 | | log::info!("Serving /"); 19 | | let name = "index.html"; 20 | | serve_template(&templates, name).await.map_err(|e| { ... | 26 | | }) 27 | | }); | |_____- closure is `FnOnce` because it moves the variable `templates` out of its environment

Different text, same wall. This was definitely the biggest problem I encountered when first doing web development in Rust.

The problem is as follows:

templates
templates

In practice, we await the Future returned by app.listen() , so this isn't a problem as far as I can tell. That's just one of those cases where the borrow checker knows less than we do. It happens!

tide has a solution for that, though - you can simply put some state in your application.

Rust code

// new: `Request` import use tide::{http::Mime, Request, Response, StatusCode}; struct State { templates: TemplateMap, } #[async_std::main] async fn main() -> Result<(), Box<dyn Error>> { // cut let mut app = tide::with_state(State { templates }); app.listen("localhost:3000").await?; app.at("/").get(|req: Request<State>| async { log::info!("Serving /"); let name = "index.html"; serve_template(&req.state().templates, name) .await .map_err(|e| { // etc. }) }); Ok(()) }

That way, the application owns the State instance, and it hands out (counted) reference to it. Internally, it uses Arc , but that implementation detail is hidden.

The above almost compiles:

Shell session

$ cargo check Checking more-jpeg v0.1.0 (/home/amos/ftl/more-jpeg) error[E0373]: async block may outlive the current function, but it borrows `req`, which is owned by the current function --> src/main.rs:21:49 | 21 | app.at("/").get(|req: Request<State>| async { | _________________________________________________^ 22 | | log::info!("Serving /"); 23 | | let name = "index.html"; 24 | | serve_template(&req.state().templates, name) | | --- `req` is borrowed here ... | 32 | | }) 33 | | }); | |_____^ may outlive borrowed value `req` | note: async block is returned here --> src/main.rs:21:43 | 21 | app.at("/").get(|req: Request<State>| async { | ___________________________________________^ 22 | | log::info!("Serving /"); 23 | | let name = "index.html"; 24 | | serve_template(&req.state().templates, name) ... | 32 | | }) 33 | | }); | |_____^ help: to force the async block to take ownership of `req` (and any other referenced variables), use the `move` keyword | 21 | app.at("/").get(|req: Request<State>| async move { 22 | log::info!("Serving /"); 23 | let name = "index.html"; 24 | serve_template(&req.state().templates, name) 25 | .await 26 | .map_err(|e| { ...

And this time, rustc has the right idea. Since we have an async block within a closure, we want that async block to take ownership of the closure's arguments - so that it may live forever.

With that fix, everything compiles and run just as it did before.

Now let's try to make our app do something useful!

html

<!-- in `templates/index.html.liquid` --> <!DOCTYPE html> <html lang="en"> <head> <title>More JPEG!</title> <link href="https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap" rel="stylesheet"> <link href="/style.css" rel="stylesheet"> <script src="/main.js"></script> </head> <body> <p>You can always use more JPEG.</p> <div id="drop-zone"> Drop an image on me! </div> </body> </html>

css

// in `templates/style.css.liquid` body { max-width: 960px; margin: 20px auto; font-size: 1.8rem; padding: 2rem; } * { font-family: 'Indie Flower', cursive; } #drop-zone { width: 100%; height: 400px; border: 4px dashed #ccc; border-radius: 1em; display: flex; justify-content: center; align-items: center; } #drop-zone.over { border-color: #93b8ff; background: #f0f4fb; } .result { width: 100%; height: auto; }

JavaScript code

// in `templates/main.js.liquid` // @ts-check "use strict"; (function () { document.addEventListener("DOMContentLoaded", () => { /** @type {HTMLDivElement} */ let dropZone = document.querySelector("#drop-zone"); dropZone.addEventListener("dragover", (ev) => { ev.preventDefault(); ev.dataTransfer.dropEffect = "move"; dropZone.classList.add("over"); }); dropZone.addEventListener("dragleave", (ev) => { dropZone.classList.remove("over"); }); dropZone.addEventListener("drop", (ev) => { ev.preventDefault(); dropZone.classList.remove("over"); if (ev.dataTransfer.items && ev.dataTransfer.items.length > 0) { let item = ev.dataTransfer.items[0].getAsFile(); console.log("dropped file ", item.name); fetch("/upload", { method: "post", body: item, }).then((res) => { if (res.status !== 200) { throw new Error(`HTTP ${res.status}`); } return res.json(); }).then((payload) => { /** @type {HTMLImageElement} */ var img = document.createElement("img"); img.src = payload.src; img.classList.add("result"); dropZone.replaceWith(img); }).catch((e) => { alert(`Something went wrong!\n\n${e}`); }); } }); console.log("drop zone", dropZone); }); })();

We'll need to add those templates to our TemplateMap :

Rust code

let templates = compile_templates(&[ "./templates/index.html.liquid", "./templates/style.css.liquid", "./templates/main.js.liquid", ]) .await?;

And also adjust our serve_template helper to take a Mime !

Rust code

async fn serve_template( templates: &TemplateMap, name: &str, mime: Mime, ) -> Result<Response, Box<dyn Error>> { // (cut) res.set_content_type(mime); res.set_body(markup); Ok(res) }

Next up, we'll make ourselves a nice little collection of mime types:

Rust code

// still in `src/main.rs` mod mimes { use std::str::FromStr; use tide::http::Mime; pub(crate) fn html() -> Mime { Mime::from_str("text/html; charset=utf-8").unwrap() } pub(crate) fn css() -> Mime { Mime::from_str("text/css; charset=utf-8").unwrap() } pub(crate) fn js() -> Mime { Mime::from_str("text/javascript; charset=utf-8").unwrap() } }

And then we can adjust our route accordingly:

Rust code

serve_template(&req.state().templates, "index.html", mimes::html()) .await .map_err(|e| { log::error!("While serving template: {}", e); tide::Error::from_str( StatusCode::InternalServerError, "Something went wrong, sorry!", ) })

But we're going to have three more routes, and, you know the rule .

That error mapping logic is a bit lengthy, so let's simplify it a bit:

Rust code

trait ForTide { fn for_tide(self) -> Result<tide::Response, tide::Error>; } impl ForTide for Result<tide::Response, Box<dyn Error>> { fn for_tide(self) -> Result<Response, tide::Error> { self.map_err(|e| { log::error!("While serving template: {}", e); tide::Error::from_str( StatusCode::InternalServerError, "Something went wrong, sorry!", ) }) } }

And now, our handlers can be nice and tidy:

Rust code

let mut app = tide::with_state(State { templates }); app.at("/").get(|req: Request<State>| async move { serve_template(&req.state().templates, "index.html", mimes::html()) .await .for_tide() }); app.at("/style.css").get(|req: Request<State>| async move { serve_template(&req.state().templates, "style.css", mimes::css()) .await .for_tide() }); app.at("/main.js").get(|req: Request<State>| async move { serve_template(&req.state().templates, "main.js", mimes::js()) .await .for_tide() }); app.listen("localhost:3000").await?;

Let's try it out!

Image decay as a service

Wonderful!

Dropping an image doesn't work - yet:

Image decay as a service

But we can make it do something pretty easily.

Let's start with reading the whole body the browser sends us, encoding it with base64, and sending it back as a Data URL .

Shell session

$ cargo add base64 Updating 'https://github.com/rust-lang/crates.io-index' index Adding base64 v0.12.3 to dependencies

We're also going to need to be able to format a JSON response, and I can think of two crates that will work just fine:

Shell session

$ cargo add serde Updating 'https://github.com/rust-lang/crates.io-index' index Adding serde v1.0.114 to dependencies $ cargo add serde_json Updating 'https://github.com/rust-lang/crates.io-index' index Adding serde_json v1.0.56 to dependencies

Now. We'll need a struct to determine the shape of our JSON response:

Rust code

// in `src/main.rs` use serde::Serialize; #[derive(Serialize)] struct UploadResponse<'a> { src: &'a str, }

And then we're good to go:

Rust code

app.at("/upload") .post(|mut req: Request<State>| async move { let body = req.body_bytes().await?; let payload = base64::encode(body); let src = format!("data:image/jpeg;base64,{}", payload); let mut res = Response::new(StatusCode::Ok); res.set_content_type(tide::http::mime::JSON); res.set_body(tide::Body::from_json(&UploadResponse { src: &src })?); Ok(res) });


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机程序设计艺术卷1:基本算法(英文版.第3版)

计算机程序设计艺术卷1:基本算法(英文版.第3版)

Donald E.Knuth / 人民邮电出版社 / 2010-10 / 119.00元

《计算机程序设计艺术》系列著作对计算机领域产生了深远的影响。这一系列堪称一项浩大的工程,自1962年开始编写,计划出版7卷,目前已经出版了4卷。《美国科学家》杂志曾将这套书与爱因斯坦的《相对论》等书并列称为20世纪最重要的12本物理学著作。目前Knuth正将毕生精力投入到这部史诗性著作的撰写中。想了解本书最新信息,请访http://www-cs-faculty.stanford.edu/~knut......一起来看看 《计算机程序设计艺术卷1:基本算法(英文版.第3版)》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具