Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content-Disposition headers for File types #161

Open
chastabor opened this issue Nov 27, 2018 · 6 comments
Open

Content-Disposition headers for File types #161

chastabor opened this issue Nov 27, 2018 · 6 comments

Comments

@chastabor
Copy link
Contributor

chastabor commented Nov 27, 2018

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?

@carllerche
Copy link
Owner

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 ?

@chastabor
Copy link
Contributor Author

chastabor commented Dec 4, 2018

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?

@carllerche
Copy link
Owner

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.

@chastabor
Copy link
Contributor Author

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?

@chastabor
Copy link
Contributor Author

chastabor commented Dec 17, 2018

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())
    });
}

@carllerche
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants