@@ -6,6 +6,8 @@ use crate::models::{NewDeletedCrate, Rights};
66use crate :: schema:: { crate_downloads, crates, dependencies} ;
77use crate :: util:: errors:: { custom, AppResult , BoxedAppError } ;
88use crate :: worker:: jobs;
9+ use axum:: extract:: rejection:: QueryRejection ;
10+ use axum:: extract:: { FromRequestParts , Query } ;
911use bigdecimal:: ToPrimitive ;
1012use chrono:: { TimeDelta , Utc } ;
1113use crates_io_database:: schema:: deleted_crates;
@@ -19,6 +21,19 @@ use http::StatusCode;
1921const DOWNLOADS_PER_MONTH_LIMIT : u64 = 500 ;
2022const AVAILABLE_AFTER : TimeDelta = TimeDelta :: hours ( 24 ) ;
2123
24+ #[ derive( Debug , Deserialize , FromRequestParts , utoipa:: IntoParams ) ]
25+ #[ from_request( via( Query ) , rejection( QueryRejection ) ) ]
26+ #[ into_params( parameter_in = Query ) ]
27+ pub struct DeleteQueryParams {
28+ message : Option < String > ,
29+ }
30+
31+ impl DeleteQueryParams {
32+ pub fn message ( & self ) -> Option < & str > {
33+ self . message . as_deref ( ) . filter ( |m| !m. is_empty ( ) )
34+ }
35+ }
36+
2237/// Delete a crate.
2338///
2439/// The crate is immediately deleted from the database, and with a small delay
@@ -31,12 +46,17 @@ const AVAILABLE_AFTER: TimeDelta = TimeDelta::hours(24);
3146#[ utoipa:: path(
3247 delete,
3348 path = "/api/v1/crates/{name}" ,
34- params( CratePath ) ,
49+ params( CratePath , DeleteQueryParams ) ,
3550 security( ( "cookie" = [ ] ) ) ,
3651 tag = "crates" ,
3752 responses( ( status = 200 , description = "Successful Response" ) ) ,
3853) ]
39- pub async fn delete_crate ( path : CratePath , parts : Parts , app : AppState ) -> AppResult < StatusCode > {
54+ pub async fn delete_crate (
55+ path : CratePath ,
56+ params : DeleteQueryParams ,
57+ parts : Parts ,
58+ app : AppState ,
59+ ) -> AppResult < StatusCode > {
4060 let mut conn = app. db_write ( ) . await ?;
4161
4262 // Check that the user is authenticated
@@ -96,6 +116,7 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe
96116 . deleted_at ( & deleted_at)
97117 . deleted_by ( user. id )
98118 . available_at ( & available_at)
119+ . maybe_message ( params. message ( ) )
99120 . build ( ) ;
100121
101122 diesel:: insert_into ( deleted_crates:: table)
@@ -200,12 +221,36 @@ mod tests {
200221 use crate :: models:: OwnerKind ;
201222 use crate :: tests:: builders:: { DependencyBuilder , PublishBuilder } ;
202223 use crate :: tests:: util:: { RequestHelper , Response , TestApp } ;
224+ use axum:: RequestPartsExt ;
203225 use crates_io_database:: schema:: crate_owners;
204226 use diesel_async:: AsyncPgConnection ;
205- use http:: StatusCode ;
227+ use http:: { Request , StatusCode } ;
206228 use insta:: assert_snapshot;
207229 use serde_json:: json;
208230
231+ #[ tokio:: test]
232+ async fn test_query_params ( ) -> anyhow:: Result < ( ) > {
233+ let check = |uri| async move {
234+ let request = Request :: builder ( ) . uri ( uri) . body ( ( ) ) ?;
235+ let ( mut parts, _) = request. into_parts ( ) ;
236+ Ok :: < _ , anyhow:: Error > ( parts. extract :: < DeleteQueryParams > ( ) . await ?)
237+ } ;
238+
239+ let params = check ( "/api/v1/crates/foo" ) . await ?;
240+ assert_none ! ( params. message) ;
241+
242+ let params = check ( "/api/v1/crates/foo?" ) . await ?;
243+ assert_none ! ( params. message) ;
244+
245+ let params = check ( "/api/v1/crates/foo?message=" ) . await ?;
246+ assert_eq ! ( assert_some!( params. message) , "" ) ;
247+
248+ let params = check ( "/api/v1/crates/foo?message=hello%20world" ) . await ?;
249+ assert_eq ! ( assert_some!( params. message) , "hello world" ) ;
250+
251+ Ok ( ( ) )
252+ }
253+
209254 #[ tokio:: test( flavor = "multi_thread" ) ]
210255 async fn test_happy_path_new_crate ( ) -> anyhow:: Result < ( ) > {
211256 let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
0 commit comments