use std::cmp;
use std::io::Write;
use chrono::{DateTime, Utc};
use futures_util::StreamExt;
use headers::HeaderMapExt;
use http::{Request, Response, status::StatusCode};
use bytes::Bytes;
use crate::async_stream::AsyncStream;
use crate::body::Body;
use crate::conditional;
use crate::davheaders;
use crate::davpath::DavPath;
use crate::errors::*;
use crate::fs::*;
use crate::{DavInner, DavMethod};
struct Range {
start: u64,
count: u64,
}
const BOUNDARY: &str = "BOUNDARY";
const BOUNDARY_START: &str = "\n--BOUNDARY\n";
const BOUNDARY_END: &str = "\n--BOUNDARY--\n";
const READ_BUF_SIZE: usize = 16384;
impl<C: Clone + Send + Sync + 'static> DavInner<C> {
pub(crate) async fn handle_get(&self, req: &Request<()>) -> DavResult<Response<Body>> {
let head = req.method() == http::Method::HEAD;
let mut path = self.path(req);
if !self.autoindex.unwrap_or(false) && !head && path.as_bytes() == b"/remote.php/webdav/" {
let mut response = Response::new(Body::empty());
let headers = response.headers_mut();
headers.insert("Content-Length", "0".parse().unwrap());
headers.insert("Accept-Ranges", "bytes".parse().unwrap());
return Ok(response);
}
let meta = self.fs.metadata(&path, &self.credentials).await?;
if meta.is_dir() {
if !path.is_collection() {
let mut res = Response::new(Body::empty());
path.add_slash();
res.headers_mut().insert(
"Location",
path.with_prefix().as_url_string().parse().unwrap(),
);
res.headers_mut().typed_insert(headers::ContentLength(0));
*res.status_mut() = StatusCode::FOUND;
return Ok(res);
}
if let Some(indexfile) = self.indexfile.as_ref() {
path.push_segment(indexfile.as_bytes());
} else {
return self.handle_autoindex(req, head).await;
}
}
let mut file = self
.fs
.open(&path, OpenOptions::read(), &self.credentials)
.await?;
#[allow(unused_mut)]
let mut meta = file.metadata().await?;
if !meta.is_file() {
return Err(DavError::Status(StatusCode::METHOD_NOT_ALLOWED));
}
let len = meta.len();
let mut curpos = 0u64;
let file_etag = davheaders::ETag::from_meta(meta.as_ref());
let mut ranges = Vec::new();
let mut do_range = match req.headers().typed_try_get::<davheaders::IfRange>() {
Ok(Some(r)) => conditional::ifrange_match(&r, file_etag.as_ref(), meta.modified().ok()),
Ok(None) => true,
Err(_) => false,
};
let mut res = Response::new(Body::empty());
let mut no_body = false;
if let Ok(modified) = meta.modified() {
res.headers_mut()
.typed_insert(headers::LastModified::from(modified));
}
if let Some(etag) = file_etag {
res.headers_mut().typed_insert(etag);
}
if let Some(redirect) = self.redirect
&& redirect
&& let Some(url) = file.redirect_url().await?
{
res.headers_mut().insert("Location", url.parse().unwrap());
*res.status_mut() = StatusCode::FOUND;
return Ok(res);
}
res.headers_mut()
.typed_insert(headers::AcceptRanges::bytes());
if let Some(s) = conditional::if_match(
req,
Some(meta.as_ref()),
self.fs.as_ref(),
&self.ls,
&path,
&self.credentials,
)
.await
{
*res.status_mut() = s;
no_body = true;
do_range = false;
}
if do_range && let Some(r) = req.headers().typed_get::<headers::Range>() {
trace!("handle_gethead: range header {r:?}");
use std::ops::Bound::*;
for range in r.satisfiable_ranges(len) {
let (start, mut count, valid) = match range {
(Included(s), Included(e)) if e >= s => (s, e - s + 1, true),
(Included(s), Unbounded) if s <= len => (s, len - s, true),
(Unbounded, Included(n)) if n <= len => (len - n, n, true),
_ => (0, 0, false),
};
if !valid || start >= len {
let r = format!("bytes */{len}");
res.headers_mut()
.insert("Content-Range", r.parse().unwrap());
*res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
ranges.clear();
no_body = true;
break;
}
if start + count > len {
count = len - start;
}
ranges.push(Range { start, count });
}
}
if !ranges.is_empty() {
if file
.seek(std::io::SeekFrom::Start(ranges[0].start))
.await
.is_err()
{
let r = format!("bytes */{len}");
res.headers_mut()
.insert("Content-Range", r.parse().unwrap());
*res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
ranges.clear();
no_body = true;
}
}
if !ranges.is_empty() {
curpos = ranges[0].start;
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
if ranges.len() == 1 {
let r = format!(
"bytes {}-{}/{}",
ranges[0].start,
ranges[0].start + ranges[0].count - 1,
len
);
res.headers_mut()
.insert("Content-Range", r.parse().unwrap());
} else {
let r = format!("multipart/byteranges; boundary={BOUNDARY}");
res.headers_mut().insert("Content-Type", r.parse().unwrap());
}
} else {
ranges.push(Range {
start: 0,
count: len,
});
}
let content_type = path.get_mime_type_str();
if ranges.len() <= 1 {
res.headers_mut()
.typed_insert(davheaders::ContentType(content_type.to_owned()));
let notmod = res.status() == StatusCode::NOT_MODIFIED;
let len = if head || !no_body || notmod {
ranges[0].count
} else {
0
};
res.headers_mut().typed_insert(headers::ContentLength(len));
}
if head || no_body {
return Ok(res);
}
let read_buf_size = self.read_buf_size.unwrap_or(READ_BUF_SIZE);
*res.body_mut() = Body::from(AsyncStream::new(|mut tx| {
async move {
let zero = [0; 4096];
let multipart = ranges.len() > 1;
for range in ranges {
trace!(
"handle_get: start = {}, count = {}",
range.start, range.count
);
if curpos != range.start {
if let Err(_e) = file.seek(std::io::SeekFrom::Start(range.start)).await {
debug!("handle_get: failed to seek to {}: {:?}", range.start, _e);
continue;
}
curpos = range.start;
}
if multipart {
let mut hdrs = Vec::new();
let _ = write!(hdrs, "{BOUNDARY_START}");
let _ = writeln!(
hdrs,
"Content-Range: bytes {}-{}/{}",
range.start,
range.start + range.count - 1,
len
);
let _ = writeln!(hdrs, "Content-Type: {content_type}");
let _ = writeln!(hdrs);
tx.send(Bytes::from(hdrs)).await;
}
let mut count = range.count;
while count > 0 {
let blen = cmp::min(count, read_buf_size as u64) as usize;
let mut buf = file.read_bytes(blen).await?;
if buf.is_empty() {
let n = if count > 4096 { 4096 } else { count as usize };
buf = Bytes::copy_from_slice(&zero[..n]);
}
let len = buf.len() as u64;
count = count.saturating_sub(len);
curpos += len;
trace!("sending {len} bytes");
tx.send(buf).await;
}
}
if multipart {
tx.send(Bytes::from(BOUNDARY_END)).await;
}
Ok::<(), std::io::Error>(())
}
}));
Ok(res)
}
pub(crate) async fn handle_autoindex(
&self,
req: &Request<()>,
head: bool,
) -> DavResult<Response<Body>> {
let mut res = Response::new(Body::empty());
let path = self.path(req);
let allow_propfind = self
.allow
.map(|x| x.contains(DavMethod::PropFind))
.unwrap_or(false);
if !self.autoindex.unwrap_or(allow_propfind) {
debug!(
"method {} not allowed on request {}",
req.method(),
req.uri()
);
return Err(DavError::StatusClose(StatusCode::METHOD_NOT_ALLOWED));
}
let mut entries = self
.fs
.read_dir(&path, ReadDirMeta::Data, &self.credentials)
.await?;
res.headers_mut()
.insert("Content-Type", "text/html; charset=utf-8".parse().unwrap());
*res.status_mut() = StatusCode::OK;
if head {
return Ok(res);
}
*res.body_mut() = Body::from(AsyncStream::new(|mut tx| {
async move {
struct Dirent {
path: String,
name: String,
meta: Box<dyn DavMetaData>,
}
let mut dirents: Vec<Dirent> = Vec::new();
while let Some(dirent) = entries.next().await {
let dirent = match dirent {
Ok(dirent) => dirent,
Err(e) => {
trace!("next dir entry error happened. Skipping {e:?}");
continue;
}
};
let mut name = dirent.name();
if name.starts_with(b".") {
continue;
}
let mut npath = path.clone();
npath.push_segment(&name);
if let Ok(meta) = dirent.metadata().await {
if meta.is_dir() {
name.push(b'/');
npath.add_slash();
}
dirents.push(Dirent {
path: npath.with_prefix().as_url_string(),
name: String::from_utf8_lossy(&name).to_string(),
meta,
});
}
}
dirents.sort_by(|a, b| {
let adir = a.meta.is_dir();
let bdir = b.meta.is_dir();
if adir && !bdir {
std::cmp::Ordering::Less
} else if bdir && !adir {
std::cmp::Ordering::Greater
} else {
(a.name).cmp(&b.name)
}
});
let upath = htmlescape::encode_minimal(&path.with_prefix().as_url_string());
let mut w = String::new();
w.push_str(
"\
<!DOCTYPE html>\n\
<html><head>\n\
<meta name=\"referrer\" content=\"no-referrer\" />\n\
<title>Index of ",
);
w.push_str(&upath);
w.push_str("</title>\n");
w.push_str(
"\
<style>\n\
table {\n\
border-collapse: separate;\n\
border-spacing: 1.5em 0.25em;\n\
}\n\
h1 {\n\
padding-left: 0.3em;\n\
}\n\
a {\n\
text-decoration: none;\n\
color: blue;\n\
}\n\
.left {\n\
text-align: left;\n\
}\n\
.mono {\n\
font-family: monospace;\n\
}\n\
.mw20 {\n\
min-width: 20em;\n\
}\n\
</style>\n\
</head>\n\
<body>\n",
);
w.push_str(&format!("<h1>Index of {}</h1>", display_path(&path)));
w.push_str(
"\
<table>\n\
<tr>\n\
<th class=\"left mw20\">Name</th>\n\
<th class=\"left\">Last modified</th>\n\
<th>Size</th>\n\
</tr>\n\
<tr><th colspan=\"3\"><hr></th></tr>\n\
<tr>\n\
<td><a href=\"..\">Parent Directory</a></td>\n\
<td> </td>\n\
<td class=\"mono\" align=\"right\">[DIR] </td>\n\
</tr>\n",
);
tx.send(Bytes::from(w)).await;
for dirent in &dirents {
let modified = match dirent.meta.modified() {
Ok(t) => DateTime::<Utc>::from(t)
.format("%Y-%m-%d %H:%M")
.to_string(),
Err(_) => "".to_string(),
};
let size = match dirent.meta.is_file() {
true => display_size(dirent.meta.len()),
false => "[DIR] ".to_string(),
};
let name = htmlescape::encode_minimal(&dirent.name);
let s = format!(
"<tr><td><a href=\"{}\">{}</a></td><td class=\"mono\">{}</td><td class=\"mono\" align=\"right\">{}</td></tr>",
dirent.path, name, modified, size
);
tx.send(Bytes::from(s)).await;
}
let mut w = String::new();
w.push_str("<tr><th colspan=\"3\"><hr></th></tr>");
w.push_str("</table></body></html>");
tx.send(Bytes::from(w)).await;
Ok::<_, std::io::Error>(())
}
}));
Ok(res)
}
}
fn display_size(size: u64) -> String {
let (formatted, unit) = ["KiB", "MiB", "GiB", "TiB", "PiB"]
.iter()
.zip(1..)
.find(|(_, power)| size >= 1024u64.pow(*power) && size < 1024u64.pow(power + 1))
.map(|(unit, power)| (size as f64 / 1024u64.pow(power) as f64, unit))
.unwrap_or_else(|| (size as f64, &"B"));
format!("{} {}", (formatted * 100f64).round() / 100f64, unit)
}
fn display_path(path: &DavPath) -> String {
let path_dsp = String::from_utf8_lossy(path.with_prefix().as_bytes());
let path_url = path.with_prefix().as_url_string();
let dpath_segs = path_dsp
.split('/')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>();
let upath_segs = path_url
.split('/')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>();
let mut dpath = String::new();
let mut upath = String::new();
if dpath_segs.is_empty() {
dpath.push('/');
} else {
dpath.push_str("<a href = \"/\">/</a>");
}
for idx in 0..dpath_segs.len() {
upath.push('/');
upath.push_str(upath_segs[idx]);
let dseg = htmlescape::encode_minimal(dpath_segs[idx]);
if idx == dpath_segs.len() - 1 {
dpath.push_str(&dseg);
} else {
dpath.push_str(&format!("<a href = \"{upath}\">{dseg}</a>/"));
}
}
dpath
}