breeze/src/view.rs

126 lines
4.0 KiB
Rust
Raw Normal View History

2023-01-30 17:11:30 -06:00
use std::{
path::{Component, PathBuf},
sync::Arc,
};
use axum::{
body::StreamBody,
extract::{Path, State},
response::{IntoResponse, Response},
};
use bytes::Bytes;
use hyper::{http::HeaderValue, StatusCode};
use tokio::{fs::File, runtime::Handle};
2023-01-30 17:11:30 -06:00
use tokio_util::io::ReaderStream;
2023-01-30 18:14:25 -06:00
pub enum ViewSuccess {
2023-01-30 17:11:30 -06:00
FromDisk(File),
FromCache(Bytes),
}
2023-01-30 18:14:25 -06:00
pub enum ViewError {
NotFound, // 404
2023-02-01 18:05:13 -06:00
InternalServerError, // 500
2023-01-30 18:14:25 -06:00
}
impl IntoResponse for ViewSuccess {
2023-01-30 17:11:30 -06:00
fn into_response(self) -> Response {
match self {
2023-01-30 18:14:25 -06:00
ViewSuccess::FromDisk(file) => {
// get handle to current runtime
// i use this to block on futures here (not async)
let handle = Handle::current();
let _ = handle.enter();
// read the metadata of the file on disk
// this function isn't async
// .. so we have to use handle.block_on() to get the metadata
let metadata = futures::executor::block_on(file.metadata());
// if we error then return 500
if metadata.is_err() {
error!("failed to read metadata from disk");
return ViewError::InternalServerError.into_response();
}
// unwrap (which we know is safe) and read the file size as a string
let metadata = metadata.unwrap();
let len_str = metadata.len().to_string();
debug!("file is {} bytes on disk", &len_str);
// HeaderValue::from_str will never error if only visible ASCII characters are passed (32-127)
// .. so unwrapping this should be fine
let content_length = HeaderValue::from_str(&len_str).unwrap();
2023-01-30 17:11:30 -06:00
// create a streamed body response (we want to stream larger files)
let reader = ReaderStream::new(file);
let stream = StreamBody::new(reader);
// extract mutable headers from the response
let mut res = stream.into_response();
let headers = res.headers_mut();
// clear headers, browser can imply content type
headers.clear();
// insert Content-Length header
// that way the browser shows how big a file is when it's being downloaded
headers.insert("Content-Length", content_length);
res
2023-01-30 17:11:30 -06:00
}
2023-01-30 18:14:25 -06:00
ViewSuccess::FromCache(data) => {
2023-01-30 17:11:30 -06:00
// extract mutable headers from the response
2023-06-28 16:24:07 -05:00
let mut res = data.into_response();
2023-01-30 17:11:30 -06:00
let headers = res.headers_mut();
// clear the headers, let the browser imply it
headers.clear();
res
}
}
}
}
2023-01-30 18:14:25 -06:00
impl IntoResponse for ViewError {
fn into_response(self) -> Response {
match self {
ViewError::NotFound => {
// convert string into response, change status code
let mut res = "not found!".into_response();
*res.status_mut() = StatusCode::NOT_FOUND;
res
}
ViewError::InternalServerError => {
// convert string into response, change status code
let mut res = "internal server error!".into_response();
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
2023-01-30 18:14:25 -06:00
res
}
}
}
}
2023-01-30 17:11:30 -06:00
#[axum::debug_handler]
pub async fn view(
State(engine): State<Arc<crate::engine::Engine>>,
Path(original_path): Path<PathBuf>,
2023-01-30 18:14:25 -06:00
) -> Result<ViewSuccess, ViewError> {
2023-01-30 17:11:30 -06:00
// (hopefully) prevent path traversal, just check for any non-file components
if original_path
.components()
.any(|x| !matches!(x, Component::Normal(_)))
{
2023-02-01 18:05:13 -06:00
warn!("a request attempted path traversal");
2023-01-30 18:14:25 -06:00
return Err(ViewError::NotFound);
2023-01-30 17:11:30 -06:00
}
2023-02-01 18:05:13 -06:00
// get result from the engine!
2023-01-30 17:11:30 -06:00
engine.get_upload(&original_path).await
}