Skip to content

Commit

Permalink
Add PATCH /crates/:crate/:version route
Browse files Browse the repository at this point in the history
Signed-off-by: Rustin170506 <[email protected]>
  • Loading branch information
Rustin170506 authored and Turbo87 committed Sep 12, 2024
1 parent 3678583 commit 624a705
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 3 deletions.
143 changes: 141 additions & 2 deletions src/controllers/version/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,36 @@
use axum::extract::Path;
use axum::Json;
use crates_io_worker::BackgroundJob;
use diesel::{ExpressionMethods, RunQueryDsl};
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
use http::request::Parts;
use http::StatusCode;
use serde::Deserialize;
use serde_json::Value;
use tokio::runtime::Handle;

use crate::app::AppState;
use crate::models::VersionOwnerAction;
use crate::auth::AuthCheck;
use crate::models::token::EndpointScope;
use crate::models::{
insert_version_owner_action, Crate, Rights, Version, VersionAction, VersionOwnerAction,
};
use crate::rate_limiter::LimitedAction;
use crate::tasks::spawn_blocking;
use crate::util::errors::{version_not_found, AppResult};
use crate::util::diesel::Conn;
use crate::util::errors::{bad_request, custom, version_not_found, AppResult};
use crate::views::{EncodableDependency, EncodableVersion};
use crate::worker::jobs::{self, UpdateDefaultVersion};

use super::version_and_crate;

#[derive(Deserialize)]
pub struct VersionUpdate {
yanked: Option<bool>,
yank_message: Option<String>,
}

/// Handles the `GET /crates/:crate_id/:version/dependencies` route.
///
/// This information can be obtained directly from the index.
Expand Down Expand Up @@ -84,3 +103,123 @@ pub async fn show(
})
.await
}

/// Handles the `PATCH /crates/:crate/:version` route.
///
/// This endpoint allows updating the yanked state of a version, including a yank message.
pub async fn update(
state: AppState,
Path((crate_name, version)): Path<(String, String)>,
req: Parts,
Json(update_data): Json<VersionUpdate>,
) -> AppResult<Json<Value>> {
if semver::Version::parse(&version).is_err() {
return Err(version_not_found(&crate_name, &version));
}

let conn = state.db_write().await?;
spawn_blocking(move || {
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
let (mut version, krate) = version_and_crate(conn, &crate_name, &version)?;

apply_yank_update(&state, &req, conn, &mut version, &krate, &update_data)?;

let published_by = version.published_by(conn);
let actions = VersionOwnerAction::by_version(conn, &version)?;
let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions);
Ok(Json(json!({ "version": updated_version })))
})
.await
}

fn apply_yank_update(
state: &AppState,
req: &Parts,
conn: &mut impl Conn,
version: &mut Version,
krate: &Crate,
update_data: &VersionUpdate,
) -> AppResult<()> {
// Try to update the yank state first, to avoid unnecessary checks.
update_version_yank_state(version, update_data)?;

// Add authentication check
let auth = AuthCheck::default()
.with_endpoint_scope(EndpointScope::Yank)
.for_crate(&krate.name)
.check(req, conn)?;

// Add rate limiting check
state
.rate_limiter
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, conn)?;

let api_token_id = auth.api_token_id();
let user = auth.user();
let owners = krate.owners(conn)?;

// Check user rights
if Handle::current().block_on(user.rights(state, &owners))? < Rights::Publish {
if user.is_admin {
warn!(
"Admin {} is updating {}@{}",
user.gh_login, krate.name, version.num
);
} else {
return Err(custom(
StatusCode::FORBIDDEN,
"must already be an owner to update version",
));
}
}

diesel::update(&*version)
.set((
crate::schema::versions::yanked.eq(version.yanked),
crate::schema::versions::yank_message.eq(&version.yank_message),
))
.execute(conn)?;

// Add version owner action
let action = if version.yanked {
VersionAction::Yank
} else {
VersionAction::Unyank
};
insert_version_owner_action(conn, version.id, user.id, api_token_id, action)?;

// Enqueue jobs
jobs::enqueue_sync_to_index(&krate.name, conn)?;
UpdateDefaultVersion::new(krate.id).enqueue(conn)?;

Ok(())
}

fn update_version_yank_state(version: &mut Version, update_data: &VersionUpdate) -> AppResult<()> {
match (update_data.yanked, &update_data.yank_message) {
(Some(true), Some(message)) => {
version.yanked = true;
version.yank_message = Some(message.clone());
}
(Some(yanked), None) => {
version.yanked = yanked;
version.yank_message = None;
}
(Some(false), Some(_)) => {
return Err(bad_request("Cannot set yank message when unyanking"));
}
(None, Some(message)) => {
if version.yanked {
version.yank_message = Some(message.clone());
} else {
return Err(bad_request(
"Cannot update yank message for a version that is not yanked",
));
}
}
// If both yanked and yank_message are None, do nothing.
// This function only cares about updating the yanked state and yank message.
(None, None) => {}
}
Ok(())
}
2 changes: 1 addition & 1 deletion src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> {
.route("/api/v1/crates/:crate_id", get(krate::metadata::show))
.route(
"/api/v1/crates/:crate_id/:version",
get(version::metadata::show),
get(version::metadata::show).patch(version::metadata::update),
)
.route(
"/api/v1/crates/:crate_id/:version/readme",
Expand Down

0 comments on commit 624a705

Please sign in to comment.