static_web_server/
directory_listing_download.rs1use async_compression::tokio::write::GzipEncoder;
10use async_tar::Builder;
11use bytes::BytesMut;
12use clap::ValueEnum;
13use headers::{ContentType, HeaderMapExt};
14use http::{HeaderValue, Method, Response};
15use hyper::{Body, body::Sender};
16use mime_guess::Mime;
17use std::fmt::Display;
18use std::path::Path;
19use std::path::PathBuf;
20use std::str::FromStr;
21use std::task::Poll::{Pending, Ready};
22use tokio::fs;
23use tokio::io;
24use tokio::io::AsyncWriteExt;
25use tokio_util::compat::TokioAsyncWriteCompatExt;
26
27use crate::Result;
28use crate::handler::RequestHandlerOpts;
29use crate::http_ext::MethodExt;
30
31pub const DOWNLOAD_PARAM_KEY: &str = "download";
33
34#[derive(Debug, Serialize, Deserialize, Clone, ValueEnum, Eq, Hash, PartialEq)]
36#[serde(rename_all = "lowercase")]
37pub enum DirDownloadFmt {
38 Targz,
40}
41
42impl Display for DirDownloadFmt {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 std::fmt::Debug::fmt(self, f)
45 }
46}
47
48pub struct DirDownloadOpts<'a> {
50 pub method: &'a Method,
52 pub disable_symlinks: bool,
54 pub ignore_hidden_files: bool,
56}
57
58pub fn init(formats: &Vec<DirDownloadFmt>, handler_opts: &mut RequestHandlerOpts) {
60 for fmt in formats {
61 if !handler_opts.dir_listing_download.contains(fmt) {
63 tracing::info!("directory listing download: enabled format {}", &fmt);
64 handler_opts.dir_listing_download.push(fmt.to_owned());
65 }
66 }
67 tracing::info!(
68 "directory listing download: enabled={}",
69 !handler_opts.dir_listing_download.is_empty()
70 );
71}
72
73pub struct ChannelBuffer {
75 s: Sender,
76}
77
78impl tokio::io::AsyncWrite for ChannelBuffer {
79 fn poll_write(
80 self: std::pin::Pin<&mut Self>,
81 cx: &mut std::task::Context<'_>,
82 buf: &[u8],
83 ) -> std::task::Poll<Result<usize, std::io::Error>> {
84 let this = self.get_mut();
85 let b = BytesMut::from(buf);
86 match this.s.poll_ready(cx) {
87 Ready(r) => match r {
88 Ok(()) => match this.s.try_send_data(b.freeze()) {
89 Ok(_) => Ready(Ok(buf.len())),
90 Err(_) => Pending,
91 },
92 Err(e) => Ready(Err(io::Error::new(io::ErrorKind::BrokenPipe, e))),
93 },
94 Pending => Pending,
95 }
96 }
97
98 fn poll_flush(
99 self: std::pin::Pin<&mut Self>,
100 _cx: &mut std::task::Context<'_>,
101 ) -> std::task::Poll<Result<(), std::io::Error>> {
102 std::task::Poll::Ready(Ok(()))
103 }
104
105 fn poll_shutdown(
106 self: std::pin::Pin<&mut Self>,
107 _cx: &mut std::task::Context<'_>,
108 ) -> std::task::Poll<Result<(), std::io::Error>> {
109 std::task::Poll::Ready(Ok(()))
110 }
111}
112
113async fn archive(
114 path: PathBuf,
115 src_path: PathBuf,
116 cb: ChannelBuffer,
117 follow_symlinks: bool,
118 ignore_hidden: bool,
119) -> Result {
120 let gz = GzipEncoder::with_quality(cb, async_compression::Level::Default);
121 let mut a = Builder::new(gz.compat_write());
122 a.follow_symlinks(follow_symlinks);
123
124 let mut stack = vec![(src_path.to_path_buf(), true, false)];
131 while let Some((src, is_dir, is_symlink)) = stack.pop() {
132 let dest = path.join(src.strip_prefix(&src_path)?);
133
134 if is_dir || (is_symlink && follow_symlinks && src.is_dir()) {
136 let mut entries = fs::read_dir(&src).await?;
137 while let Some(entry) = entries.next_entry().await? {
138 let name = entry.file_name();
140 if ignore_hidden && name.as_encoded_bytes().first().is_some_and(|c| *c == b'.') {
141 continue;
142 }
143
144 let file_type = entry.file_type().await?;
145 stack.push((entry.path(), file_type.is_dir(), file_type.is_symlink()));
146 }
147 if dest != Path::new("") {
148 a.append_dir(&dest, &src).await?;
149 }
150 } else {
151 a.append_path_with_name(src, &dest).await?;
153 }
154 }
155
156 a.finish().await?;
157 a.into_inner().await?.into_inner().shutdown().await?;
159
160 Ok(())
161}
162
163pub fn archive_reply<P, Q>(path: P, src_path: Q, opts: DirDownloadOpts<'_>) -> Response<Body>
169where
170 P: AsRef<Path>,
171 Q: AsRef<Path>,
172{
173 let archive_name = path.as_ref().with_extension("tar.gz");
174 let mut resp = Response::new(Body::empty());
175
176 resp.headers_mut().typed_insert(ContentType::from(
177 Mime::from_str("application/gzip").unwrap(),
179 ));
180 let hvals = format!(
181 "attachment; filename=\"{}\"",
182 archive_name.to_string_lossy()
183 );
184 match HeaderValue::from_str(hvals.as_str()) {
185 Ok(hval) => {
186 resp.headers_mut()
187 .insert(hyper::header::CONTENT_DISPOSITION, hval);
188 }
189 Err(err) => {
190 tracing::error!("can't make content disposition from {}: {:?}", hvals, err);
193 }
194 }
195
196 if opts.method.is_head() {
198 return resp;
199 }
200
201 let (tx, body) = Body::channel();
202 tokio::task::spawn(archive(
203 path.as_ref().into(),
204 src_path.as_ref().into(),
205 ChannelBuffer { s: tx },
206 !opts.disable_symlinks,
207 opts.ignore_hidden_files,
208 ));
209 *resp.body_mut() = body;
210
211 resp
212}