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;
|
2023-06-28 16:02:40 -05:00
|
|
|
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-11-09 20:22:02 -06:00
|
|
|
use tracing::{error, debug, warn};
|
2023-01-30 17:11:30 -06:00
|
|
|
|
2023-11-09 20:22:02 -06:00
|
|
|
/// Responses for a successful view operation
|
2023-01-30 18:14:25 -06:00
|
|
|
pub enum ViewSuccess {
|
2023-11-09 20:22:02 -06:00
|
|
|
/// A file read from disk, suitable for larger files.
|
|
|
|
///
|
|
|
|
/// The file provided will be streamed from disk and
|
|
|
|
/// back to the viewer.
|
|
|
|
///
|
|
|
|
/// This is only ever used if a file exceeds the
|
|
|
|
/// cache's maximum file size.
|
2023-01-30 17:11:30 -06:00
|
|
|
FromDisk(File),
|
2023-11-09 20:22:02 -06:00
|
|
|
|
|
|
|
/// A file read from in-memory cache, best for smaller files.
|
|
|
|
///
|
|
|
|
/// The file is taken from the cache in its entirety
|
|
|
|
/// and sent back to the viewer.
|
|
|
|
///
|
|
|
|
/// If a file can be fit into cache, this will be
|
|
|
|
/// used even if it's read from disk.
|
2023-01-30 17:11:30 -06:00
|
|
|
FromCache(Bytes),
|
|
|
|
}
|
|
|
|
|
2023-11-09 20:22:02 -06:00
|
|
|
/// Responses for a failed view operation
|
2023-01-30 18:14:25 -06:00
|
|
|
pub enum ViewError {
|
2023-11-09 20:22:02 -06:00
|
|
|
/// Will send status code 404 witha plaintext "not found" message.
|
|
|
|
NotFound,
|
|
|
|
|
|
|
|
/// Will send status code 500 with a plaintext "internal server error" message.
|
|
|
|
InternalServerError,
|
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) => {
|
2023-11-09 20:22:02 -06:00
|
|
|
// get handle to current tokio runtime
|
2023-06-28 16:02:40 -05:00
|
|
|
// 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);
|
|
|
|
|
2023-06-28 16:02:40 -05:00
|
|
|
// 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 {
|
2023-11-09 20:22:02 -06:00
|
|
|
ViewError::NotFound => (
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
"not found!"
|
|
|
|
).into_response(),
|
|
|
|
|
|
|
|
ViewError::InternalServerError => (
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
"internal server error!"
|
|
|
|
).into_response(),
|
2023-01-30 18:14:25 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-09 20:22:02 -06:00
|
|
|
/// The request handler for /p/* path.
|
|
|
|
/// All file views are handled here.
|
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
|
|
|
|
}
|