Skip to content

Commit 54f11c9

Browse files
committed
Improve scenario zipping and path sanitization
Sanitizes scenario names and paths to prevent filesystem issues and ensures correct zip archive structure. Adds checks for existence of input and output directories before processing, and raises an error if summary export results are missing.
1 parent 785b1b9 commit 54f11c9

File tree

1 file changed

+17
-10
lines changed

1 file changed

+17
-10
lines changed

cea/interfaces/dashboard/api/contents.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -445,46 +445,53 @@ async def download_scenario(form: DownloadScenario, project_root: CEAProjectRoot
445445

446446
# Use compresslevel=1 for faster zipping, at the cost of compression ratio
447447
with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED, compresslevel=1) as zip_file:
448-
base_path = Path(project_root) / project
448+
base_path = Path(secure_path(Path(project_root, project).resolve()))
449449

450450
# Collect all files first for batch processing
451451
files_to_zip = []
452452
for scenario in scenarios:
453-
scenario_path = base_path / scenario
453+
# sanitize scenario for fs ops and zip arcnames
454+
scenario_name = Path(scenario).name
455+
scenario_path = Path(secure_path((base_path / scenario_name).resolve()))
456+
454457
if not scenario_path.exists():
455458
continue
456459

457-
if input_files:
458-
input_paths = (scenario_path / "inputs")
460+
input_paths = (scenario_path / "inputs")
461+
if input_files and input_paths.exists():
459462
for root, dirs, files in os.walk(input_paths):
460463
root_path = Path(root)
461464
for file in files:
462465
if Path(file).suffix in VALID_EXTENSIONS:
463466
item_path = root_path / file
464-
relative_path = str(Path(scenario) / "inputs" / item_path.relative_to(input_paths))
467+
relative_path = str(Path(scenario_name) / "inputs" / item_path.relative_to(input_paths))
465468
files_to_zip.append((item_path, relative_path))
466469

467-
if output_files_level == "detailed":
468-
output_paths = (scenario_path / "outputs")
470+
output_paths = (scenario_path / "outputs")
471+
if output_files_level == "detailed" and output_paths.exists():
469472
for root, dirs, files in os.walk(output_paths):
470473
root_path = Path(root)
471474
for file in files:
472475
if Path(file).suffix in VALID_EXTENSIONS:
473476
item_path = root_path / file
474-
relative_path = str(Path(scenario) / "outputs" / item_path.relative_to(output_paths))
477+
relative_path = str(Path(scenario_name) / "outputs" / item_path.relative_to(output_paths))
475478
files_to_zip.append((item_path, relative_path))
479+
476480
elif output_files_level == "summary":
477481
# create summary files first
478-
await run_in_threadpool(run_summary, str(base_path), scenario)
482+
await run_in_threadpool(run_summary, str(base_path), scenario_name)
479483

480484
export_paths = (scenario_path / "export" / "results")
485+
if not export_paths.exists():
486+
raise ValueError(f"Export results path does not exist for scenario {scenario_name}")
487+
481488
for root, dirs, files in os.walk(export_paths):
482489
root_path = Path(root)
483490
for file in files:
484491
if Path(file).suffix in VALID_EXTENSIONS:
485492
item_path = root_path / file
486493
relative_path = str(
487-
Path(scenario) / "export" / "results" / item_path.relative_to(export_paths))
494+
Path(scenario_name) / "export" / "results" / item_path.relative_to(export_paths))
488495
files_to_zip.append((item_path, relative_path))
489496

490497
# Batch write all files to zip

0 commit comments

Comments
 (0)