Skip to content

Commit dc70376

Browse files
committed
controllers/version/metadata: Move update_version() to dedicated module
1 parent 09881fa commit dc70376

File tree

5 files changed

+188
-180
lines changed

5 files changed

+188
-180
lines changed

src/controllers/version.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod dependencies;
33
pub mod downloads;
44
pub mod metadata;
55
pub mod readme;
6+
pub mod update;
67
pub mod yank;
78

89
use axum::extract::{FromRequestParts, Path};

src/controllers/version/metadata.rs

Lines changed: 2 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,16 @@
44
//! index or cached metadata which was extracted (client side) from the
55
//! `Cargo.toml` file.
66
7-
use axum::Json;
87
use axum_extra::json;
98
use axum_extra::response::ErasedJson;
10-
use crates_io_worker::BackgroundJob;
11-
use diesel::prelude::*;
12-
use diesel_async::{AsyncPgConnection, RunQueryDsl};
13-
use http::request::Parts;
14-
use http::StatusCode;
15-
use serde::Deserialize;
169

1710
use crate::app::AppState;
18-
use crate::auth::{AuthCheck, Authentication};
19-
use crate::models::token::EndpointScope;
20-
use crate::models::{
21-
Crate, NewVersionOwnerAction, Rights, Version, VersionAction, VersionOwnerAction,
22-
};
23-
use crate::rate_limiter::LimitedAction;
24-
use crate::schema::versions;
25-
use crate::util::errors::{bad_request, custom, AppResult};
11+
use crate::models::VersionOwnerAction;
12+
use crate::util::errors::AppResult;
2613
use crate::views::EncodableVersion;
27-
use crate::worker::jobs::{SyncToGitIndex, SyncToSparseIndex, UpdateDefaultVersion};
2814

2915
use super::CrateVersionPath;
3016

31-
#[derive(Deserialize)]
32-
pub struct VersionUpdate {
33-
yanked: Option<bool>,
34-
yank_message: Option<String>,
35-
}
36-
#[derive(Deserialize)]
37-
pub struct VersionUpdateRequest {
38-
version: VersionUpdate,
39-
}
40-
4117
/// Get crate version metadata.
4218
#[utoipa::path(
4319
get,
@@ -55,155 +31,3 @@ pub async fn find_version(state: AppState, path: CrateVersionPath) -> AppResult<
5531
let version = EncodableVersion::from(version, &krate.name, published_by, actions);
5632
Ok(json!({ "version": version }))
5733
}
58-
59-
/// Update a crate version.
60-
///
61-
/// This endpoint allows updating the `yanked` state of a version, including a yank message.
62-
#[utoipa::path(
63-
patch,
64-
path = "/api/v1/crates/{name}/{version}",
65-
params(CrateVersionPath),
66-
tag = "versions",
67-
responses((status = 200, description = "Successful Response")),
68-
)]
69-
pub async fn update_version(
70-
state: AppState,
71-
path: CrateVersionPath,
72-
req: Parts,
73-
Json(update_request): Json<VersionUpdateRequest>,
74-
) -> AppResult<ErasedJson> {
75-
let mut conn = state.db_write().await?;
76-
let (mut version, krate) = path.load_version_and_crate(&mut conn).await?;
77-
validate_yank_update(&update_request.version, &version)?;
78-
let auth = authenticate(&req, &mut conn, &krate.name).await?;
79-
80-
state
81-
.rate_limiter
82-
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, &mut conn)
83-
.await?;
84-
85-
perform_version_yank_update(
86-
&state,
87-
&mut conn,
88-
&mut version,
89-
&krate,
90-
&auth,
91-
update_request.version.yanked,
92-
update_request.version.yank_message,
93-
)
94-
.await?;
95-
96-
let published_by = version.published_by(&mut conn).await?;
97-
let actions = VersionOwnerAction::by_version(&mut conn, &version).await?;
98-
let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions);
99-
Ok(json!({ "version": updated_version }))
100-
}
101-
102-
fn validate_yank_update(update_data: &VersionUpdate, version: &Version) -> AppResult<()> {
103-
if update_data.yank_message.is_some() {
104-
if matches!(update_data.yanked, Some(false)) {
105-
return Err(bad_request("Cannot set yank message when unyanking"));
106-
}
107-
108-
if update_data.yanked.is_none() && !version.yanked {
109-
return Err(bad_request(
110-
"Cannot update yank message for a version that is not yanked",
111-
));
112-
}
113-
}
114-
115-
Ok(())
116-
}
117-
118-
pub async fn authenticate(
119-
req: &Parts,
120-
conn: &mut AsyncPgConnection,
121-
name: &str,
122-
) -> AppResult<Authentication> {
123-
AuthCheck::default()
124-
.with_endpoint_scope(EndpointScope::Yank)
125-
.for_crate(name)
126-
.check(req, conn)
127-
.await
128-
}
129-
130-
pub async fn perform_version_yank_update(
131-
state: &AppState,
132-
conn: &mut AsyncPgConnection,
133-
version: &mut Version,
134-
krate: &Crate,
135-
auth: &Authentication,
136-
yanked: Option<bool>,
137-
yank_message: Option<String>,
138-
) -> AppResult<()> {
139-
let api_token_id = auth.api_token_id();
140-
let user = auth.user();
141-
let owners = krate.owners(conn).await?;
142-
143-
let yanked = yanked.unwrap_or(version.yanked);
144-
145-
if user.rights(state, &owners).await? < Rights::Publish {
146-
if user.is_admin {
147-
let action = if yanked { "yanking" } else { "unyanking" };
148-
warn!(
149-
"Admin {} is {action} {}@{}",
150-
user.gh_login, krate.name, version.num
151-
);
152-
} else {
153-
return Err(custom(
154-
StatusCode::FORBIDDEN,
155-
"must already be an owner to yank or unyank",
156-
));
157-
}
158-
}
159-
160-
// Check if the yanked state or yank message has changed and update if necessary
161-
let updated_cnt = diesel::update(
162-
versions::table.find(version.id).filter(
163-
versions::yanked
164-
.is_distinct_from(yanked)
165-
.or(versions::yank_message.is_distinct_from(&yank_message)),
166-
),
167-
)
168-
.set((
169-
versions::yanked.eq(yanked),
170-
versions::yank_message.eq(&yank_message),
171-
))
172-
.execute(conn)
173-
.await?;
174-
175-
// If no rows were updated, return early
176-
if updated_cnt == 0 {
177-
return Ok(());
178-
}
179-
180-
// Apply the update to the version
181-
version.yanked = yanked;
182-
version.yank_message = yank_message;
183-
184-
let action = if yanked {
185-
VersionAction::Yank
186-
} else {
187-
VersionAction::Unyank
188-
};
189-
NewVersionOwnerAction::builder()
190-
.version_id(version.id)
191-
.user_id(user.id)
192-
.maybe_api_token_id(api_token_id)
193-
.action(action)
194-
.build()
195-
.insert(conn)
196-
.await?;
197-
198-
let git_index_job = SyncToGitIndex::new(&krate.name);
199-
let sparse_index_job = SyncToSparseIndex::new(&krate.name);
200-
let update_default_version_job = UpdateDefaultVersion::new(krate.id);
201-
202-
tokio::try_join!(
203-
git_index_job.enqueue(conn),
204-
sparse_index_job.enqueue(conn),
205-
update_default_version_job.enqueue(conn),
206-
)?;
207-
208-
Ok(())
209-
}

src/controllers/version/update.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use super::CrateVersionPath;
2+
use crate::app::AppState;
3+
use crate::auth::{AuthCheck, Authentication};
4+
use crate::models::token::EndpointScope;
5+
use crate::models::{
6+
Crate, NewVersionOwnerAction, Rights, Version, VersionAction, VersionOwnerAction,
7+
};
8+
use crate::rate_limiter::LimitedAction;
9+
use crate::schema::versions;
10+
use crate::util::errors::{bad_request, custom, AppResult};
11+
use crate::views::EncodableVersion;
12+
use crate::worker::jobs::{SyncToGitIndex, SyncToSparseIndex, UpdateDefaultVersion};
13+
use axum::Json;
14+
use axum_extra::json;
15+
use axum_extra::response::ErasedJson;
16+
use crates_io_worker::BackgroundJob;
17+
use diesel::prelude::*;
18+
use diesel_async::{AsyncPgConnection, RunQueryDsl};
19+
use http::request::Parts;
20+
use http::StatusCode;
21+
use serde::Deserialize;
22+
23+
#[derive(Deserialize)]
24+
pub struct VersionUpdate {
25+
yanked: Option<bool>,
26+
yank_message: Option<String>,
27+
}
28+
#[derive(Deserialize)]
29+
pub struct VersionUpdateRequest {
30+
version: VersionUpdate,
31+
}
32+
33+
/// Update a crate version.
34+
///
35+
/// This endpoint allows updating the `yanked` state of a version, including a yank message.
36+
#[utoipa::path(
37+
patch,
38+
path = "/api/v1/crates/{name}/{version}",
39+
params(CrateVersionPath),
40+
tag = "versions",
41+
responses((status = 200, description = "Successful Response")),
42+
)]
43+
pub async fn update_version(
44+
state: AppState,
45+
path: CrateVersionPath,
46+
req: Parts,
47+
Json(update_request): Json<VersionUpdateRequest>,
48+
) -> AppResult<ErasedJson> {
49+
let mut conn = state.db_write().await?;
50+
let (mut version, krate) = path.load_version_and_crate(&mut conn).await?;
51+
validate_yank_update(&update_request.version, &version)?;
52+
let auth = authenticate(&req, &mut conn, &krate.name).await?;
53+
54+
state
55+
.rate_limiter
56+
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, &mut conn)
57+
.await?;
58+
59+
perform_version_yank_update(
60+
&state,
61+
&mut conn,
62+
&mut version,
63+
&krate,
64+
&auth,
65+
update_request.version.yanked,
66+
update_request.version.yank_message,
67+
)
68+
.await?;
69+
70+
let published_by = version.published_by(&mut conn).await?;
71+
let actions = VersionOwnerAction::by_version(&mut conn, &version).await?;
72+
let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions);
73+
Ok(json!({ "version": updated_version }))
74+
}
75+
76+
fn validate_yank_update(update_data: &VersionUpdate, version: &Version) -> AppResult<()> {
77+
if update_data.yank_message.is_some() {
78+
if matches!(update_data.yanked, Some(false)) {
79+
return Err(bad_request("Cannot set yank message when unyanking"));
80+
}
81+
82+
if update_data.yanked.is_none() && !version.yanked {
83+
return Err(bad_request(
84+
"Cannot update yank message for a version that is not yanked",
85+
));
86+
}
87+
}
88+
89+
Ok(())
90+
}
91+
92+
pub async fn authenticate(
93+
req: &Parts,
94+
conn: &mut AsyncPgConnection,
95+
name: &str,
96+
) -> AppResult<Authentication> {
97+
AuthCheck::default()
98+
.with_endpoint_scope(EndpointScope::Yank)
99+
.for_crate(name)
100+
.check(req, conn)
101+
.await
102+
}
103+
104+
pub async fn perform_version_yank_update(
105+
state: &AppState,
106+
conn: &mut AsyncPgConnection,
107+
version: &mut Version,
108+
krate: &Crate,
109+
auth: &Authentication,
110+
yanked: Option<bool>,
111+
yank_message: Option<String>,
112+
) -> AppResult<()> {
113+
let api_token_id = auth.api_token_id();
114+
let user = auth.user();
115+
let owners = krate.owners(conn).await?;
116+
117+
let yanked = yanked.unwrap_or(version.yanked);
118+
119+
if user.rights(state, &owners).await? < Rights::Publish {
120+
if user.is_admin {
121+
let action = if yanked { "yanking" } else { "unyanking" };
122+
warn!(
123+
"Admin {} is {action} {}@{}",
124+
user.gh_login, krate.name, version.num
125+
);
126+
} else {
127+
return Err(custom(
128+
StatusCode::FORBIDDEN,
129+
"must already be an owner to yank or unyank",
130+
));
131+
}
132+
}
133+
134+
// Check if the yanked state or yank message has changed and update if necessary
135+
let updated_cnt = diesel::update(
136+
versions::table.find(version.id).filter(
137+
versions::yanked
138+
.is_distinct_from(yanked)
139+
.or(versions::yank_message.is_distinct_from(&yank_message)),
140+
),
141+
)
142+
.set((
143+
versions::yanked.eq(yanked),
144+
versions::yank_message.eq(&yank_message),
145+
))
146+
.execute(conn)
147+
.await?;
148+
149+
// If no rows were updated, return early
150+
if updated_cnt == 0 {
151+
return Ok(());
152+
}
153+
154+
// Apply the update to the version
155+
version.yanked = yanked;
156+
version.yank_message = yank_message;
157+
158+
let action = if yanked {
159+
VersionAction::Yank
160+
} else {
161+
VersionAction::Unyank
162+
};
163+
NewVersionOwnerAction::builder()
164+
.version_id(version.id)
165+
.user_id(user.id)
166+
.maybe_api_token_id(api_token_id)
167+
.action(action)
168+
.build()
169+
.insert(conn)
170+
.await?;
171+
172+
let git_index_job = SyncToGitIndex::new(&krate.name);
173+
let sparse_index_job = SyncToSparseIndex::new(&krate.name);
174+
let update_default_version_job = UpdateDefaultVersion::new(krate.id);
175+
176+
tokio::try_join!(
177+
git_index_job.enqueue(conn),
178+
sparse_index_job.enqueue(conn),
179+
update_default_version_job.enqueue(conn),
180+
)?;
181+
182+
Ok(())
183+
}

src/controllers/version/yank.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Endpoints for yanking and unyanking specific versions of crates
22
3-
use super::metadata::{authenticate, perform_version_yank_update};
3+
use super::update::{authenticate, perform_version_yank_update};
44
use super::CrateVersionPath;
55
use crate::app::AppState;
66
use crate::controllers::helpers::ok_true;

0 commit comments

Comments
 (0)