rocket/fs/named_file.rs
1use std::io;
2use std::path::{Path, PathBuf};
3use std::ops::{Deref, DerefMut};
4
5use tokio::fs::File;
6
7use crate::request::Request;
8use crate::response::{self, Responder};
9use crate::http::ContentType;
10
11/// A [`Responder`] that sends file data with a Content-Type based on its
12/// file extension.
13///
14/// # Example
15///
16/// A simple static file server mimicking [`FileServer`]:
17///
18/// ```rust
19/// # use rocket::get;
20/// use std::path::{PathBuf, Path};
21///
22/// use rocket::fs::{NamedFile, relative};
23///
24/// #[get("/file/<path..>")]
25/// pub async fn second(path: PathBuf) -> Option<NamedFile> {
26/// let mut path = Path::new(relative!("static")).join(path);
27/// if path.is_dir() {
28/// path.push("index.html");
29/// }
30///
31/// NamedFile::open(path).await.ok()
32/// }
33/// ```
34///
35/// Always prefer to use [`FileServer`] which has more functionality and a
36/// pithier API.
37///
38/// [`FileServer`]: crate::fs::FileServer
39#[derive(Debug)]
40pub struct NamedFile(PathBuf, File);
41
42impl NamedFile {
43 /// Attempts to open a file in read-only mode.
44 ///
45 /// # Errors
46 ///
47 /// This function will return an error if path does not already exist. Other
48 /// errors may also be returned according to
49 /// [`OpenOptions::open()`](std::fs::OpenOptions::open()).
50 ///
51 /// # Example
52 ///
53 /// ```rust
54 /// # use rocket::get;
55 /// use rocket::fs::NamedFile;
56 ///
57 /// #[get("/")]
58 /// async fn index() -> Option<NamedFile> {
59 /// NamedFile::open("index.html").await.ok()
60 /// }
61 /// ```
62 pub async fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
63 // FIXME: Grab the file size here and prohibit `seek`ing later (or else
64 // the file's effective size may change), to save on the cost of doing
65 // all of those `seek`s to determine the file size. But, what happens if
66 // the file gets changed between now and then?
67 let file = File::open(path.as_ref()).await?;
68 Ok(NamedFile(path.as_ref().to_path_buf(), file))
69 }
70
71 /// Retrieve the underlying `File`.
72 ///
73 /// # Example
74 ///
75 /// ```rust
76 /// use rocket::fs::NamedFile;
77 ///
78 /// # async fn f() -> std::io::Result<()> {
79 /// let named_file = NamedFile::open("index.html").await?;
80 /// let file = named_file.file();
81 /// # Ok(())
82 /// # }
83 /// ```
84 #[inline(always)]
85 pub fn file(&self) -> &File {
86 &self.1
87 }
88
89 /// Retrieve a mutable borrow to the underlying `File`.
90 ///
91 /// # Example
92 ///
93 /// ```rust
94 /// use rocket::fs::NamedFile;
95 ///
96 /// # async fn f() -> std::io::Result<()> {
97 /// let mut named_file = NamedFile::open("index.html").await?;
98 /// let file = named_file.file_mut();
99 /// # Ok(())
100 /// # }
101 /// ```
102 #[inline(always)]
103 pub fn file_mut(&mut self) -> &mut File {
104 &mut self.1
105 }
106
107 /// Take the underlying `File`.
108 ///
109 /// # Example
110 ///
111 /// ```rust
112 /// use rocket::fs::NamedFile;
113 ///
114 /// # async fn f() -> std::io::Result<()> {
115 /// let named_file = NamedFile::open("index.html").await?;
116 /// let file = named_file.take_file();
117 /// # Ok(())
118 /// # }
119 /// ```
120 #[inline(always)]
121 pub fn take_file(self) -> File {
122 self.1
123 }
124
125 /// Retrieve the path of this file.
126 ///
127 /// # Examples
128 ///
129 /// ```rust
130 /// use rocket::fs::NamedFile;
131 ///
132 /// # async fn demo_path() -> std::io::Result<()> {
133 /// let file = NamedFile::open("foo.txt").await?;
134 /// assert_eq!(file.path().as_os_str(), "foo.txt");
135 /// # Ok(())
136 /// # }
137 /// ```
138 #[inline(always)]
139 pub fn path(&self) -> &Path {
140 self.0.as_path()
141 }
142}
143
144/// Streams the named file to the client. Sets or overrides the Content-Type in
145/// the response according to the file's extension if the extension is
146/// recognized. See [`ContentType::from_extension()`] for more information. If
147/// you would like to stream a file with a different Content-Type than that
148/// implied by its extension, use a [`File`] directly.
149impl<'r> Responder<'r, 'static> for NamedFile {
150 fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
151 let mut response = self.1.respond_to(req)?;
152 if let Some(ext) = self.0.extension() {
153 if let Some(ct) = ContentType::from_extension(&ext.to_string_lossy()) {
154 response.set_header(ct);
155 }
156 }
157
158 Ok(response)
159 }
160}
161
162impl Deref for NamedFile {
163 type Target = File;
164
165 fn deref(&self) -> &File {
166 &self.1
167 }
168}
169
170impl DerefMut for NamedFile {
171 fn deref_mut(&mut self) -> &mut File {
172 &mut self.1
173 }
174}