rocket/fs/named_file.rs
1use std::io;
2use std::path::{Path, PathBuf};
3use std::ops::{Deref, DerefMut};
4
5use tokio::fs::{File, OpenOptions};
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 // TODO: 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 pub async fn open_with<P: AsRef<Path>>(path: P, opts: &OpenOptions) -> io::Result<NamedFile> {
72 let file = opts.open(path.as_ref()).await?;
73 Ok(NamedFile(path.as_ref().to_path_buf(), file))
74 }
75
76 /// Retrieve the underlying `File`.
77 ///
78 /// # Example
79 ///
80 /// ```rust
81 /// use rocket::fs::NamedFile;
82 ///
83 /// # async fn f() -> std::io::Result<()> {
84 /// let named_file = NamedFile::open("index.html").await?;
85 /// let file = named_file.file();
86 /// # Ok(())
87 /// # }
88 /// ```
89 #[inline(always)]
90 pub fn file(&self) -> &File {
91 &self.1
92 }
93
94 /// Retrieve a mutable borrow to the underlying `File`.
95 ///
96 /// # Example
97 ///
98 /// ```rust
99 /// use rocket::fs::NamedFile;
100 ///
101 /// # async fn f() -> std::io::Result<()> {
102 /// let mut named_file = NamedFile::open("index.html").await?;
103 /// let file = named_file.file_mut();
104 /// # Ok(())
105 /// # }
106 /// ```
107 #[inline(always)]
108 pub fn file_mut(&mut self) -> &mut File {
109 &mut self.1
110 }
111
112 /// Take the underlying `File`.
113 ///
114 /// # Example
115 ///
116 /// ```rust
117 /// use rocket::fs::NamedFile;
118 ///
119 /// # async fn f() -> std::io::Result<()> {
120 /// let named_file = NamedFile::open("index.html").await?;
121 /// let file = named_file.take_file();
122 /// # Ok(())
123 /// # }
124 /// ```
125 #[inline(always)]
126 pub fn take_file(self) -> File {
127 self.1
128 }
129
130 /// Retrieve the path of this file.
131 ///
132 /// # Examples
133 ///
134 /// ```rust
135 /// use rocket::fs::NamedFile;
136 ///
137 /// # async fn demo_path() -> std::io::Result<()> {
138 /// let file = NamedFile::open("foo.txt").await?;
139 /// assert_eq!(file.path().as_os_str(), "foo.txt");
140 /// # Ok(())
141 /// # }
142 /// ```
143 #[inline(always)]
144 pub fn path(&self) -> &Path {
145 self.0.as_path()
146 }
147}
148
149/// Streams the named file to the client. Sets or overrides the Content-Type in
150/// the response according to the file's extension if the extension is
151/// recognized. See [`ContentType::from_extension()`] for more information. If
152/// you would like to stream a file with a different Content-Type than that
153/// implied by its extension, use a [`File`] directly.
154impl<'r> Responder<'r, 'static> for NamedFile {
155 fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
156 let mut response = self.1.respond_to(req)?;
157 if let Some(ext) = self.0.extension() {
158 if let Some(ct) = ContentType::from_extension(&ext.to_string_lossy()) {
159 response.set_header(ct);
160 }
161 }
162
163 Ok(response)
164 }
165}
166
167impl Deref for NamedFile {
168 type Target = File;
169
170 fn deref(&self) -> &File {
171 &self.1
172 }
173}
174
175impl DerefMut for NamedFile {
176 fn deref_mut(&mut self) -> &mut File {
177 &mut self.1
178 }
179}