4747logger = get_logger ('Code Scanner' )
4848
4949
50+ class _UploadProgressAggregator :
51+ """Aggregates upload progress across parallel batch uploads for display in the progress bar."""
52+
53+ def __init__ (self , progress_bar : 'BaseProgressBar' ) -> None :
54+ self ._progress_bar = progress_bar
55+ self ._slots : list [list [int ]] = []
56+
57+ def create_callback (self ) -> Callable [[int , int ], None ]:
58+ """Create a progress callback for one batch upload. Each batch gets its own slot."""
59+ slot = [0 , 0 ]
60+ self ._slots .append (slot )
61+
62+ def on_upload_progress (bytes_read : int , total_bytes : int ) -> None :
63+ slot [0 ] = bytes_read
64+ slot [1 ] = total_bytes
65+
66+ # Sum across all batch slots to show combined progress
67+ total_read = sum (s [0 ] for s in self ._slots )
68+ total_size = sum (s [1 ] for s in self ._slots )
69+
70+ if total_read >= total_size :
71+ self ._progress_bar .update_right_side_label (None )
72+ else :
73+ mb_read = total_read / (1024 * 1024 )
74+ mb_total = total_size / (1024 * 1024 )
75+ self ._progress_bar .update_right_side_label (f'Uploading { mb_read :.1f} / { mb_total :.1f} MB' )
76+
77+ return on_upload_progress
78+
79+
5080def scan_disk_files (ctx : typer .Context , paths : tuple [str , ...]) -> None :
5181 scan_type = ctx .obj ['scan_type' ]
5282 progress_bar = ctx .obj ['progress_bar' ]
@@ -121,6 +151,9 @@ def _get_scan_documents_thread_func(
121151 severity_threshold = ctx .obj ['severity_threshold' ]
122152 sync_option = ctx .obj ['sync' ]
123153 command_scan_type = ctx .info_name
154+ progress_bar = ctx .obj ['progress_bar' ]
155+
156+ aggregator = _UploadProgressAggregator (progress_bar )
124157
125158 def _scan_batch_thread_func (batch : list [Document ]) -> tuple [str , CliError , LocalScanResult ]:
126159 local_scan_result = error = error_message = None
@@ -143,6 +176,7 @@ def _scan_batch_thread_func(batch: list[Document]) -> tuple[str, CliError, Local
143176 is_commit_range ,
144177 scan_parameters ,
145178 should_use_sync_flow ,
179+ on_upload_progress = aggregator .create_callback (),
146180 )
147181
148182 enrich_scan_result_with_data_from_detection_rules (cycode_client , scan_result )
@@ -268,11 +302,14 @@ def _perform_scan_v4_async(
268302 scan_parameters : dict ,
269303 is_git_diff : bool ,
270304 is_commit_range : bool ,
305+ on_upload_progress : Optional [Callable ] = None ,
271306) -> ZippedFileScanResult :
272307 upload_link = cycode_client .get_upload_link (scan_type )
273308 logger .debug ('Got upload link, %s' , {'upload_id' : upload_link .upload_id })
274309
275- cycode_client .upload_to_presigned_post (upload_link .url , upload_link .presigned_post_fields , zipped_documents )
310+ cycode_client .upload_to_presigned_post (
311+ upload_link .url , upload_link .presigned_post_fields , zipped_documents , on_upload_progress
312+ )
276313 logger .debug ('Uploaded zip to presigned URL' )
277314
278315 scan_async_result = cycode_client .scan_repository_from_upload_id (
@@ -292,9 +329,14 @@ def _perform_scan_async(
292329 scan_type : str ,
293330 scan_parameters : dict ,
294331 is_commit_range : bool ,
332+ on_upload_progress : Optional [Callable ] = None ,
295333) -> ZippedFileScanResult :
296334 scan_async_result = cycode_client .zipped_file_scan_async (
297- zipped_documents , scan_type , scan_parameters , is_commit_range = is_commit_range
335+ zipped_documents ,
336+ scan_type ,
337+ scan_parameters ,
338+ is_commit_range = is_commit_range ,
339+ on_upload_progress = on_upload_progress ,
298340 )
299341 logger .debug ('Async scan request has been triggered successfully, %s' , {'scan_id' : scan_async_result .scan_id })
300342
@@ -326,6 +368,7 @@ def _perform_scan(
326368 is_commit_range : bool ,
327369 scan_parameters : dict ,
328370 should_use_sync_flow : bool = False ,
371+ on_upload_progress : Optional [Callable ] = None ,
329372) -> ZippedFileScanResult :
330373 if should_use_sync_flow :
331374 # it does not support commit range scans; should_use_sync_flow handles it
@@ -334,12 +377,20 @@ def _perform_scan(
334377 if should_use_presigned_upload (scan_type ):
335378 try :
336379 return _perform_scan_v4_async (
337- cycode_client , zipped_documents , scan_type , scan_parameters , is_git_diff , is_commit_range
380+ cycode_client ,
381+ zipped_documents ,
382+ scan_type ,
383+ scan_parameters ,
384+ is_git_diff ,
385+ is_commit_range ,
386+ on_upload_progress ,
338387 )
339388 except requests .exceptions .RequestException :
340389 logger .warning ('Direct upload to object storage failed. Falling back to upload via Cycode API. ' )
341390
342- return _perform_scan_async (cycode_client , zipped_documents , scan_type , scan_parameters , is_commit_range )
391+ return _perform_scan_async (
392+ cycode_client , zipped_documents , scan_type , scan_parameters , is_commit_range , on_upload_progress
393+ )
343394
344395
345396def poll_scan_results (
0 commit comments