aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/archive.rs24
-rw-r--r--src/args.rs16
-rw-r--r--src/config.rs37
-rw-r--r--src/errors.rs3
-rw-r--r--src/file_op.rs18
-rw-r--r--src/listing.rs39
-rw-r--r--src/main.rs56
-rw-r--r--src/pipe.rs2
-rw-r--r--src/renderer.rs20
-rw-r--r--src/webdav_fs.rs83
10 files changed, 226 insertions, 72 deletions
diff --git a/src/archive.rs b/src/archive.rs
index b8ba4d4..da79ef8 100644
--- a/src/archive.rs
+++ b/src/archive.rs
@@ -28,27 +28,27 @@ pub enum ArchiveMethod {
impl ArchiveMethod {
pub fn extension(self) -> String {
match self {
- ArchiveMethod::TarGz => "tar.gz",
- ArchiveMethod::Tar => "tar",
- ArchiveMethod::Zip => "zip",
+ Self::TarGz => "tar.gz",
+ Self::Tar => "tar",
+ Self::Zip => "zip",
}
.to_string()
}
pub fn content_type(self) -> String {
match self {
- ArchiveMethod::TarGz => "application/gzip",
- ArchiveMethod::Tar => "application/tar",
- ArchiveMethod::Zip => "application/zip",
+ Self::TarGz => "application/gzip",
+ Self::Tar => "application/tar",
+ Self::Zip => "application/zip",
}
.to_string()
}
pub fn is_enabled(self, tar_enabled: bool, tar_gz_enabled: bool, zip_enabled: bool) -> bool {
match self {
- ArchiveMethod::TarGz => tar_gz_enabled,
- ArchiveMethod::Tar => tar_enabled,
- ArchiveMethod::Zip => zip_enabled,
+ Self::TarGz => tar_gz_enabled,
+ Self::Tar => tar_enabled,
+ Self::Zip => zip_enabled,
}
}
@@ -69,9 +69,9 @@ impl ArchiveMethod {
{
let dir = dir.as_ref();
match self {
- ArchiveMethod::TarGz => tar_gz(dir, skip_symlinks, out),
- ArchiveMethod::Tar => tar_dir(dir, skip_symlinks, out),
- ArchiveMethod::Zip => zip_dir(dir, skip_symlinks, out),
+ Self::TarGz => tar_gz(dir, skip_symlinks, out),
+ Self::Tar => tar_dir(dir, skip_symlinks, out),
+ Self::Zip => zip_dir(dir, skip_symlinks, out),
}
}
}
diff --git a/src/args.rs b/src/args.rs
index 70ad208..f117b1c 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -1,8 +1,8 @@
use std::net::IpAddr;
use std::path::PathBuf;
+use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue};
use clap::{Parser, ValueEnum, ValueHint};
-use http::header::{HeaderMap, HeaderName, HeaderValue};
use crate::auth;
use crate::listing::{SortingMethod, SortingOrder};
@@ -162,7 +162,7 @@ pub struct CliArgs {
/// The provided path is not a physical file system path. Instead, it's relative to the serve
/// dir. For instance, if the serve dir is '/home/hello', set this to '/upload' to allow
/// uploading to '/home/hello/upload'.
- /// When specified via environment variable, a path always needs to the specified.
+ /// When specified via environment variable, a path always needs to be specified.
#[arg(short = 'u', long = "upload-files", value_hint = ValueHint::FilePath, num_args(0..=1), value_delimiter(','), env = "MINISERVE_ALLOWED_UPLOAD_DIR")]
pub allowed_upload_dir: Option<Vec<PathBuf>>,
@@ -203,7 +203,11 @@ pub struct CliArgs {
pub media_type_raw: Option<String>,
/// Enable overriding existing files during file upload
- #[arg(short = 'o', long = "overwrite-files", env = "OVERWRITE_FILES")]
+ #[arg(
+ short = 'o',
+ long = "overwrite-files",
+ env = "MINISERVE_OVERWRITE_FILES"
+ )]
pub overwrite_files: bool,
/// Enable uncompressed tar archive generation
@@ -313,6 +317,12 @@ pub struct CliArgs {
/// and return an error instead.
#[arg(short = 'I', long, env = "MINISERVE_DISABLE_INDEXING")]
pub disable_indexing: bool,
+
+ /// Enable read-only WebDAV support (PROPFIND requests)
+ ///
+ /// Currently incompatible with -P|--no-symlinks (see https://github.com/messense/dav-server-rs/issues/37)
+ #[arg(long, env = "MINISERVE_ENABLE_WEBDAV", conflicts_with = "no_symlinks")]
+ pub enable_webdav: bool,
}
/// Checks whether an interface is valid, i.e. it can be parsed into an IP address
diff --git a/src/config.rs b/src/config.rs
index 6e8b89e..449d2ae 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -5,8 +5,8 @@ use std::{
path::PathBuf,
};
+use actix_web::http::header::HeaderMap;
use anyhow::{anyhow, Context, Result};
-use http::HeaderMap;
#[cfg(feature = "tls")]
use rustls_pemfile as pemfile;
@@ -152,6 +152,9 @@ pub struct MiniserveConfig {
/// If enabled, indexing is disabled.
pub disable_indexing: bool,
+ /// If enabled, respond to WebDAV requests (read-only).
+ pub webdav_enabled: bool,
+
/// If set, use provided rustls config for TLS
#[cfg(feature = "tls")]
pub tls_rustls_config: Option<rustls::ServerConfig>,
@@ -196,13 +199,13 @@ impl MiniserveConfig {
// Otherwise, we should apply route_prefix to static files.
let (favicon_route, css_route) = if args.random_route {
(
- format!("/{}", nanoid::nanoid!(10, &ROUTE_ALPHABET)),
- format!("/{}", nanoid::nanoid!(10, &ROUTE_ALPHABET)),
+ "/__miniserve_internal/favicon.svg".into(),
+ "/__miniserve_internal/style.css".into(),
)
} else {
(
- format!("{}/{}", route_prefix, nanoid::nanoid!(10, &ROUTE_ALPHABET)),
- format!("{}/{}", route_prefix, nanoid::nanoid!(10, &ROUTE_ALPHABET)),
+ format!("{}/{}", route_prefix, "__miniserve_internal/favicon.ico"),
+ format!("{}/{}", route_prefix, "__miniserve_internal/style.css"),
)
};
@@ -226,24 +229,15 @@ impl MiniserveConfig {
let key_file = &mut BufReader::new(
File::open(&tls_key).context(format!("Couldn't access TLS key {tls_key:?}"))?,
);
- let cert_chain = pemfile::certs(cert_file).context("Reading cert file")?;
- let key = pemfile::read_all(key_file)
+ let cert_chain = pemfile::certs(cert_file)
+ .map(|cert| cert.expect("Invalid certificate in certificate chain"))
+ .collect();
+ let private_key = pemfile::private_key(key_file)
.context("Reading private key file")?
- .into_iter()
- .find_map(|item| match item {
- pemfile::Item::RSAKey(key)
- | pemfile::Item::PKCS8Key(key)
- | pemfile::Item::ECKey(key) => Some(key),
- _ => None,
- })
- .ok_or_else(|| anyhow!("No supported private key in file"))?;
+ .expect("No private key found");
let server_config = rustls::ServerConfig::builder()
- .with_safe_defaults()
.with_no_client_auth()
- .with_single_cert(
- cert_chain.into_iter().map(rustls::Certificate).collect(),
- rustls::PrivateKey(key),
- )?;
+ .with_single_cert(cert_chain, private_key)?;
Some(server_config)
} else {
None
@@ -281,7 +275,7 @@ impl MiniserveConfig {
.transpose()?
.unwrap_or_default();
- Ok(MiniserveConfig {
+ Ok(Self {
verbose: args.verbose,
path: args.path.unwrap_or_else(|| PathBuf::from(".")),
port,
@@ -319,6 +313,7 @@ impl MiniserveConfig {
show_wget_footer: args.show_wget_footer,
readme: args.readme,
disable_indexing: args.disable_indexing,
+ webdav_enabled: args.enable_webdav,
tls_rustls_config: tls_rustls_server_config,
compress_response: args.compress_response,
})
diff --git a/src/errors.rs b/src/errors.rs
index 600834b..f0e22ab 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -24,6 +24,9 @@ Please set an explicit serve path like: `miniserve /my/path`")]
/// In case miniserve was invoked with --no-symlinks but the serve path is a symlink
#[error("The -P|--no-symlinks option was provided but the serve path '{0}' is a symlink")]
NoSymlinksOptionWithSymlinkServePath(String),
+
+ #[error("The --enable-webdav option was provided, but the serve path '{0}' is a file")]
+ WebdavWithFileServePath(String),
}
#[derive(Debug, Error)]
diff --git a/src/file_op.rs b/src/file_op.rs
index 9f5902c..76a7234 100644
--- a/src/file_op.rs
+++ b/src/file_op.rs
@@ -61,7 +61,7 @@ async fn handle_multipart(
allow_hidden_paths: bool,
allow_symlinks: bool,
) -> Result<u64, RuntimeError> {
- let field_name = field.name().to_string();
+ let field_name = field.name().expect("No name field found").to_string();
match tokio::fs::metadata(&path).await {
Err(_) => Err(RuntimeError::InsufficientPermissionsError(
@@ -143,12 +143,16 @@ async fn handle_multipart(
};
}
- let filename = field.content_disposition().get_filename().ok_or_else(|| {
- RuntimeError::ParseError(
- "HTTP header".to_string(),
- "Failed to retrieve the name of the file to upload".to_string(),
- )
- })?;
+ let filename = field
+ .content_disposition()
+ .expect("No content-disposition field found")
+ .get_filename()
+ .ok_or_else(|| {
+ RuntimeError::ParseError(
+ "HTTP header".to_string(),
+ "Failed to retrieve the name of the file to upload".to_string(),
+ )
+ })?;
let filename_path = sanitize_path(Path::new(&filename), allow_hidden_paths)
.ok_or_else(|| RuntimeError::InvalidPathError("Invalid file name to upload".to_string()))?;
diff --git a/src/listing.rs b/src/listing.rs
index e24b41c..d908e23 100644
--- a/src/listing.rs
+++ b/src/listing.rs
@@ -3,7 +3,9 @@ use std::io;
use std::path::{Component, Path};
use std::time::SystemTime;
-use actix_web::{dev::ServiceResponse, web::Query, HttpMessage, HttpRequest, HttpResponse};
+use actix_web::{
+ dev::ServiceResponse, http::Uri, web::Query, HttpMessage, HttpRequest, HttpResponse,
+};
use bytesize::ByteSize;
use clap::ValueEnum;
use comrak::{markdown_to_html, ComrakOptions};
@@ -17,16 +19,26 @@ use crate::auth::CurrentUser;
use crate::errors::{self, RuntimeError};
use crate::renderer;
-use self::percent_encode_sets::PATH_SEGMENT;
+use self::percent_encode_sets::COMPONENT;
/// "percent-encode sets" as defined by WHATWG specs:
/// https://url.spec.whatwg.org/#percent-encoded-bytes
mod percent_encode_sets {
use percent_encoding::{AsciiSet, CONTROLS};
- const BASE: &AsciiSet = &CONTROLS.add(b'%');
- pub const QUERY: &AsciiSet = &BASE.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');
+ pub const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');
pub const PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}');
- pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'\\');
+ pub const USERINFO: &AsciiSet = &PATH
+ .add(b'/')
+ .add(b':')
+ .add(b';')
+ .add(b'=')
+ .add(b'@')
+ .add(b'[')
+ .add(b'\\')
+ .add(b']')
+ .add(b'^')
+ .add(b'|');
+ pub const COMPONENT: &AsciiSet = &USERINFO.add(b'$').add(b'%').add(b'&').add(b'+').add(b',');
}
/// Query parameters used by listing APIs
@@ -109,7 +121,7 @@ impl Entry {
last_modification_date: Option<SystemTime>,
symlink_info: Option<String>,
) -> Self {
- Entry {
+ Self {
name,
entry_type,
link,
@@ -141,7 +153,7 @@ pub struct Breadcrumb {
impl Breadcrumb {
fn new(name: String, link: String) -> Self {
- Breadcrumb { name, link }
+ Self { name, link }
}
}
@@ -173,7 +185,7 @@ pub fn directory_listing(
let base = Path::new(serve_path);
let random_route_abs = format!("/{}", conf.route_prefix);
let abs_uri = {
- let res = http::Uri::builder()
+ let res = Uri::builder()
.scheme(req.connection_info().scheme())
.authority(req.connection_info().host())
.path_and_query(req.uri().to_string())
@@ -214,7 +226,7 @@ pub fn directory_listing(
Component::Normal(s) => {
name = s.to_string_lossy().to_string();
link_accumulator
- .push_str(&(utf8_percent_encode(&name, PATH_SEGMENT).to_string() + "/"));
+ .push_str(&(utf8_percent_encode(&name, COMPONENT).to_string() + "/"));
}
_ => name = "".to_string(),
};
@@ -253,7 +265,7 @@ pub fn directory_listing(
.and_then(|path| std::fs::read_link(path).ok())
.map(|path| path.to_string_lossy().into_owned());
let file_url = base
- .join(utf8_percent_encode(&file_name, PATH_SEGMENT).to_string())
+ .join(utf8_percent_encode(&file_name, COMPONENT).to_string())
.to_string_lossy()
.to_string();
@@ -262,10 +274,7 @@ pub fn directory_listing(
if conf.no_symlinks && is_symlink {
continue;
}
- let last_modification_date = match metadata.modified() {
- Ok(date) => Some(date),
- Err(_) => None,
- };
+ let last_modification_date = metadata.modified().ok();
if metadata.is_dir() {
entries.push(Entry::new(
@@ -286,7 +295,7 @@ pub fn directory_listing(
symlink_dest,
));
if conf.readme && readme_rx.is_match(&file_name.to_lowercase()) {
- let ext = file_name.split('.').last().unwrap().to_lowercase();
+ let ext = file_name.split('.').next_back().unwrap().to_lowercase();
readme = Some((
file_name.to_string(),
if ext == "md" {
diff --git a/src/main.rs b/src/main.rs
index aa40585..ccf611c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,13 +6,18 @@ use std::time::Duration;
use actix_files::NamedFile;
use actix_web::{
dev::{fn_service, ServiceRequest, ServiceResponse},
- http::header::ContentType,
+ guard,
+ http::{header::ContentType, Method},
middleware, web, App, HttpRequest, HttpResponse, Responder,
};
use actix_web_httpauth::middleware::HttpAuthentication;
use anyhow::Result;
use clap::{crate_version, CommandFactory, Parser};
use colored::*;
+use dav_server::{
+ actix::{DavRequest, DavResponse},
+ DavConfig, DavHandler, DavMethodSet,
+};
use fast_qr::QRBuilder;
use log::{error, warn};
@@ -27,9 +32,11 @@ mod file_utils;
mod listing;
mod pipe;
mod renderer;
+mod webdav_fs;
use crate::config::MiniserveConfig;
use crate::errors::{RuntimeError, StartupError};
+use crate::webdav_fs::RestrictedFs;
static STYLESHEET: &str = grass::include!("data/style.scss");
@@ -52,9 +59,8 @@ fn main() -> Result<()> {
let miniserve_config = MiniserveConfig::try_from_args(args)?;
- run(miniserve_config).map_err(|e| {
+ run(miniserve_config).inspect_err(|e| {
errors::log_error_chain(e.to_string());
- e
})?;
Ok(())
@@ -89,6 +95,12 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {
));
}
+ if miniserve_config.webdav_enabled && miniserve_config.path.is_file() {
+ return Err(StartupError::WebdavWithFileServePath(
+ miniserve_config.path.to_string_lossy().to_string(),
+ ));
+ }
+
let inside_config = miniserve_config.clone();
let canon_path = miniserve_config
@@ -228,7 +240,7 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {
#[cfg(feature = "tls")]
let srv = match &miniserve_config.tls_rustls_config {
- Some(tls_config) => srv.listen_rustls(listener, tls_config.clone()),
+ Some(tls_config) => srv.listen_rustls_0_23(listener, tls_config.clone()),
None => srv.listen(listener),
};
@@ -308,7 +320,9 @@ fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders {
/// This is where we configure the app to serve an index file, the file listing, or a single file.
fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
let dir_service = || {
- let mut files = actix_files::Files::new("", &conf.path);
+ // use routing guard so propfind and options requests fall through to the webdav handler
+ let mut files = actix_files::Files::new("", &conf.path)
+ .guard(guard::Any(guard::Get()).or(guard::Head()));
// Use specific index file if one was provided.
if let Some(ref index_file) = conf.index {
@@ -377,6 +391,38 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
// Handle directories
app.service(dir_service());
}
+
+ if conf.webdav_enabled {
+ let fs = RestrictedFs::new(&conf.path, conf.show_hidden);
+
+ let dav_server = DavHandler::builder()
+ .filesystem(fs)
+ .methods(DavMethodSet::WEBDAV_RO)
+ .hide_symlinks(conf.no_symlinks)
+ .strip_prefix(conf.route_prefix.to_owned())
+ .build_handler();
+
+ app.app_data(web::Data::new(dav_server.clone()));
+
+ app.service(
+ // actix requires tail segment to be named, even if unused
+ web::resource("/{tail}*")
+ .guard(
+ guard::Any(guard::Options())
+ .or(guard::Method(Method::from_bytes(b"PROPFIND").unwrap())),
+ )
+ .to(dav_handler),
+ );
+ }
+}
+
+async fn dav_handler(req: DavRequest, davhandler: web::Data<DavHandler>) -> DavResponse {
+ if let Some(prefix) = req.prefix() {
+ let config = DavConfig::new().strip_prefix(prefix);
+ davhandler.handle_with(config, req.request).await.into()
+ } else {
+ davhandler.handle(req.request).await.into()
+ }
}
async fn error_404(req: HttpRequest) -> Result<HttpResponse, RuntimeError> {
diff --git a/src/pipe.rs b/src/pipe.rs
index 51f094a..45ada8b 100644
--- a/src/pipe.rs
+++ b/src/pipe.rs
@@ -17,7 +17,7 @@ pub struct Pipe {
impl Pipe {
/// Wrap the given sender in a `Pipe`.
pub fn new(destination: Sender<io::Result<Bytes>>) -> Self {
- Pipe {
+ Self {
dest: destination,
bytes: BytesMut::new(),
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 9af601c..035309d 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -1,6 +1,6 @@
use std::time::SystemTime;
-use actix_web::http::StatusCode;
+use actix_web::http::{StatusCode, Uri};
use chrono::{DateTime, Local};
use chrono_humanize::Humanize;
use clap::{crate_name, crate_version, ValueEnum};
@@ -9,7 +9,6 @@ use fast_qr::{
qr::QRCodeError,
QRBuilder,
};
-use http::Uri;
use maud::{html, Markup, PreEscaped, DOCTYPE};
use strum::{Display, IntoEnumIterator};
@@ -118,7 +117,7 @@ pub fn page(
}
}
}
- @if conf.mkdir_enabled {
+ @if conf.mkdir_enabled && upload_allowed {
div.toolbar_box {
form id="mkdir" action=(mkdir_action) method="POST" enctype="multipart/form-data" {
p { "Specify a directory name to create" }
@@ -358,10 +357,10 @@ pub enum ThemeSlug {
impl ThemeSlug {
pub fn css(&self) -> &str {
match self {
- ThemeSlug::Squirrel => grass::include!("data/themes/squirrel.scss"),
- ThemeSlug::Archlinux => grass::include!("data/themes/archlinux.scss"),
- ThemeSlug::Zenburn => grass::include!("data/themes/zenburn.scss"),
- ThemeSlug::Monokai => grass::include!("data/themes/monokai.scss"),
+ Self::Squirrel => grass::include!("data/themes/squirrel.scss"),
+ Self::Archlinux => grass::include!("data/themes/archlinux.scss"),
+ Self::Zenburn => grass::include!("data/themes/zenburn.scss"),
+ Self::Monokai => grass::include!("data/themes/monokai.scss"),
}
}
@@ -553,7 +552,12 @@ fn entry_row(
@if !raw {
@if let Some(size) = entry.size {
span.mobile-info.size {
- (maud::display(size))
+ (build_link("size", &format!("{}", size), sort_method, sort_order))
+ }
+ }
+ @if let Some(modification_timer) = humanize_systemtime(entry.last_modification_date) {
+ span.mobile-info.history {
+ (build_link("date", &modification_timer, sort_method, sort_order))
}
}
}
diff --git a/src/webdav_fs.rs b/src/webdav_fs.rs
new file mode 100644
index 0000000..cf434ba
--- /dev/null
+++ b/src/webdav_fs.rs
@@ -0,0 +1,83 @@
+//! Helper types and functions to allow configuring hidden files visibility
+//! for WebDAV handlers
+
+use dav_server::{davpath::DavPath, fs::*, localfs::LocalFs};
+use futures::{future::ready, StreamExt, TryFutureExt};
+use std::path::{Component, Path};
+
+/// A dav_server local filesystem backend that can be configured to deny access
+/// to files and directories with names starting with a dot.
+#[derive(Clone)]
+pub struct RestrictedFs {
+ local: Box<LocalFs>,
+ show_hidden: bool,
+}
+
+impl RestrictedFs {
+ /// Creates a new RestrictedFs serving the local path at "base".
+ /// If "show_hidden" is false, access to hidden files is prevented.
+ pub fn new<P: AsRef<Path>>(base: P, show_hidden: bool) -> Box<RestrictedFs> {
+ let local = LocalFs::new(base, false, false, false);
+ Box::new(RestrictedFs { local, show_hidden })
+ }
+}
+
+/// true if any normal component of path either starts with dot or can't be turned into a str
+fn path_has_hidden_components(path: &DavPath) -> bool {
+ path.as_pathbuf().components().any(|c| match c {
+ Component::Normal(name) => name.to_str().is_none_or(|s| s.starts_with('.')),
+ _ => false,
+ })
+}
+
+impl DavFileSystem for RestrictedFs {
+ fn open<'a>(
+ &'a self,
+ path: &'a DavPath,
+ options: OpenOptions,
+ ) -> FsFuture<'a, Box<dyn DavFile>> {
+ if !path_has_hidden_components(path) || self.show_hidden {
+ self.local.open(path, options)
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+
+ fn read_dir<'a>(
+ &'a self,
+ path: &'a DavPath,
+ meta: ReadDirMeta,
+ ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
+ if self.show_hidden {
+ self.local.read_dir(path, meta)
+ } else if !path_has_hidden_components(path) {
+ Box::pin(self.local.read_dir(path, meta).map_ok(|stream| {
+ let dyn_stream: FsStream<Box<dyn DavDirEntry>> = Box::pin(stream.filter(|entry| {
+ ready(match entry {
+ Ok(ref e) => !e.name().starts_with(b"."),
+ _ => false,
+ })
+ }));
+ dyn_stream
+ }))
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+
+ fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
+ if !path_has_hidden_components(path) || self.show_hidden {
+ self.local.metadata(path)
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+
+ fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
+ if !path_has_hidden_components(path) || self.show_hidden {
+ self.local.symlink_metadata(path)
+ } else {
+ Box::pin(ready(Err(FsError::NotFound)))
+ }
+ }
+}