use dropshot::ApiDescription;
use dropshot::Body;
use dropshot::ConfigLogging;
use dropshot::ConfigLoggingLevel;
use dropshot::HttpError;
use dropshot::RequestContext;
use dropshot::ServerBuilder;
use dropshot::{endpoint, Path};
use http::{Response, StatusCode};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::PathBuf;
struct FileServerContext {
base: PathBuf,
}
#[tokio::main]
async fn main() -> Result<(), String> {
let config_logging =
ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Info };
let log = config_logging
.to_logger("example-basic")
.map_err(|error| format!("failed to create logger: {}", error))?;
let mut api = ApiDescription::new();
api.register(static_content).unwrap();
let context = FileServerContext { base: PathBuf::from(".") };
let server = ServerBuilder::new(api, context, log)
.start()
.map_err(|error| format!("failed to create server: {}", error))?;
server.await
}
#[derive(Deserialize, JsonSchema)]
struct AllPath {
path: Vec<String>,
}
#[endpoint {
method = GET,
/*
* Match literally every path including the empty path.
*/
path = "/{path:.*}",
/*
* This isn't an API so we don't want this to appear in the OpenAPI
* description if we were to generate it.
*/
unpublished = true,
}]
async fn static_content(
rqctx: RequestContext<FileServerContext>,
path: Path<AllPath>,
) -> Result<Response<Body>, HttpError> {
let path = path.into_inner().path;
let mut entry = rqctx.context().base.clone();
for component in &path {
if !entry.is_dir() {
return Err(HttpError::for_bad_request(
None,
format!("expected directory: {:?}", entry),
));
}
assert_ne!(component, ".");
assert_ne!(component, "..");
entry.push(component);
let m = entry.symlink_metadata().map_err(|e| {
HttpError::for_bad_request(
None,
format!("failed to access {:?}: {:#}", entry, e),
)
})?;
if m.file_type().is_symlink() {
return Err(HttpError::for_bad_request(
None,
format!("attempted to follow symlink {:?}", entry),
));
}
}
if entry.is_dir() {
let body = dir_body(&entry).await.map_err(|e| {
HttpError::for_bad_request(
None,
format!("failed to read dir {:?}: {:#}", entry, e),
)
})?;
Ok(Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, "text/html")
.body(body.into())?)
} else {
let file = tokio::fs::File::open(&entry).await.map_err(|e| {
HttpError::for_bad_request(
None,
format!("failed to read file {:?}: {:#}", entry, e),
)
})?;
let file_access = hyper_staticfile::vfs::TokioFileAccess::new(file);
let file_stream =
hyper_staticfile::util::FileBytesStream::new(file_access);
let body = Body::wrap(hyper_staticfile::Body::Full(file_stream));
let content_type = mime_guess::from_path(&entry)
.first()
.map_or_else(|| "text/plain".to_string(), |m| m.to_string());
Ok(Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, content_type)
.body(body)?)
}
}
async fn dir_body(dir_path: &PathBuf) -> Result<String, std::io::Error> {
let dir_link = dir_path.to_string_lossy();
let mut dir = tokio::fs::read_dir(&dir_path).await?;
let mut body = String::new();
body.push_str(
format!(
"<html>
<head><title>{}/</title></head>
<body>
<h1>{}/</h1>
",
dir_link, dir_link
)
.as_str(),
);
body.push_str("<ul>\n");
while let Some(entry) = dir.next_entry().await? {
let name = entry.file_name();
let name = name.to_string_lossy();
body.push_str(
format!(
r#"<li><a href="{}{}">{}</a></li>"#,
name,
if entry.file_type().await?.is_dir() { "/" } else { "" },
name
)
.as_str(),
);
body.push('\n');
}
body.push_str(
"</ul>
</body>
</html>\n",
);
Ok(body)
}