Skip to main content

kellnr_docs/
api.rs

1use axum::Json;
2use axum::extract::{Path, State};
3use axum::response::Redirect;
4use kellnr_appstate::{AppState, DbState, SettingsState};
5use kellnr_auth::token::Token;
6use kellnr_common::original_name::OriginalName;
7use kellnr_common::version::Version;
8use kellnr_error::api_error::ApiResult;
9use kellnr_registry::kellnr_api::check_ownership;
10
11use crate::doc_archive::DocArchive;
12use crate::doc_queue_response::DocQueueResponse;
13use crate::docs_error::DocsError;
14use crate::upload_response::DocUploadResponse;
15use crate::{compute_doc_url, get_latest_version_with_doc};
16
17pub async fn docs_in_queue(State(db): DbState) -> ApiResult<Json<DocQueueResponse>> {
18    let doc = db.get_doc_queue().await?;
19    Ok(Json(DocQueueResponse::from(doc)))
20}
21
22pub async fn latest_docs(
23    Path(package): Path<OriginalName>,
24    State(settings): SettingsState,
25    State(db): DbState,
26) -> Redirect {
27    let name = package.to_normalized();
28    let opt_doc_version = get_latest_version_with_doc(&name, &settings);
29    let res_db_version = db.get_max_version_from_name(&name).await;
30
31    if let Some(doc_version) = opt_doc_version
32        && let Ok(db_version) = res_db_version
33        && doc_version == db_version
34    {
35        return Redirect::temporary(&compute_doc_url(&name, &db_version, &settings.origin.path));
36    }
37
38    Redirect::temporary("/")
39}
40
41pub async fn publish_docs(
42    Path((package, version)): Path<(OriginalName, Version)>,
43    token: Token,
44    State(state): AppState,
45    mut docs: DocArchive,
46) -> ApiResult<Json<DocUploadResponse>> {
47    let db = state.db;
48    let settings = state.settings;
49    let normalized_name = package.to_normalized();
50    let crate_version = &version.to_string();
51
52    // Check if crate with the version exists.
53    if let Some(id) = db.get_crate_id(&normalized_name).await? {
54        if !db.crate_version_exists(id, crate_version).await? {
55            return crate_does_not_exist(&normalized_name, crate_version);
56        }
57    } else {
58        return crate_does_not_exist(&normalized_name, crate_version);
59    }
60
61    // Check if user from token is an owner of the crate.
62    // If not, he is not allowed to push the docs.
63    let user = kellnr_auth::maybe_user::MaybeUser::from_token(token);
64    check_ownership(&normalized_name, &user, &db).await?;
65
66    let doc_path = settings.docs_path().join(&*package).join(crate_version);
67
68    let _ = tokio::task::spawn_blocking(move || docs.extract(&doc_path))
69        .await
70        .map_err(|_| DocsError::ExtractFailed)?;
71
72    db.update_docs_link(
73        &normalized_name,
74        &version,
75        &compute_doc_url(&package, &version, &settings.origin.path),
76    )
77    .await?;
78
79    Ok(Json(DocUploadResponse::new(
80        "Successfully published docs.".to_string(),
81        &package,
82        &version,
83        &settings.origin.path,
84    )))
85}
86
87fn crate_does_not_exist(
88    crate_name: &str,
89    crate_version: &str,
90) -> ApiResult<Json<DocUploadResponse>> {
91    Err(DocsError::CrateDoesNotExist(crate_name.to_string(), crate_version.to_string()).into())
92}
93
94#[cfg(test)]
95mod tests {
96    use std::path::PathBuf;
97    use std::sync::Arc;
98
99    use axum::Router;
100    use axum::body::Body;
101    use axum::http::Request;
102    use axum::routing::get;
103    use http_body_util::BodyExt;
104    use kellnr_appstate::AppStateData;
105    use kellnr_common::normalized_name::NormalizedName;
106    use kellnr_db::mock::MockDb;
107    use kellnr_db::{DbProvider, DocQueueEntry};
108    use tower::ServiceExt;
109
110    use super::*;
111    use crate::doc_queue_response::DocQueueEntryResponse;
112
113    #[tokio::test]
114    async fn doc_in_queue_returns_queue_entries() {
115        let mut db = MockDb::new();
116        db.expect_get_doc_queue().returning(|| {
117            Ok(vec![
118                DocQueueEntry {
119                    id: 0,
120                    normalized_name: NormalizedName::from_unchecked("crate1".to_string()),
121                    version: "0.0.1".to_string(),
122                    path: PathBuf::default(),
123                },
124                DocQueueEntry {
125                    id: 1,
126                    normalized_name: NormalizedName::from_unchecked("crate2".to_string()),
127                    version: "0.0.2".to_string(),
128                    path: PathBuf::default(),
129                },
130            ])
131        });
132
133        let kellnr = app(Arc::new(db));
134        let r = kellnr
135            .oneshot(Request::get("/queue").body(Body::empty()).unwrap())
136            .await
137            .unwrap();
138
139        let actual = r.into_body().collect().await.unwrap().to_bytes();
140        let actual = serde_json::from_slice::<DocQueueResponse>(&actual).unwrap();
141        assert_eq!(
142            DocQueueResponse {
143                queue: vec![
144                    DocQueueEntryResponse {
145                        name: "crate1".to_string(),
146                        version: "0.0.1".to_string()
147                    },
148                    DocQueueEntryResponse {
149                        name: "crate2".to_string(),
150                        version: "0.0.2".to_string()
151                    }
152                ]
153            },
154            actual
155        );
156    }
157
158    fn app(db: Arc<dyn DbProvider>) -> Router {
159        Router::new()
160            .route("/queue", get(docs_in_queue))
161            .with_state(AppStateData {
162                db,
163                ..kellnr_appstate::test_state()
164            })
165    }
166}