Skip to content

Commit 7cae863

Browse files
authored
Merge pull request #9423 from Rustin170506/rustin-patch-update-patch
Add `PATCH /crates/:crate/:version` route
2 parents 103d06d + 45ec3fb commit 7cae863

13 files changed

+717
-69
lines changed

src/controllers/version/metadata.rs

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,43 @@
66
77
use axum::extract::Path;
88
use axum::Json;
9+
use crates_io_worker::BackgroundJob;
10+
use diesel::{
11+
BoolExpressionMethods, ExpressionMethods, PgExpressionMethods, QueryDsl, RunQueryDsl,
12+
};
913
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
14+
use http::request::Parts;
15+
use http::StatusCode;
16+
use serde::Deserialize;
1017
use serde_json::Value;
18+
use tokio::runtime::Handle;
1119

1220
use crate::app::AppState;
13-
use crate::models::VersionOwnerAction;
21+
use crate::auth::AuthCheck;
22+
use crate::models::token::EndpointScope;
23+
use crate::models::{
24+
insert_version_owner_action, Crate, Rights, Version, VersionAction, VersionOwnerAction,
25+
};
26+
use crate::rate_limiter::LimitedAction;
27+
use crate::schema::versions;
1428
use crate::tasks::spawn_blocking;
15-
use crate::util::errors::{version_not_found, AppResult};
29+
use crate::util::diesel::Conn;
30+
use crate::util::errors::{bad_request, custom, version_not_found, AppResult};
1631
use crate::views::{EncodableDependency, EncodableVersion};
32+
use crate::worker::jobs::{self, UpdateDefaultVersion};
1733

1834
use super::version_and_crate;
1935

36+
#[derive(Deserialize)]
37+
pub struct VersionUpdate {
38+
yanked: Option<bool>,
39+
yank_message: Option<String>,
40+
}
41+
#[derive(Deserialize)]
42+
pub struct VersionUpdateRequest {
43+
version: VersionUpdate,
44+
}
45+
2046
/// Handles the `GET /crates/:crate_id/:version/dependencies` route.
2147
///
2248
/// This information can be obtained directly from the index.
@@ -84,3 +110,132 @@ pub async fn show(
84110
})
85111
.await
86112
}
113+
114+
/// Handles the `PATCH /crates/:crate/:version` route.
115+
///
116+
/// This endpoint allows updating the yanked state of a version, including a yank message.
117+
pub async fn update(
118+
state: AppState,
119+
Path((crate_name, version)): Path<(String, String)>,
120+
req: Parts,
121+
Json(update_request): Json<VersionUpdateRequest>,
122+
) -> AppResult<Json<Value>> {
123+
if semver::Version::parse(&version).is_err() {
124+
return Err(version_not_found(&crate_name, &version));
125+
}
126+
127+
let conn = state.db_write().await?;
128+
spawn_blocking(move || {
129+
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
130+
let (mut version, krate) = version_and_crate(conn, &crate_name, &version)?;
131+
132+
validate_yank_update(&update_request.version, &version)?;
133+
perform_version_yank_update(
134+
&state,
135+
&req,
136+
conn,
137+
&mut version,
138+
&krate,
139+
update_request.version.yanked,
140+
update_request.version.yank_message,
141+
)?;
142+
143+
let published_by = version.published_by(conn);
144+
let actions = VersionOwnerAction::by_version(conn, &version)?;
145+
let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions);
146+
Ok(Json(json!({ "version": updated_version })))
147+
})
148+
.await
149+
}
150+
151+
fn validate_yank_update(update_data: &VersionUpdate, version: &Version) -> AppResult<()> {
152+
match (update_data.yanked, &update_data.yank_message) {
153+
(Some(false), Some(_)) => {
154+
return Err(bad_request("Cannot set yank message when unyanking"));
155+
}
156+
(None, Some(_)) => {
157+
if !version.yanked {
158+
return Err(bad_request(
159+
"Cannot update yank message for a version that is not yanked",
160+
));
161+
}
162+
}
163+
_ => {}
164+
}
165+
Ok(())
166+
}
167+
168+
pub fn perform_version_yank_update(
169+
state: &AppState,
170+
req: &Parts,
171+
conn: &mut impl Conn,
172+
version: &mut Version,
173+
krate: &Crate,
174+
yanked: Option<bool>,
175+
yank_message: Option<String>,
176+
) -> AppResult<()> {
177+
let auth = AuthCheck::default()
178+
.with_endpoint_scope(EndpointScope::Yank)
179+
.for_crate(&krate.name)
180+
.check(req, conn)?;
181+
182+
state
183+
.rate_limiter
184+
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, conn)?;
185+
186+
let api_token_id = auth.api_token_id();
187+
let user = auth.user();
188+
let owners = krate.owners(conn)?;
189+
190+
let yanked = yanked.unwrap_or(version.yanked);
191+
192+
if Handle::current().block_on(user.rights(state, &owners))? < Rights::Publish {
193+
if user.is_admin {
194+
let action = if yanked { "yanking" } else { "unyanking" };
195+
warn!(
196+
"Admin {} is {action} {}@{}",
197+
user.gh_login, krate.name, version.num
198+
);
199+
} else {
200+
return Err(custom(
201+
StatusCode::FORBIDDEN,
202+
"must already be an owner to yank or unyank",
203+
));
204+
}
205+
}
206+
207+
// Check if the yanked state or yank message has changed and update if necessary
208+
let updated_cnt = diesel::update(
209+
versions::table.find(version.id).filter(
210+
versions::yanked
211+
.is_distinct_from(yanked)
212+
.or(versions::yank_message.is_distinct_from(&yank_message)),
213+
),
214+
)
215+
.set((
216+
versions::yanked.eq(yanked),
217+
versions::yank_message.eq(&yank_message),
218+
))
219+
.execute(conn)?;
220+
221+
// If no rows were updated, return early
222+
if updated_cnt == 0 {
223+
return Ok(());
224+
}
225+
226+
// Apply the update to the version
227+
version.yanked = yanked;
228+
version.yank_message = yank_message;
229+
230+
let action = if 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: 4 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,8 @@ 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+
perform_version_yank_update(&state, &req, conn, &mut version, &krate, Some(yanked), None)?;
12060
ok_true()
12161
})
12262
.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",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
source: src/tests/krate/yanking.rs
3+
expression: json
4+
---
5+
{
6+
"version": {
7+
"id": 1,
8+
"crate": "patchable",
9+
"num": "1.0.0",
10+
"dl_path": "/api/v1/crates/patchable/1.0.0/download",
11+
"readme_path": "/api/v1/crates/patchable/1.0.0/readme",
12+
"updated_at": "[datetime]",
13+
"created_at": "[datetime]",
14+
"downloads": 0,
15+
"features": {},
16+
"yanked": true,
17+
"yank_message": "Yanking reason",
18+
"lib_links": null,
19+
"license": "MIT",
20+
"links": {
21+
"dependencies": "/api/v1/crates/patchable/1.0.0/dependencies",
22+
"version_downloads": "/api/v1/crates/patchable/1.0.0/downloads",
23+
"authors": "/api/v1/crates/patchable/1.0.0/authors"
24+
},
25+
"crate_size": 151,
26+
"published_by": {
27+
"id": 1,
28+
"login": "foo",
29+
"name": null,
30+
"avatar": null,
31+
"url": "https://github.com/foo"
32+
},
33+
"audit_actions": [
34+
{
35+
"action": "publish",
36+
"user": {
37+
"id": 1,
38+
"login": "foo",
39+
"name": null,
40+
"avatar": null,
41+
"url": "https://github.com/foo"
42+
},
43+
"time": "[datetime]"
44+
},
45+
{
46+
"action": "yank",
47+
"user": {
48+
"id": 1,
49+
"login": "foo",
50+
"name": null,
51+
"avatar": null,
52+
"url": "https://github.com/foo"
53+
},
54+
"time": "[datetime]"
55+
}
56+
],
57+
"checksum": "ddfc395ab340f413ee1d1ed0afce51a7c9df1c99c551fed5aef76edd4abe4048",
58+
"rust_version": null,
59+
"has_lib": false,
60+
"bin_names": []
61+
}
62+
}

0 commit comments

Comments
 (0)