@@ -2346,6 +2346,11 @@ def progress_reporter(test_id, token):
23462346 if not upload_type_request (log , test_id , repo_folder , test , request ):
23472347 return "EMPTY"
23482348
2349+ elif request .form ['type' ] == 'artifactupload' :
2350+ log .info (f'[PROGRESS_REPORTER][Test: { test_id } ] Artifact upload' )
2351+ if not upload_artifact_type_request (log , test_id , repo_folder , test , request ):
2352+ return "EMPTY"
2353+
23492354 elif request .form ['type' ] == 'finish' :
23502355 log .info (f'[PROGRESS_REPORTER][Test: { test_id } ] Test finished' )
23512356 finish_type_request (log , test_id , test , request )
@@ -2675,6 +2680,78 @@ def upload_type_request(log, test_id, repo_folder, test, request) -> bool:
26752680 return False
26762681
26772682
2683+ # Artifact types the CI VM may upload, mapped to the fixed filenames the REST
2684+ # API expects under <SAMPLE_REPOSITORY>/test_artifacts/<run_id>/ (see the
2685+ # system route _get_gcs_artifacts in mod_api).
2686+ ARTIFACT_TYPES = frozenset ({'binary' , 'coredump' , 'combined_stdout' })
2687+
2688+
2689+ def _artifact_target_name (artifact_type : str , platform ) -> Optional [str ]:
2690+ """Map an artifact type + platform to the exact filename the API resolves."""
2691+ if artifact_type == 'binary' :
2692+ return 'ccextractor' if platform == TestPlatform .linux else 'ccextractorwinfull.exe'
2693+ if artifact_type == 'coredump' :
2694+ return 'coredump'
2695+ if artifact_type == 'combined_stdout' :
2696+ return 'combined_stdout.log'
2697+ return None
2698+
2699+
2700+ def upload_artifact_type_request (log , test_id , repo_folder , test , request ) -> bool :
2701+ """
2702+ Handle the artifactupload request type for the progress reporter.
2703+
2704+ Stores a CI artifact (binary, coredump, or combined stdout log) under
2705+ ``<SAMPLE_REPOSITORY>/test_artifacts/<test_id>/`` with the fixed name the
2706+ REST API expects. The target name is derived server-side from
2707+ ``artifact_type`` and the test platform, never from the uploaded filename,
2708+ so a crafted filename cannot escape the artifact directory.
2709+
2710+ :param log: logger
2711+ :type log: Logger
2712+ :param test_id: the id of the test the artifact belongs to
2713+ :type test_id: int
2714+ :param repo_folder: SAMPLE_REPOSITORY path
2715+ :type repo_folder: str
2716+ :param test: the concerned test
2717+ :type test: Test
2718+ :param request: request parameters
2719+ :type request: Request
2720+ :return: True on success, False on validation failure
2721+ :rtype: bool
2722+ """
2723+ artifact_type = request .form .get ('artifact_type' , '' )
2724+ if artifact_type not in ARTIFACT_TYPES :
2725+ log .warning (f'[Test: { test_id } ] Rejected artifact upload: bad artifact_type { artifact_type !r} ' )
2726+ return False
2727+
2728+ if 'file' not in request .files :
2729+ log .warning (f'[Test: { test_id } ] Artifact upload missing file' )
2730+ return False
2731+
2732+ uploaded_file = request .files ['file' ]
2733+ if secure_filename (uploaded_file .filename or '' ) == '' :
2734+ log .warning (f'[Test: { test_id } ] Artifact upload has empty filename' )
2735+ return False
2736+
2737+ target_name = _artifact_target_name (artifact_type , test .platform )
2738+ if target_name is None :
2739+ return False
2740+
2741+ artifact_dir = os .path .join (repo_folder , 'test_artifacts' , str (test .id ))
2742+ temp_dir = os .path .join (repo_folder , 'TempFiles' )
2743+ os .makedirs (artifact_dir , exist_ok = True )
2744+ os .makedirs (temp_dir , exist_ok = True )
2745+
2746+ temp_path = os .path .join (temp_dir , f'artifact_{ test .id } _{ target_name } ' )
2747+ uploaded_file .save (temp_path )
2748+ final_path = os .path .join (artifact_dir , target_name )
2749+ os .replace (temp_path , final_path )
2750+
2751+ log .info (f'[Test: { test_id } ] Stored { artifact_type } artifact at { final_path } ' )
2752+ return True
2753+
2754+
26782755def finish_type_request (log , test_id , test , request ):
26792756 """
26802757 Handle finish request type for progress reporter.
0 commit comments