Skip to content

Commit 406616d

Browse files
committed
Add PATCH /crates/:crate/:version route
Signed-off-by: Rustin170506 <[email protected]>
1 parent 3678583 commit 406616d

File tree

3 files changed

+163
-67
lines changed

3 files changed

+163
-67
lines changed

src/controllers/version/metadata.rs

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,37 @@
66
77
use axum::extract::Path;
88
use axum::Json;
9+
use crates_io_worker::BackgroundJob;
10+
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
911
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
12+
use http::request::Parts;
13+
use http::StatusCode;
14+
use serde::Deserialize;
1015
use serde_json::Value;
16+
use tokio::runtime::Handle;
1117

1218
use crate::app::AppState;
13-
use crate::models::VersionOwnerAction;
19+
use crate::auth::AuthCheck;
20+
use crate::models::token::EndpointScope;
21+
use crate::models::{
22+
insert_version_owner_action, Crate, Rights, Version, VersionAction, VersionOwnerAction,
23+
};
24+
use crate::rate_limiter::LimitedAction;
25+
use crate::schema::versions;
1426
use crate::tasks::spawn_blocking;
15-
use crate::util::errors::{version_not_found, AppResult};
27+
use crate::util::diesel::Conn;
28+
use crate::util::errors::{bad_request, custom, version_not_found, AppResult};
1629
use crate::views::{EncodableDependency, EncodableVersion};
30+
use crate::worker::jobs::{self, UpdateDefaultVersion};
1731

1832
use super::version_and_crate;
1933

34+
#[derive(Deserialize)]
35+
pub struct VersionUpdate {
36+
yanked: Option<bool>,
37+
yank_message: Option<String>,
38+
}
39+
2040
/// Handles the `GET /crates/:crate_id/:version/dependencies` route.
2141
///
2242
/// This information can be obtained directly from the index.
@@ -84,3 +104,138 @@ pub async fn show(
84104
})
85105
.await
86106
}
107+
108+
/// Handles the `PATCH /crates/:crate/:version` route.
109+
///
110+
/// This endpoint allows updating the yanked state of a version, including a yank message.
111+
pub async fn update(
112+
state: AppState,
113+
Path((crate_name, version)): Path<(String, String)>,
114+
req: Parts,
115+
Json(update_data): Json<VersionUpdate>,
116+
) -> AppResult<Json<Value>> {
117+
if semver::Version::parse(&version).is_err() {
118+
return Err(version_not_found(&crate_name, &version));
119+
}
120+
121+
let conn = state.db_write().await?;
122+
spawn_blocking(move || {
123+
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
124+
let (mut version, krate) = version_and_crate(conn, &crate_name, &version)?;
125+
126+
validate_yank_update(&update_data, &version)?;
127+
apply_yank_update(&mut version, &update_data);
128+
perform_version_yank_update(&state, &req, conn, &version, &krate)?;
129+
130+
let published_by = version.published_by(conn);
131+
let actions = VersionOwnerAction::by_version(conn, &version)?;
132+
let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions);
133+
Ok(Json(json!({ "version": updated_version })))
134+
})
135+
.await
136+
}
137+
138+
fn validate_yank_update(update_data: &VersionUpdate, version: &Version) -> AppResult<()> {
139+
match (update_data.yanked, &update_data.yank_message) {
140+
(Some(false), Some(_)) => {
141+
return Err(bad_request("Cannot set yank message when unyanking"));
142+
}
143+
(None, Some(_)) => {
144+
if !version.yanked {
145+
return Err(bad_request(
146+
"Cannot update yank message for a version that is not yanked",
147+
));
148+
}
149+
}
150+
_ => {}
151+
}
152+
Ok(())
153+
}
154+
155+
fn apply_yank_update(version: &mut Version, update_data: &VersionUpdate) {
156+
match (update_data.yanked, &update_data.yank_message) {
157+
(Some(true), Some(message)) => {
158+
version.yanked = true;
159+
version.yank_message = Some(message.clone());
160+
}
161+
(Some(yanked), None) => {
162+
version.yanked = yanked;
163+
version.yank_message = None;
164+
}
165+
(None, Some(message)) => {
166+
version.yank_message = Some(message.clone());
167+
}
168+
// If both yanked and yank_message are None, do nothing.
169+
_ => {}
170+
}
171+
}
172+
173+
pub fn perform_version_yank_update(
174+
state: &AppState,
175+
req: &Parts,
176+
conn: &mut impl Conn,
177+
version: &Version,
178+
krate: &Crate,
179+
) -> AppResult<()> {
180+
let auth = AuthCheck::default()
181+
.with_endpoint_scope(EndpointScope::Yank)
182+
.for_crate(&krate.name)
183+
.check(req, conn)?;
184+
185+
state
186+
.rate_limiter
187+
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, conn)?;
188+
189+
let api_token_id = auth.api_token_id();
190+
let user = auth.user();
191+
let owners = krate.owners(conn)?;
192+
193+
if Handle::current().block_on(user.rights(state, &owners))? < Rights::Publish {
194+
if user.is_admin {
195+
let action = if version.yanked {
196+
"yanking"
197+
} else {
198+
"unyanking"
199+
};
200+
warn!(
201+
"Admin {} is {action} {}@{}",
202+
user.gh_login, krate.name, version.num
203+
);
204+
} else {
205+
return Err(custom(
206+
StatusCode::FORBIDDEN,
207+
"must already be an owner to yank or unyank",
208+
));
209+
}
210+
}
211+
212+
// Check if the yanked state or yank message has changed
213+
let (yanked, yank_message) = versions::table
214+
.find(version.id)
215+
.select((versions::yanked, versions::yank_message))
216+
.first::<(bool, Option<String>)>(conn)?;
217+
218+
if yanked == version.yanked && yank_message == version.yank_message {
219+
// No changes, return early
220+
return Ok(());
221+
}
222+
223+
diesel::update(version)
224+
.set((
225+
versions::yanked.eq(version.yanked),
226+
versions::yank_message.eq(&version.yank_message),
227+
))
228+
.execute(conn)?;
229+
230+
let action = if version.yanked {
231+
VersionAction::Yank
232+
} else {
233+
VersionAction::Unyank
234+
};
235+
insert_version_owner_action(conn, version.id, user.id, api_token_id, action)?;
236+
237+
jobs::enqueue_sync_to_index(&krate.name, conn)?;
238+
UpdateDefaultVersion::new(krate.id).enqueue(conn)?;
239+
240+
Ok(())
241+
}

src/controllers/version/yank.rs

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
//! Endpoints for yanking and unyanking specific versions of crates
22
3+
use super::metadata::perform_version_yank_update;
34
use super::version_and_crate;
45
use crate::app::AppState;
5-
use crate::auth::AuthCheck;
66
use crate::controllers::helpers::ok_true;
7-
use crate::models::token::EndpointScope;
8-
use crate::models::Rights;
9-
use crate::models::{insert_version_owner_action, VersionAction};
10-
use crate::rate_limiter::LimitedAction;
11-
use crate::schema::versions;
127
use crate::tasks::spawn_blocking;
13-
use crate::util::errors::{custom, version_not_found, AppResult};
14-
use crate::worker::jobs;
15-
use crate::worker::jobs::UpdateDefaultVersion;
8+
use crate::util::errors::{version_not_found, AppResult};
169
use axum::extract::Path;
1710
use axum::response::Response;
18-
use crates_io_worker::BackgroundJob;
19-
use diesel::prelude::*;
2011
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
2112
use http::request::Parts;
22-
use http::StatusCode;
23-
use tokio::runtime::Handle;
2413

2514
/// Handles the `DELETE /crates/:crate_id/:version/yank` route.
2615
/// This does not delete a crate version, it makes the crate
@@ -66,57 +55,9 @@ async fn modify_yank(
6655
let conn = state.db_write().await?;
6756
spawn_blocking(move || {
6857
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
69-
70-
let auth = AuthCheck::default()
71-
.with_endpoint_scope(EndpointScope::Yank)
72-
.for_crate(&crate_name)
73-
.check(&req, conn)?;
74-
75-
state
76-
.rate_limiter
77-
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, conn)?;
78-
79-
let (version, krate) = version_and_crate(conn, &crate_name, &version)?;
80-
let api_token_id = auth.api_token_id();
81-
let user = auth.user();
82-
let owners = krate.owners(conn)?;
83-
84-
if Handle::current().block_on(user.rights(&state, &owners))? < Rights::Publish {
85-
if user.is_admin {
86-
let action = if yanked { "yanking" } else { "unyanking" };
87-
warn!(
88-
"Admin {} is {action} {}@{}",
89-
user.gh_login, krate.name, version.num
90-
);
91-
} else {
92-
return Err(custom(
93-
StatusCode::FORBIDDEN,
94-
"must already be an owner to yank or unyank",
95-
));
96-
}
97-
}
98-
99-
if version.yanked == yanked {
100-
// The crate is already in the state requested, nothing to do
101-
return ok_true();
102-
}
103-
104-
diesel::update(&version)
105-
.set(versions::yanked.eq(yanked))
106-
.execute(conn)?;
107-
108-
let action = if yanked {
109-
VersionAction::Yank
110-
} else {
111-
VersionAction::Unyank
112-
};
113-
114-
insert_version_owner_action(conn, version.id, user.id, api_token_id, action)?;
115-
116-
jobs::enqueue_sync_to_index(&krate.name, conn)?;
117-
118-
UpdateDefaultVersion::new(krate.id).enqueue(conn)?;
119-
58+
let (mut version, krate) = version_and_crate(conn, &crate_name, &version)?;
59+
version.yanked = yanked;
60+
perform_version_yank_update(&state, &req, conn, &version, &krate)?;
12061
ok_true()
12162
})
12263
.await

src/router.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> {
4545
.route("/api/v1/crates/:crate_id", get(krate::metadata::show))
4646
.route(
4747
"/api/v1/crates/:crate_id/:version",
48-
get(version::metadata::show),
48+
get(version::metadata::show).patch(version::metadata::update),
4949
)
5050
.route(
5151
"/api/v1/crates/:crate_id/:version/readme",

0 commit comments

Comments
 (0)