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' ]
@@ -120,6 +150,9 @@ def _get_scan_documents_thread_func(
120150 severity_threshold = ctx .obj ['severity_threshold' ]
121151 sync_option = ctx .obj ['sync' ]
122152 command_scan_type = ctx .info_name
153+ progress_bar = ctx .obj ['progress_bar' ]
154+
155+ aggregator = _UploadProgressAggregator (progress_bar )
123156
124157 def _scan_batch_thread_func (batch : list [Document ]) -> tuple [str , CliError , LocalScanResult ]:
125158 local_scan_result = error = error_message = None
@@ -142,6 +175,7 @@ def _scan_batch_thread_func(batch: list[Document]) -> tuple[str, CliError, Local
142175 is_commit_range ,
143176 scan_parameters ,
144177 should_use_sync_flow ,
178+ on_upload_progress = aggregator .create_callback (),
145179 )
146180
147181 enrich_scan_result_with_data_from_detection_rules (cycode_client , scan_result )
@@ -267,11 +301,14 @@ def _perform_scan_v4_async(
267301 scan_parameters : dict ,
268302 is_git_diff : bool ,
269303 is_commit_range : bool ,
304+ on_upload_progress : Optional [Callable ] = None ,
270305) -> ZippedFileScanResult :
271306 upload_link = cycode_client .get_upload_link (scan_type )
272307 logger .debug ('Got upload link, %s' , {'upload_id' : upload_link .upload_id })
273308
274- cycode_client .upload_to_presigned_post (upload_link .url , upload_link .presigned_post_fields , zipped_documents )
309+ cycode_client .upload_to_presigned_post (
310+ upload_link .url , upload_link .presigned_post_fields , zipped_documents , on_upload_progress
311+ )
275312 logger .debug ('Uploaded zip to presigned URL' )
276313
277314 scan_async_result = cycode_client .scan_repository_from_upload_id (
@@ -291,9 +328,14 @@ def _perform_scan_async(
291328 scan_type : str ,
292329 scan_parameters : dict ,
293330 is_commit_range : bool ,
331+ on_upload_progress : Optional [Callable ] = None ,
294332) -> ZippedFileScanResult :
295333 scan_async_result = cycode_client .zipped_file_scan_async (
296- zipped_documents , scan_type , scan_parameters , is_commit_range = is_commit_range
334+ zipped_documents ,
335+ scan_type ,
336+ scan_parameters ,
337+ is_commit_range = is_commit_range ,
338+ on_upload_progress = on_upload_progress ,
297339 )
298340 logger .debug ('Async scan request has been triggered successfully, %s' , {'scan_id' : scan_async_result .scan_id })
299341
@@ -325,6 +367,7 @@ def _perform_scan(
325367 is_commit_range : bool ,
326368 scan_parameters : dict ,
327369 should_use_sync_flow : bool = False ,
370+ on_upload_progress : Optional [Callable ] = None ,
328371) -> ZippedFileScanResult :
329372 if should_use_sync_flow :
330373 # it does not support commit range scans; should_use_sync_flow handles it
@@ -333,12 +376,20 @@ def _perform_scan(
333376 if should_use_presigned_upload (scan_type ):
334377 try :
335378 return _perform_scan_v4_async (
336- cycode_client , zipped_documents , scan_type , scan_parameters , is_git_diff , is_commit_range
379+ cycode_client ,
380+ zipped_documents ,
381+ scan_type ,
382+ scan_parameters ,
383+ is_git_diff ,
384+ is_commit_range ,
385+ on_upload_progress ,
337386 )
338387 except requests .exceptions .RequestException :
339388 logger .warning ('Direct upload to object storage failed. Falling back to upload via Cycode API. ' )
340389
341- return _perform_scan_async (cycode_client , zipped_documents , scan_type , scan_parameters , is_commit_range )
390+ return _perform_scan_async (
391+ cycode_client , zipped_documents , scan_type , scan_parameters , is_commit_range , on_upload_progress
392+ )
342393
343394
344395def poll_scan_results (
0 commit comments