@@ -6,6 +6,8 @@ use crate::models::{NewDeletedCrate, Rights};
6
6
use crate :: schema:: { crate_downloads, crates, dependencies} ;
7
7
use crate :: util:: errors:: { custom, AppResult , BoxedAppError } ;
8
8
use crate :: worker:: jobs;
9
+ use axum:: extract:: rejection:: QueryRejection ;
10
+ use axum:: extract:: { FromRequestParts , Query } ;
9
11
use bigdecimal:: ToPrimitive ;
10
12
use chrono:: { TimeDelta , Utc } ;
11
13
use crates_io_database:: schema:: deleted_crates;
@@ -19,6 +21,19 @@ use http::StatusCode;
19
21
const DOWNLOADS_PER_MONTH_LIMIT : u64 = 500 ;
20
22
const AVAILABLE_AFTER : TimeDelta = TimeDelta :: hours ( 24 ) ;
21
23
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
+
22
37
/// Delete a crate.
23
38
///
24
39
/// The crate is immediately deleted from the database, and with a small delay
@@ -31,12 +46,17 @@ const AVAILABLE_AFTER: TimeDelta = TimeDelta::hours(24);
31
46
#[ utoipa:: path(
32
47
delete,
33
48
path = "/api/v1/crates/{name}" ,
34
- params( CratePath ) ,
49
+ params( CratePath , DeleteQueryParams ) ,
35
50
security( ( "cookie" = [ ] ) ) ,
36
51
tag = "crates" ,
37
52
responses( ( status = 200 , description = "Successful Response" ) ) ,
38
53
) ]
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 > {
40
60
let mut conn = app. db_write ( ) . await ?;
41
61
42
62
// Check that the user is authenticated
@@ -96,6 +116,7 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe
96
116
. deleted_at ( & deleted_at)
97
117
. deleted_by ( user. id )
98
118
. available_at ( & available_at)
119
+ . maybe_message ( params. message ( ) )
99
120
. build ( ) ;
100
121
101
122
diesel:: insert_into ( deleted_crates:: table)
@@ -200,12 +221,36 @@ mod tests {
200
221
use crate :: models:: OwnerKind ;
201
222
use crate :: tests:: builders:: { DependencyBuilder , PublishBuilder } ;
202
223
use crate :: tests:: util:: { RequestHelper , Response , TestApp } ;
224
+ use axum:: RequestPartsExt ;
203
225
use crates_io_database:: schema:: crate_owners;
204
226
use diesel_async:: AsyncPgConnection ;
205
- use http:: StatusCode ;
227
+ use http:: { Request , StatusCode } ;
206
228
use insta:: assert_snapshot;
207
229
use serde_json:: json;
208
230
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
+
209
254
#[ tokio:: test( flavor = "multi_thread" ) ]
210
255
async fn test_happy_path_new_crate ( ) -> anyhow:: Result < ( ) > {
211
256
let ( app, anon, user) = TestApp :: full ( ) . with_user ( ) . await ;
0 commit comments