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));
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 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 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),
76 )
77 .await?;
78
79 Ok(Json(DocUploadResponse::new(
80 "Successfully published docs.".to_string(),
81 &package,
82 &version,
83 )))
84}
85
86fn crate_does_not_exist(
87 crate_name: &str,
88 crate_version: &str,
89) -> ApiResult<Json<DocUploadResponse>> {
90 Err(DocsError::CrateDoesNotExist(crate_name.to_string(), crate_version.to_string()).into())
91}
92
93#[cfg(test)]
94mod tests {
95 use std::path::PathBuf;
96 use std::sync::Arc;
97
98 use axum::Router;
99 use axum::body::Body;
100 use axum::http::Request;
101 use axum::routing::get;
102 use http_body_util::BodyExt;
103 use kellnr_appstate::AppStateData;
104 use kellnr_common::normalized_name::NormalizedName;
105 use kellnr_db::mock::MockDb;
106 use kellnr_db::{DbProvider, DocQueueEntry};
107 use tower::ServiceExt;
108
109 use super::*;
110 use crate::doc_queue_response::DocQueueEntryResponse;
111
112 #[tokio::test]
113 async fn doc_in_queue_returns_queue_entries() {
114 let mut db = MockDb::new();
115 db.expect_get_doc_queue().returning(|| {
116 Ok(vec![
117 DocQueueEntry {
118 id: 0,
119 normalized_name: NormalizedName::from_unchecked("crate1".to_string()),
120 version: "0.0.1".to_string(),
121 path: PathBuf::default(),
122 },
123 DocQueueEntry {
124 id: 1,
125 normalized_name: NormalizedName::from_unchecked("crate2".to_string()),
126 version: "0.0.2".to_string(),
127 path: PathBuf::default(),
128 },
129 ])
130 });
131
132 let kellnr = app(Arc::new(db));
133 let r = kellnr
134 .oneshot(Request::get("/queue").body(Body::empty()).unwrap())
135 .await
136 .unwrap();
137
138 let actual = r.into_body().collect().await.unwrap().to_bytes();
139 let actual = serde_json::from_slice::<DocQueueResponse>(&actual).unwrap();
140 assert_eq!(
141 DocQueueResponse {
142 queue: vec![
143 DocQueueEntryResponse {
144 name: "crate1".to_string(),
145 version: "0.0.1".to_string()
146 },
147 DocQueueEntryResponse {
148 name: "crate2".to_string(),
149 version: "0.0.2".to_string()
150 }
151 ]
152 },
153 actual
154 );
155 }
156
157 fn app(db: Arc<dyn DbProvider>) -> Router {
158 Router::new()
159 .route("/queue", get(docs_in_queue))
160 .with_state(AppStateData {
161 db,
162 ..kellnr_appstate::test_state()
163 })
164 }
165}