Open chastabor opened 5 years ago
Well, right now you can create a custom response type to handle this.
One question is if setting the Content-Disposition
header should be default when responding with files.
Thoughts @shepmaster ?
I don't believe that we would want to add a Content-Disposition header as browsers already default to inline
if they recognize the file type.
On another note I'm still not sure how to create a custom response type for this. I've created a type and web impl as the following:
extern crate serde_derive;
#[macro_use]
extern crate tower_web;
extern crate tokio;
use tokio::fs::File;
use std::path::PathBuf;
use std::net::SocketAddr;
use tower_web::ServiceBuilder;
use tokio::net::TcpListener;
use std::env;
#[derive(Debug, Response)]
struct DownloadFile {
#[web(header)]
content_type: String,
#[web(header)]
content_disposition: String,
file: File,
}
#[derive(Debug)]
pub struct StaticFile {
root_dir: PathBuf,
}
impl_web! {
impl StaticFile {
#[get("/library/*relative_path")]
fn m4v(&self, relative_path: PathBuf) -> DownloadFile {
let mut path = self.root_dir.clone();
path.push(relative_path);
let filename = match relative_path.file_name() {
Some(f) => match f.to_str() {
Some(f) => f,
None => "default.m4v",
},
None => "default.m4v",
};
DownloadFile {
content_type: "video/x-m4v".to_string(),
content_disposition: format!("attachment; filename=\"{}\"", filename),
file: File::open(path).into(),
}
}
}
}
pub fn main() {
let addr = match env::var("ADDRESS") {
Ok(a) => a.to_owned(),
Err(_) => "127.0.0.1:8443".to_owned(),
};
let addr: SocketAddr = addr.parse().unwrap();
println!("Listening on http://{}", addr);
tokio::run({
ServiceBuilder::new()
.resource(StaticFile{
root_dir: "/var/lib/www".into(),
})
.serve(TcpListener::bind(&addr).unwrap().incoming())
});
}
with following cargo file:
[package]
name = "sfile"
version = "0.1.0"
edition = "2018"
[dependencies]
tower-web = "0.3.3"
tokio = "0.1.10"
serde = "1.0.44"
serde_derive = "1.0.44"
futures = "0.1.18"
and then I get the following error:
error[E0277]: the trait bound `DownloadFile: tokio::prelude::Future` is not satisfied
--> src/main.rs:28:1
|
28 | / impl_web! {
29 | | impl StaticFile {
30 | |
31 | | #[get("/library/*relative_path")]
... |
48 | | }
49 | | }
| |_^ the trait `tokio::prelude::Future` is not implemented for `DownloadFile`
|
= note: required because of the requirements on the impl of `tokio::prelude::IntoFuture` for `DownloadFile`
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: Could not compile `sfile`.
To learn more, run the command again with --verbose.
I guess I need to implement a future trait for the DownloadFile type? or is there a better way to do this?
You need to return a future of the DownloadFIle
type, so something like:
-> impl Future<Item = DownloadFile>
You can then just return Ok(DownloadFile).into_future()
(assuming you have the IntoFuture
trait in scope)`... I agree this is not ideal though.
Thanks for the response. I appreciate you looking at this. From what I can tell with the into_future() change the compiler now wants to serialize the file field instead of recognizing the field as a file type. I'm not sure how to prompt it to use that field as a File and not treat it as a serialized json response. So with the example code the error looks like the following:
error[E0277]: the trait bound `tokio_fs::file::File: __IMPL_RESPONSE_FOR_DownloadFile::_IMPL_SERIALIZE_FOR_ShadowDownloadFile::_serde::Serialize` is not satisfied
--> src/main.rs:20:5
|
20 | file: File,
| ^^^^ the trait `__IMPL_RESPONSE_FOR_DownloadFile::_IMPL_SERIALIZE_FOR_ShadowDownloadFile::_serde::Serialize` is not implemented for `tokio_fs::file::File`
|
= note: required by `__IMPL_RESPONSE_FOR_DownloadFile::_IMPL_SERIALIZE_FOR_ShadowDownloadFile::_serde::ser::SerializeStruct::serialize_field`
With the larger code base for my service I get the following error:
error[E0277]: the trait bound `tokio::fs::File: serde::Serialize` is not satisfied
--> src/main.rs:105:5
|
105 | file: File,
| ^^^^ the trait `serde::Serialize` is not implemented for `tokio::fs::File`
|
= note: required by `serde::ser::SerializeStruct::serialize_field`
Also if I can have it utilize the file response type, I'm thinking that the src/response/file.rs Response implementation will still not add any other headers other than the content_type_header saved in the context. Is that correct?
So here is a working example using the IntoFuture step you introduced above, via a flat out non-DRY duplication of your File Response trait implemented for the DownloadFile structure. I'm hoping this may clarify what I'm trying to do, if it was not clear before.
---------- Cargo.toml
[package]
name = "sfile"
version = "0.1.0"
edition = "2018"
[dependencies]
tower-web = { git = "https://github.com/carllerche/tower-web", rev = "2fee497a82a4", features = ["async-await-preview"] }
tokio = "0.1.10"
tokio-io = "0.1.7"
serde = "1.0.70"
serde_derive = "1.0.70"
futures = "0.1.21"
bytes = "0.4.7"
http = "0.1.7"
---------- src/main.rs
#[macro_use]
extern crate futures;
#[macro_use]
extern crate tower_web;
use tokio::fs::File;
use std::path::PathBuf;
use std::net::SocketAddr;
use tokio::net::TcpListener;
use std::env;
use std::fs::File as StdFile;
use futures::{ IntoFuture, Future, Async, Poll };
use std::io;
use bytes::BytesMut;
use http;
use tower_web::{
ServiceBuilder,
error,
response::{
Response,
Serializer,
Context,
},
util::BufStream,
};
use tokio_io::AsyncRead;
#[derive(Debug)]
struct DownloadFile {
content_type: String,
content_disposition: String,
file: File,
}
impl Response for DownloadFile {
type Buf = io::Cursor<BytesMut>;
type Body = error::Map<File>;
fn into_http<S: Serializer>(self, _context: &Context<S>) -> Result<http::Response<Self::Body>, tower_web::Error> {
let content_type = http::header::HeaderValue::from_str(&self.content_type).unwrap();
let content_disposition = http::header::HeaderValue::from_str(&self.content_disposition).unwrap();
Ok(http::Response::builder()
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.header(http::header::CONTENT_DISPOSITION, content_disposition)
.body(error::Map::new(self.file))
.unwrap())
}
}
impl BufStream for DownloadFile {
type Item = io::Cursor<BytesMut>;
type Error = io::Error;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
let mut v = BytesMut::with_capacity(8 * 1024);
let len = try_ready!(self.file.read_buf(&mut v));
if len == 0 {
Ok(Async::Ready(None))
} else {
Ok(Async::Ready(Some(io::Cursor::new(v))))
}
}
}
#[derive(Debug)]
pub struct StaticFile {
root_dir: PathBuf,
}
impl_web! {
impl StaticFile {
#[get("/library/*relative_path")]
fn m4v(&self, relative_path: PathBuf) -> impl Future<Item = DownloadFile, Error = io::Error> {
let mut path = self.root_dir.clone();
path.push(relative_path.clone());
let filename = match relative_path.file_name() {
Some(f) => match f.to_str() {
Some(f) => f,
None => "default.m4v",
},
None => "default.m4v",
};
match StdFile::open(path) {
Ok(file) => Ok(DownloadFile {
content_type: "video/x-m4v".to_string(),
content_disposition: format!("attachment; filename=\"{}\"", filename),
file: File::from_std(file),
}).into_future(),
Err(e) => Err(e).into_future(),
}
}
}
}
pub fn main() {
let addr = match env::var("ADDRESS") {
Ok(a) => a.to_owned(),
Err(_) => "127.0.0.1:8080".to_owned(),
};
let addr: SocketAddr = addr.parse().unwrap();
println!("Listening on http://{}", addr);
tokio::run({
ServiceBuilder::new()
.resource(StaticFile{
root_dir: "/var/lib/www".into(),
})
.serve(TcpListener::bind(&addr).unwrap().incoming())
});
}
Ok, I think the derive response macro may need to have an annotation to indicate that the file should be the body and not serialized w/ serde.
I need to have browsers automatically download some files. Is it currently possible to add a
Content-Disposition: attachment; filename="--file--"
header, where --file-- would dynamically be generated? I send this information along with file mime info for File types I wish to force the file to be downloaded instead of being displayed in the browser. If not what would be the best way to add that feature?