about summary refs log tree commit diff
path: root/kittybox-rs/src/media/mod.rs
blob: 0ce2ec994d562a434f2d1e631f35e804a0335ee6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use axum::{
    extract::{Extension, Host, multipart::Multipart, Path},
    response::{IntoResponse, Response}, headers::HeaderValue,
};
use kittybox_util::error::{MicropubError, ErrorType};
use kittybox_indieauth::Scope;
use crate::indieauth::{User, backend::AuthBackend};

pub mod storage;
use storage::{MediaStore, MediaStoreError, Metadata, ErrorKind};
pub use storage::file::FileStore;

impl From<MediaStoreError> for MicropubError {
    fn from(err: MediaStoreError) -> Self {
        Self {
            error: ErrorType::InternalServerError,
            error_description: format!("{}", err)
        }
    }
}

#[tracing::instrument(skip(blobstore))]
pub(crate) async fn upload<S: MediaStore, A: AuthBackend>(
    mut upload: Multipart,
    Extension(blobstore): Extension<S>,
    user: User<A>
) -> Response {
    if !user.check_scope(&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(crate) async fn serve<S: MediaStore>(
    Host(host): Host,
    Path(path): Path<String>,
    Extension(blobstore): Extension<S>
) -> 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
                            .as_deref()
                            .unwrap_or("application/octet-stream")
                    ).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<S: MediaStore, A: AuthBackend>(blobstore: S, auth: A) -> axum::Router {
    axum::Router::new()
        .route("/", axum::routing::post(upload::<S, A>))
        .route("/uploads/*file", axum::routing::get(serve::<S>))
        .layer(axum::Extension(blobstore))
        .layer(axum::Extension(auth))
}