from dataclasses import dataclass from pathlib import Path from flask import ( Blueprint, current_app, flash, redirect, render_template, request, send_file, url_for, ) from werkzeug.datastructures import FileStorage from app.constants import MODES, OUTPUT_FORMATS, SORTABLE_FIELDS, SORT_ORDERS from app.services.exporter import build_export from app.services.parser import LogParseError, parse_log_file from app.services.processing import ( ProcessingError, ProcessingOptions, filter_records, sort_records, ) from app.services.storage import load_result_metadata, persist_result main_blueprint = Blueprint("main", __name__) @dataclass(slots=True) class FormData: mode: str output_format: str sort_by: str order: str policy_cs: str policy_ci: str severity_cs: str severity_ci: str def _normalize_form() -> FormData: return FormData( mode=request.form.get("mode", "vendor").strip(), output_format=request.form.get("output_format", "text").strip(), sort_by=request.form.get("sort_by", "datetime").strip(), order=request.form.get("order", "asc").strip(), policy_cs=request.form.get("policy_cs", "").strip(), policy_ci=request.form.get("policy_ci", "").strip(), severity_cs=request.form.get("severity_cs", "").strip(), severity_ci=request.form.get("severity_ci", "").strip(), ) def _validate_form(file: FileStorage | None, form: FormData) -> list[str]: errors: list[str] = [] if file is None or not file.filename: errors.append("Please choose a log file to upload.") if form.mode not in MODES: errors.append("Invalid mode selection.") if form.output_format not in OUTPUT_FORMATS: errors.append("Invalid output format selection.") if form.sort_by not in SORTABLE_FIELDS: errors.append("Invalid sort field selection.") if form.order not in SORT_ORDERS: errors.append("Invalid sort order selection.") if form.policy_cs and form.policy_ci: errors.append( "Policy filter must use either case-sensitive or case-insensitive match, not both." ) if form.severity_cs and form.severity_ci: errors.append( "Severity filter must use either case-sensitive or case-insensitive match, not both." ) return errors @main_blueprint.get("/") def index(): return render_template("index.html") @main_blueprint.post("/convert") def convert(): uploaded_file = request.files.get("log_file") form = _normalize_form() errors = _validate_form(uploaded_file, form) if errors: for error in errors: flash(error, "danger") return render_template("index.html", form=form), 400 assert uploaded_file is not None try: records, union_keys = parse_log_file(uploaded_file.stream) options = ProcessingOptions( policy_cs=form.policy_cs, policy_ci=form.policy_ci, severity_cs=form.severity_cs, severity_ci=form.severity_ci, sort_by=form.sort_by, order=form.order, mode=form.mode, ) filtered_records = filter_records(records, options) sorted_records = sort_records(filtered_records, options) export_result = build_export(sorted_records, union_keys, form.mode, form.output_format) metadata = persist_result( output_dir=current_app.config["OUTPUT_DIRECTORY"], export_result=export_result, ) except (LogParseError, ProcessingError) as exc: flash(str(exc), "danger") return render_template("index.html", form=form), 400 except UnicodeDecodeError: flash("The uploaded file is not valid UTF-8 text.", "danger") return render_template("index.html", form=form), 400 preview_limit = current_app.config["PREVIEW_RECORD_LIMIT"] return render_template( "result.html", result_id=metadata.result_id, preview_text=export_result.preview(preview_limit), output_format=form.output_format, record_count=len(sorted_records), parsed_count=len(records), filtered_count=len(sorted_records), mode=form.mode, sort_by=form.sort_by, order=form.order, ) @main_blueprint.get("/download/") def download(result_id: str): metadata = load_result_metadata(current_app.config["OUTPUT_DIRECTORY"], result_id) if metadata is None: flash("Requested output file could not be found.", "danger") return redirect(url_for("main.index")) return send_file( Path(metadata["file_path"]), as_attachment=True, download_name=metadata["download_name"], mimetype=metadata["mimetype"], max_age=0, )