@@ -589,16 +589,12 @@ impl AzureClient {
589
589
Ok ( ( ) )
590
590
}
591
591
592
- pub async fn bulk_delete_request ( & self , paths : Vec < Path > ) -> Result < Vec < Result < Path > > > {
593
- if paths. is_empty ( ) {
594
- return Ok ( Vec :: new ( ) ) ;
595
- }
596
-
597
- let credential = self . get_credential ( ) . await ?;
598
-
599
- // https://www.ietf.org/rfc/rfc2046
600
- let boundary = format ! ( "batch_{}" , uuid:: Uuid :: new_v4( ) ) ;
601
-
592
+ fn build_bulk_delete_body (
593
+ & self ,
594
+ boundary : & str ,
595
+ paths : & [ Path ] ,
596
+ credential : & Option < Arc < AzureCredential > > ,
597
+ ) -> Vec < u8 > {
602
598
let mut body_bytes = Vec :: with_capacity ( paths. len ( ) * 2048 ) ;
603
599
604
600
for ( idx, path) in paths. iter ( ) . enumerate ( ) {
@@ -612,21 +608,35 @@ impl AzureClient {
612
608
// Each subrequest must be authorized individually [1] and we use
613
609
// the CredentialExt for this.
614
610
// [1]: https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id#request-body
615
- . with_azure_authorization ( & credential, & self . config . account )
611
+ . with_azure_authorization ( credential, & self . config . account )
616
612
. build ( )
617
613
. unwrap ( ) ;
618
614
619
615
// Url for part requests must be relative and without base
620
616
let relative_url = self . config . service . make_relative ( request. url ( ) ) . unwrap ( ) ;
621
617
622
- serialize_part_delete_request ( & mut body_bytes, & boundary, idx, request, relative_url)
618
+ serialize_part_delete_request ( & mut body_bytes, boundary, idx, request, relative_url)
623
619
}
624
620
625
621
// Encode end marker
626
622
extend ( & mut body_bytes, b"--" ) ;
627
623
extend ( & mut body_bytes, boundary. as_bytes ( ) ) ;
628
624
extend ( & mut body_bytes, b"--" ) ;
629
625
extend ( & mut body_bytes, b"\r \n " ) ;
626
+ body_bytes
627
+ }
628
+
629
+ pub async fn bulk_delete_request ( & self , paths : Vec < Path > ) -> Result < Vec < Result < Path > > > {
630
+ if paths. is_empty ( ) {
631
+ return Ok ( Vec :: new ( ) ) ;
632
+ }
633
+
634
+ let credential = self . get_credential ( ) . await ?;
635
+
636
+ // https://www.ietf.org/rfc/rfc2046
637
+ let boundary = format ! ( "batch_{}" , uuid:: Uuid :: new_v4( ) ) ;
638
+
639
+ let body_bytes = self . build_bulk_delete_body ( & boundary, & paths, & credential) ;
630
640
631
641
// Send multipart request
632
642
let url = self . config . path_url ( & Path :: from ( "/" ) ) ;
@@ -1085,8 +1095,10 @@ pub(crate) struct UserDelegationKey {
1085
1095
#[ cfg( test) ]
1086
1096
mod tests {
1087
1097
use bytes:: Bytes ;
1098
+ use regex:: bytes:: Regex ;
1088
1099
1089
1100
use super :: * ;
1101
+ use crate :: StaticCredentialProvider ;
1090
1102
1091
1103
#[ test]
1092
1104
fn deserde_azure ( ) {
@@ -1276,4 +1288,156 @@ mod tests {
1276
1288
let _delegated_key_response_internal: UserDelegationKey =
1277
1289
quick_xml:: de:: from_str ( S ) . unwrap ( ) ;
1278
1290
}
1291
+
1292
+ #[ tokio:: test]
1293
+ async fn test_build_bulk_delete_body ( ) {
1294
+ let credential_provider = Arc :: new ( StaticCredentialProvider :: new (
1295
+ AzureCredential :: BearerToken ( "static-token" . to_string ( ) ) ,
1296
+ ) ) ;
1297
+
1298
+ let config = AzureConfig {
1299
+ account : "testaccount" . to_string ( ) ,
1300
+ container : "testcontainer" . to_string ( ) ,
1301
+ credentials : credential_provider,
1302
+ service : "http://example.com" . try_into ( ) . unwrap ( ) ,
1303
+ retry_config : Default :: default ( ) ,
1304
+ is_emulator : false ,
1305
+ skip_signature : false ,
1306
+ disable_tagging : false ,
1307
+ client_options : Default :: default ( ) ,
1308
+ } ;
1309
+
1310
+ let client = AzureClient :: new ( config) . unwrap ( ) ;
1311
+
1312
+ let credential = client. get_credential ( ) . await . unwrap ( ) ;
1313
+ let paths = & [ Path :: from ( "a" ) , Path :: from ( "b" ) , Path :: from ( "c" ) ] ;
1314
+
1315
+ let boundary = "batch_statictestboundary" . to_string ( ) ;
1316
+
1317
+ let body_bytes = client. build_bulk_delete_body ( & boundary, paths, & credential) ;
1318
+
1319
+ // Replace Date header value with a static date
1320
+ let re = Regex :: new ( "Date:[^\r ]+" ) . unwrap ( ) ;
1321
+ let body_bytes = re
1322
+ . replace_all ( & body_bytes, b"Date: Tue, 05 Nov 2024 15:01:15 GMT" )
1323
+ . to_vec ( ) ;
1324
+
1325
+ let expected_body = b"--batch_statictestboundary\r
1326
+ Content-Type: application/http\r
1327
+ Content-Transfer-Encoding: binary\r
1328
+ Content-ID: 0\r
1329
+ \r
1330
+ DELETE /testcontainer/a HTTP/1.1\r
1331
+ Content-Length: 0\r
1332
+ Date: Tue, 05 Nov 2024 15:01:15 GMT\r
1333
+ X-Ms-Version: 2023-11-03\r
1334
+ Authorization: Bearer static-token\r
1335
+ \r
1336
+ \r
1337
+ --batch_statictestboundary\r
1338
+ Content-Type: application/http\r
1339
+ Content-Transfer-Encoding: binary\r
1340
+ Content-ID: 1\r
1341
+ \r
1342
+ DELETE /testcontainer/b HTTP/1.1\r
1343
+ Content-Length: 0\r
1344
+ Date: Tue, 05 Nov 2024 15:01:15 GMT\r
1345
+ X-Ms-Version: 2023-11-03\r
1346
+ Authorization: Bearer static-token\r
1347
+ \r
1348
+ \r
1349
+ --batch_statictestboundary\r
1350
+ Content-Type: application/http\r
1351
+ Content-Transfer-Encoding: binary\r
1352
+ Content-ID: 2\r
1353
+ \r
1354
+ DELETE /testcontainer/c HTTP/1.1\r
1355
+ Content-Length: 0\r
1356
+ Date: Tue, 05 Nov 2024 15:01:15 GMT\r
1357
+ X-Ms-Version: 2023-11-03\r
1358
+ Authorization: Bearer static-token\r
1359
+ \r
1360
+ \r
1361
+ --batch_statictestboundary--\r \n "
1362
+ . to_vec ( ) ;
1363
+
1364
+ assert_eq ! ( expected_body, body_bytes) ;
1365
+ }
1366
+
1367
+ #[ tokio:: test]
1368
+ async fn test_parse_blob_batch_delete_response ( ) {
1369
+ let response_body = b"--batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r
1370
+ Content-Type: application/http\r
1371
+ Content-ID: 0\r
1372
+ \r
1373
+ HTTP/1.1 202 Accepted\r
1374
+ x-ms-delete-type-permanent: true\r
1375
+ x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e284f\r
1376
+ x-ms-version: 2018-11-09\r
1377
+ \r
1378
+ --batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r
1379
+ Content-Type: application/http\r
1380
+ Content-ID: 1\r
1381
+ \r
1382
+ HTTP/1.1 202 Accepted\r
1383
+ x-ms-delete-type-permanent: true\r
1384
+ x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e2851\r
1385
+ x-ms-version: 2018-11-09\r
1386
+ \r
1387
+ --batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed\r
1388
+ Content-Type: application/http\r
1389
+ Content-ID: 2\r
1390
+ \r
1391
+ HTTP/1.1 404 The specified blob does not exist.\r
1392
+ x-ms-error-code: BlobNotFound\r
1393
+ x-ms-request-id: 778fdc83-801e-0000-62ff-0334671e2852\r
1394
+ x-ms-version: 2018-11-09\r
1395
+ Content-Length: 216\r
1396
+ Content-Type: application/xml\r
1397
+ \r
1398
+ <?xml version=\" 1.0\" encoding=\" utf-8\" ?>
1399
+ <Error><Code>BlobNotFound</Code><Message>The specified blob does not exist.
1400
+ RequestId:778fdc83-801e-0000-62ff-0334671e2852
1401
+ Time:2018-06-14T16:46:54.6040685Z</Message></Error>\r
1402
+ --batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed--\r \n " ;
1403
+
1404
+ let response: reqwest:: Response = http:: Response :: builder ( )
1405
+ . status ( 202 )
1406
+ . header ( "Transfer-Encoding" , "chunked" )
1407
+ . header (
1408
+ "Content-Type" ,
1409
+ "multipart/mixed; boundary=batchresponse_66925647-d0cb-4109-b6d3-28efe3e1e5ed" ,
1410
+ )
1411
+ . header ( "x-ms-request-id" , "778fdc83-801e-0000-62ff-033467000000" )
1412
+ . header ( "x-ms-version" , "2018-11-09" )
1413
+ . body ( Bytes :: from ( response_body. as_slice ( ) ) )
1414
+ . unwrap ( )
1415
+ . into ( ) ;
1416
+
1417
+ let paths = & [ Path :: from ( "a" ) , Path :: from ( "b" ) , Path :: from ( "c" ) ] ;
1418
+
1419
+ let results = parse_blob_batch_delete_response ( response, paths)
1420
+ . await
1421
+ . unwrap ( ) ;
1422
+
1423
+ assert ! ( results[ 0 ] . is_ok( ) ) ;
1424
+ assert_eq ! ( & paths[ 0 ] , results[ 0 ] . as_ref( ) . unwrap( ) ) ;
1425
+
1426
+ assert ! ( results[ 1 ] . is_ok( ) ) ;
1427
+ assert_eq ! ( & paths[ 1 ] , results[ 1 ] . as_ref( ) . unwrap( ) ) ;
1428
+
1429
+ assert ! ( results[ 2 ] . is_err( ) ) ;
1430
+ let err = results[ 2 ] . as_ref ( ) . unwrap_err ( ) ;
1431
+ let crate :: Error :: NotFound { source, .. } = err else {
1432
+ unreachable ! ( "must be not found" )
1433
+ } ;
1434
+ let Some ( Error :: DeleteFailed { path, code, reason } ) = source. downcast_ref :: < Error > ( )
1435
+ else {
1436
+ unreachable ! ( "must be client error" )
1437
+ } ;
1438
+
1439
+ assert_eq ! ( paths[ 2 ] . as_ref( ) , path) ;
1440
+ assert_eq ! ( "404" , code) ;
1441
+ assert_eq ! ( "The specified blob does not exist." , reason) ;
1442
+ }
1279
1443
}
0 commit comments