use axum::{ extract::{Extension, Host, multipart::{Multipart, MultipartError}, Path}, response::{IntoResponse, Response}, headers::HeaderValue, }; use kittybox_util::error::{MicropubError, ErrorType}; use crate::tokenauth::User; pub mod storage; use storage::{MediaStore, MediaStoreError, Metadata, ErrorKind}; pub use storage::file::FileStore; impl From for MicropubError { fn from(err: MediaStoreError) -> Self { Self { error: ErrorType::InternalServerError, error_description: format!("{}", err) } } } #[tracing::instrument(skip(blobstore))] pub async fn upload( mut upload: Multipart, Extension(blobstore): Extension, user: User ) -> Response { if !user.check_scope("media") { return MicropubError { error: ErrorType::NotAuthorized, error_description: "Interacting with the media storage requires the \"media\" scope.".to_owned() }.into_response(); } let host = user.me.host().unwrap().to_string() + &user.me.port().map(|i| format!(":{}", i)).unwrap_or_default(); let field = match upload.next_field().await { Ok(Some(field)) => field, Ok(None) => { return MicropubError { error: ErrorType::InvalidRequest, error_description: "Send multipart/form-data with one field named file".to_owned() }.into_response(); }, Err(err) => { return MicropubError { error: ErrorType::InternalServerError, error_description: format!("Error while parsing multipart/form-data: {}", err) }.into_response(); }, }; let metadata: Metadata = (&field).into(); match blobstore.write_streaming(&host, metadata, field).await { Ok(filename) => IntoResponse::into_response(( axum::http::StatusCode::CREATED, [ ("Location", user.me.join( &format!(".kittybox/media/uploads/{}", filename) ).unwrap().as_str()) ] )), Err(err) => MicropubError::from(err).into_response() } } #[tracing::instrument(skip(blobstore))] pub async fn serve( Host(host): Host, Path(path): Path, Extension(blobstore): Extension ) -> Response { use axum::http::StatusCode; tracing::debug!("Searching for file..."); match blobstore.read_streaming(&host, path.as_str()).await { Ok((metadata, stream)) => { tracing::debug!("Metadata: {:?}", metadata); let mut r = Response::builder(); { let headers = r.headers_mut().unwrap(); headers.insert( "Content-Type", HeaderValue::from_str(&metadata.content_type).unwrap() ); if let Some(length) = metadata.length { headers.insert( "Content-Length", HeaderValue::from_str(&length.to_string()).unwrap() ); } } r.body(axum::body::StreamBody::new(stream)).unwrap().into_response() }, Err(err) => match err.kind() { ErrorKind::NotFound => { IntoResponse::into_response(StatusCode::NOT_FOUND) }, _ => { tracing::error!("{}", err); IntoResponse::into_response(StatusCode::INTERNAL_SERVER_ERROR) } } } } pub fn router(blobstore: S) -> axum::Router { axum::Router::new() .route("/", axum::routing::post(upload::)) .route("/uploads/*file", axum::routing::get(serve::)) .layer(axum::Extension(blobstore)) }