1- use std:: collections:: HashMap ;
2- use std:: fs:: File ;
3- use std:: io:: { BufReader , Write } ;
4- use std:: path:: Path ;
5- use std:: sync:: Arc ;
6- use std:: sync:: atomic:: { AtomicU32 , Ordering } ;
7-
8- use futures:: future:: join_all;
9- use futures:: stream:: { self , StreamExt } ;
1+ use std:: {
2+ collections:: HashMap ,
3+ fs:: File ,
4+ io:: { BufReader , Write } ,
5+ path:: Path ,
6+ sync:: Arc ,
7+ } ;
8+
9+ use chrono:: { DateTime , Duration , Local } ;
10+ use futures:: {
11+ future:: join_all,
12+ stream:: { self , StreamExt } ,
13+ } ;
1014use indicatif:: { MultiProgress , ProgressBar , ProgressStyle } ;
15+ use log;
1116use serde:: { Deserialize , Serialize } ;
1217use serde_json:: Value ;
1318use tokio:: sync:: Mutex ;
@@ -35,6 +40,25 @@ pub struct FindDuplicatesParams {
3540 pub concurrency : usize ,
3641}
3742
43+ /// Parameters for pruning SBOMs
44+ #[ derive( Default , Serialize ) ]
45+ pub struct PruneParams {
46+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
47+ pub q : Option < String > ,
48+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
49+ pub limit : Option < u32 > ,
50+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
51+ pub published_before : Option < DateTime < Local > > ,
52+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
53+ pub older_than : Option < i64 > ,
54+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
55+ pub label : Option < Vec < String > > ,
56+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
57+ pub keep_latest : Option < u32 > ,
58+ pub dry_run : bool ,
59+ pub concurrency : usize ,
60+ }
61+
3862/// SBOM entry for duplicate detection
3963#[ derive( Debug , Clone ) ]
4064struct SbomEntry {
@@ -357,24 +381,135 @@ pub async fn delete_by_query(
357381 ) ;
358382 }
359383 return Ok ( DeleteResult {
360- deleted : 0 ,
361- skipped : 0 ,
362- failed : 0 ,
384+ deleted : vec ! [ ] ,
385+ deleted_total : 0 ,
386+ skipped : vec ! [ ] ,
387+ skipped_total : 0 ,
388+ failed : vec ! [ ] ,
389+ failed_total : 0 ,
363390 total,
364391 } ) ;
365392 }
366393
367394 delete_list ( client, entries, concurrency) . await
368395}
369396
370- /// Result of deleting duplicates
397+ /// Prune SBOMs based on the given parameters
398+ pub async fn prune ( client : & ApiClient , params : & PruneParams ) -> Result < DeleteResult , ApiError > {
399+ // Build query parameters for listing SBOMs to prune
400+ let mut query = params. q . as_deref ( ) . unwrap_or ( "" ) . to_string ( ) ;
401+ if let Some ( d) = params. published_before . as_ref ( ) {
402+ query. push_str ( & format ! ( "&published<{}" , d. to_rfc3339( ) ) ) ;
403+ }
404+
405+ if let Some ( older_than) = params. older_than {
406+ let older_than_time = Local :: now ( ) - Duration :: days ( older_than) ;
407+ query. push_str ( & format ! ( "&ingested<{}" , older_than_time. to_rfc3339( ) ) ) ;
408+ }
409+
410+ if let Some ( labels) = & params. label {
411+ for l in labels. iter ( ) {
412+ query. push_str ( & format ! ( "&labels:{}" , l) ) ;
413+ }
414+ }
415+
416+ let ( offset, sort) = match params. keep_latest {
417+ Some ( v) => ( Some ( v) , Some ( "ingested:desc" . to_string ( ) ) ) ,
418+ None => ( None , None ) ,
419+ } ;
420+ let list_params = ListParams {
421+ q : Some ( query) ,
422+ limit : params. limit ,
423+ offset,
424+ sort,
425+ } ;
426+
427+ log:: info!(
428+ "Pruning SBOMs with query: {}, offset: {:?}, sort: {:?}" ,
429+ list_params. q. as_deref( ) . unwrap_or( "" ) ,
430+ list_params. offset,
431+ list_params. sort
432+ ) ;
433+
434+ // Get list of SBOMs matching the criteria
435+ let response = list ( client, & list_params) . await ?;
436+ let parsed: Value = serde_json:: from_str ( & response)
437+ . map_err ( |e| ApiError :: InternalError ( format ! ( "Failed to parse response: {}" , e) ) ) ?;
438+
439+ let items = parsed
440+ . get ( "items" )
441+ . and_then ( |v| v. as_array ( ) )
442+ . ok_or_else ( || ApiError :: InternalError ( "No items in response" . to_string ( ) ) ) ?;
443+
444+ let total = items. len ( ) as u32 ;
445+
446+ // Convert items to delete entries
447+ let entries: Vec < DeleteEntry > = items
448+ . iter ( )
449+ . filter_map ( |item| {
450+ let id = item. get ( "id" ) . and_then ( |v| v. as_str ( ) ) ?;
451+ let document_id = item
452+ . get ( "document_id" )
453+ . and_then ( |v| v. as_str ( ) )
454+ . unwrap_or ( "unknown" ) ;
455+ Some ( DeleteEntry {
456+ id : id. to_string ( ) ,
457+ document_id : document_id. to_string ( ) ,
458+ } )
459+ } )
460+ . collect ( ) ;
461+
462+ // If dry run, just return the count without deleting
463+ if params. dry_run {
464+ return Ok ( DeleteResult {
465+ deleted : vec ! [ ] ,
466+ deleted_total : 0 ,
467+ skipped : vec ! [ ] ,
468+ skipped_total : 0 ,
469+ failed : vec ! [ ] ,
470+ failed_total : 0 ,
471+ total,
472+ } ) ;
473+ }
474+
475+ // Perform the actual deletion
476+ delete_list ( client, entries, params. concurrency ) . await
477+ }
478+
479+ #[ derive( Debug , Clone , Serialize ) ]
480+ /// Result of deleting SBOMs
371481pub struct DeleteResult {
372- pub deleted : u32 ,
373- pub skipped : u32 ,
374- pub failed : u32 ,
482+ pub deleted : Vec < DeletedResult > ,
483+ pub deleted_total : u32 ,
484+ pub skipped : Vec < SkippedResult > ,
485+ pub skipped_total : u32 ,
486+ pub failed : Vec < FailedResult > ,
487+ pub failed_total : u32 ,
375488 pub total : u32 ,
376489}
377490
491+ #[ derive( Debug , Clone , Serialize ) ]
492+ /// Successfully deleted SBOM
493+ pub struct DeletedResult {
494+ pub sbom_id : String ,
495+ pub document_id : String ,
496+ }
497+
498+ #[ derive( Debug , Clone , Serialize ) ]
499+ /// Skipped SBOM (not found)
500+ pub struct SkippedResult {
501+ pub sbom_id : String ,
502+ pub document_id : String ,
503+ }
504+
505+ #[ derive( Debug , Clone , Serialize ) ]
506+ /// Failed to delete SBOM
507+ pub struct FailedResult {
508+ pub sbom_id : String ,
509+ pub document_id : String ,
510+ pub error : String ,
511+ }
512+
378513/// Entry to delete with its document_id for logging
379514#[ derive( Clone ) ]
380515pub struct DeleteEntry {
@@ -421,7 +556,7 @@ pub async fn delete_list(
421556 let total = entries. len ( ) as u32 ;
422557
423558 eprintln ! (
424- "Deleting {} duplicates with {} concurrent requests...\n " ,
559+ "Deleting {} sboms with {} concurrent requests...\n " ,
425560 total, concurrency
426561 ) ;
427562
@@ -432,9 +567,9 @@ pub async fn delete_list(
432567 . progress_chars ( "█▓░" ) ,
433568 ) ;
434569
435- let deleted = Arc :: new ( AtomicU32 :: new ( 0 ) ) ;
436- let skipped = Arc :: new ( AtomicU32 :: new ( 0 ) ) ;
437- let failed = Arc :: new ( AtomicU32 :: new ( 0 ) ) ;
570+ let deleted = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
571+ let skipped = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
572+ let failed = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
438573
439574 stream:: iter ( entries)
440575 . for_each_concurrent ( concurrency, |entry| {
@@ -446,13 +581,26 @@ pub async fn delete_list(
446581 async move {
447582 match delete ( & client, & entry. id ) . await {
448583 Ok ( _) => {
449- deleted. fetch_add ( 1 , Ordering :: Relaxed ) ;
584+ let mut deleted_list = deleted. lock ( ) . await ;
585+ deleted_list. push ( DeletedResult {
586+ sbom_id : entry. id . clone ( ) ,
587+ document_id : entry. document_id . clone ( ) ,
588+ } ) ;
450589 }
451590 Err ( ApiError :: NotFound ( _) ) => {
452- skipped. fetch_add ( 1 , Ordering :: Relaxed ) ;
591+ let mut skipped_list = skipped. lock ( ) . await ;
592+ skipped_list. push ( SkippedResult {
593+ sbom_id : entry. id . clone ( ) ,
594+ document_id : entry. document_id . clone ( ) ,
595+ } ) ;
453596 }
454597 Err ( e) => {
455- failed. fetch_add ( 1 , Ordering :: Relaxed ) ;
598+ let mut failed_list = failed. lock ( ) . await ;
599+ failed_list. push ( FailedResult {
600+ sbom_id : entry. id . clone ( ) ,
601+ document_id : entry. document_id . clone ( ) ,
602+ error : e. to_string ( ) ,
603+ } ) ;
456604 progress. println ( format ! (
457605 "Failed to delete {} (document_id: {}): {}" ,
458606 entry. id, entry. document_id, e
@@ -466,10 +614,17 @@ pub async fn delete_list(
466614
467615 progress. finish_with_message ( "complete" ) ;
468616
617+ let deleted_list = deleted. lock ( ) . await ;
618+ let skipped_list = skipped. lock ( ) . await ;
619+ let failed_list = failed. lock ( ) . await ;
620+
469621 Ok ( DeleteResult {
470- deleted : deleted. load ( Ordering :: Relaxed ) ,
471- skipped : skipped. load ( Ordering :: Relaxed ) ,
472- failed : failed. load ( Ordering :: Relaxed ) ,
622+ deleted : deleted_list. clone ( ) ,
623+ deleted_total : deleted_list. len ( ) as u32 ,
624+ skipped : skipped_list. clone ( ) ,
625+ skipped_total : skipped_list. len ( ) as u32 ,
626+ failed : failed_list. clone ( ) ,
627+ failed_total : failed_list. len ( ) as u32 ,
473628 total,
474629 } )
475630}
@@ -492,9 +647,12 @@ pub async fn delete_duplicates(
492647 ) ;
493648 }
494649 return Ok ( DeleteResult {
495- deleted : 0 ,
496- skipped : 0 ,
497- failed : 0 ,
650+ deleted : vec ! [ ] ,
651+ deleted_total : 0 ,
652+ skipped : vec ! [ ] ,
653+ skipped_total : 0 ,
654+ failed : vec ! [ ] ,
655+ failed_total : 0 ,
498656 total,
499657 } ) ;
500658 }
0 commit comments