From f81e99726acb088989fb70ad6672216d3edc0b89 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 29 May 2026 11:22:12 +0200 Subject: [PATCH] split handler and cors into separate files --- bin/cors.ml | 35 +++++++++++ bin/main.ml | 141 +-------------------------------------------- bin/pdf_handler.ml | 108 ++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 139 deletions(-) create mode 100644 bin/cors.ml create mode 100644 bin/pdf_handler.ml diff --git a/bin/cors.ml b/bin/cors.ml new file mode 100644 index 0000000..1e50226 --- /dev/null +++ b/bin/cors.ml @@ -0,0 +1,35 @@ +let allowed_origins = + [ + "https://chess-scribe.org"; + "https://www.chess-scribe.org"; + "http://localhost:3000"; + ] + +let is_dev_mode () = + Array.to_list Sys.argv + |> List.exists (fun arg -> arg = "DREAM_ENV=development") + +let middleware next_handler request = + let allowed_origin = + if is_dev_mode () then "*" + else + match Dream.header request "Origin" with + | Some origin when List.mem origin allowed_origins -> origin + | _ -> "https://chess-scribe.org" + in + + if Dream.method_ request = `OPTIONS then ( + let response = Dream.response "" in + Dream.add_header response "Access-Control-Allow-Origin" allowed_origin; + Dream.add_header response "Access-Control-Allow-Methods" + "POST, GET, OPTIONS"; + Dream.add_header response "Access-Control-Allow-Headers" + "Content-Type, Authorization"; + Dream.add_header response "Access-Control-Max-Age" "86400"; + Dream.set_status response `No_Content; + Lwt.return response) + else + let open Lwt.Syntax in + let* response = next_handler request in + Dream.add_header response "Access-Control-Allow-Origin" allowed_origin; + Lwt.return response diff --git a/bin/main.ml b/bin/main.ml index 77b6dd4..790ce72 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -2,153 +2,16 @@ open Lwt.Syntax open Yojson.Safe.Util open Pgn_logic -let allowed_origins = - [ - "https://chess-scribe.org"; - "https://www.chess-scribe.org"; - "http://localhost:3000"; - ] - -let is_dev_mode () = - Array.to_list Sys.argv - |> List.exists (fun arg -> arg = "DREAM_ENV=development") - -let cors_middleware next_handler request = - let allowed_origin = - if is_dev_mode () then "*" - else - match Dream.header request "Origin" with - | Some origin when List.mem origin allowed_origins -> origin - | _ -> "https://chess-scribe.org" - in - - if Dream.method_ request = `OPTIONS then ( - let response = Dream.response "" in - Dream.add_header response "Access-Control-Allow-Origin" allowed_origin; - Dream.add_header response "Access-Control-Allow-Methods" - "POST, GET, OPTIONS"; - Dream.add_header response "Access-Control-Allow-Headers" - "Content-Type, Authorization"; - Dream.add_header response "Access-Control-Max-Age" "86400"; - Dream.set_status response `No_Content; - Lwt.return response) - else - let* response = next_handler request in - Dream.add_header response "Access-Control-Allow-Origin" allowed_origin; - Lwt.return response - -let handle_convert request = - let temp_id = - Int64.to_string (Int64.of_float (Unix.gettimeofday () *. 1000.0)) - in - - let temp_dir = Filename.get_temp_dir_name () in - - let tex_file_path = - Filename.concat temp_dir (Printf.sprintf "game-%s.tex" temp_id) - in - - let pdf_file_path = - Filename.concat temp_dir (Printf.sprintf "game-%s.pdf" temp_id) - in - - let log_file_path = - Filename.concat temp_dir (Printf.sprintf "game-%s.log" temp_id) - in - - let aux_file_path = - Filename.concat temp_dir (Printf.sprintf "game-%s.aux" temp_id) - in - - let cleanup () = - [ tex_file_path; pdf_file_path; log_file_path; aux_file_path ] - |> List.iter (fun path -> if Sys.file_exists path then Sys.remove path) - in - - let* body = Dream.body request in - - try - let json = Yojson.Safe.from_string body in - - let pgn = json |> member "pgn" |> to_string in - let diagram_clock = json |> member "diagramClock" |> to_bool in - let diagrams_json = json |> member "diagrams" in - - let diagrams_data = - Yojson.Safe.to_string diagrams_json |> Pgn2tex.parse_diagrams_json - in - - let game_tex = - "\\begin{document}\\begin{multicols}{2}" - ^ Pgn2tex.to_tex pgn ~diagram_data:diagrams_data ~clock:diagram_clock - ^ "\\end{multicols}\\end{document}" - in - - Out_channel.with_open_text tex_file_path (fun oc -> - Out_channel.output_string oc game_tex); - - let argv = - [| - "pdflatex"; - "-fmt"; - "./preambles/chess"; - "-interaction=nonstopmode"; - Printf.sprintf "-output-directory=%s" temp_dir; - tex_file_path; - |] - in - - let pid = - Unix.create_process "pdflatex" argv Unix.stdin Unix.stdout Unix.stderr - in - (* let _, process_status = Unix.waitpid [] pid in *) - let _, _ = Unix.waitpid [] pid in - - if Sys.file_exists pdf_file_path then ( - let pdf_content = - In_channel.with_open_bin pdf_file_path In_channel.input_all - in - - cleanup (); - - (* TODO: record_metrics "SUCCESS" start_time; *) - Dream.respond ~status:`Created - ~headers: - [ - ("Content-Disposition", "inline"); - ("Content-Type", "application/pdf"); - ] - pdf_content) - else failwith "PDF generation failed: output file not found on disk" - with - | Type_error (msg, _) -> - cleanup (); - Dream.json ~status:`Bad_Request - (Printf.sprintf - {|{"type": "error", "message": "JSON structure error: %s"}|} msg) - | Yojson.Json_error msg -> - cleanup (); - Dream.json ~status:`Bad_Request - (Printf.sprintf - {|{"type": "error", "message": "Invalid JSON syntax: %s"}|} msg) - | exn -> - cleanup (); - let err_msg = Printexc.to_string exn in - (* TODO: record_metrics "FAIL" start_time err_msg; *) - (* TODO: logger.error err_msg; *) - Dream.json ~status:`Internal_Server_Error - (Printf.sprintf {|{"type": "error", "message": "%s"}|} err_msg) - let () = Dream.run ~interface:"0.0.0.0" ~port:8080 - @@ Dream.logger @@ cors_middleware + @@ Dream.logger @@ Cors.middleware @@ Dream.router [ Dream.get "/" (fun request -> Dream.from_filesystem "static" "doc.html" request); Dream.get "/health" (fun _ -> Dream.json {|{"type": "success", "message": "API is functional"}|}); - Dream.post "/api/v1/pdf" handle_convert; + Dream.post "/api/v1/pdf" Pdf_handler.handle_convert; Dream.get "/api/v1" (fun request -> Dream.from_filesystem "static" "doc.html" request); Dream.get "/swagger.json" (fun request -> diff --git a/bin/pdf_handler.ml b/bin/pdf_handler.ml new file mode 100644 index 0000000..4bee05f --- /dev/null +++ b/bin/pdf_handler.ml @@ -0,0 +1,108 @@ +open Lwt.Syntax +open Yojson.Safe.Util +open Pgn_logic + +let tex_start = "\\begin{document}\\begin{multicols}{2}" +let tex_end = "\\end{multicols}\\end{document}" + +let handle_convert request = + let temp_id = + Int64.to_string (Int64.of_float (Unix.gettimeofday () *. 1000.0)) + in + + let temp_dir = Filename.get_temp_dir_name () in + + let tex_file_path = + Filename.concat temp_dir (Printf.sprintf "game-%s.tex" temp_id) + in + + let pdf_file_path = + Filename.concat temp_dir (Printf.sprintf "game-%s.pdf" temp_id) + in + + let log_file_path = + Filename.concat temp_dir (Printf.sprintf "game-%s.log" temp_id) + in + + let aux_file_path = + Filename.concat temp_dir (Printf.sprintf "game-%s.aux" temp_id) + in + + let cleanup () = + [ tex_file_path; pdf_file_path; log_file_path; aux_file_path ] + |> List.iter (fun path -> if Sys.file_exists path then Sys.remove path) + in + + let* body = Dream.body request in + + try + let json = Yojson.Safe.from_string body in + + let pgn = json |> member "pgn" |> to_string in + let diagram_clock = json |> member "diagramClock" |> to_bool in + let diagrams_json = json |> member "diagrams" in + + let diagrams_data = + Yojson.Safe.to_string diagrams_json |> Pgn2tex.parse_diagrams_json + in + + let game_tex = + tex_start + ^ Pgn2tex.to_tex pgn ~diagram_data:diagrams_data ~clock:diagram_clock + ^ tex_end + in + + Out_channel.with_open_text tex_file_path (fun oc -> + Out_channel.output_string oc game_tex); + + let argv = + [| + "pdflatex"; + "-fmt"; + "./preambles/chess"; + "-interaction=nonstopmode"; + Printf.sprintf "-output-directory=%s" temp_dir; + tex_file_path; + |] + in + + let pid = + Unix.create_process "pdflatex" argv Unix.stdin Unix.stdout Unix.stderr + in + (* let _, process_status = Unix.waitpid [] pid in *) + let _, _ = Unix.waitpid [] pid in + + if Sys.file_exists pdf_file_path then ( + let pdf_content = + In_channel.with_open_bin pdf_file_path In_channel.input_all + in + + cleanup (); + + (* TODO: record_metrics "SUCCESS" start_time; *) + Dream.respond ~status:`Created + ~headers: + [ + ("Content-Disposition", "inline"); + ("Content-Type", "application/pdf"); + ] + pdf_content) + else failwith "PDF generation failed: output file not found on disk" + with + | Type_error (msg, _) -> + cleanup (); + Dream.json ~status:`Bad_Request + (Printf.sprintf + {|{"type": "error", "message": "JSON structure error: %s"}|} msg) + | Yojson.Json_error msg -> + cleanup (); + Dream.json ~status:`Bad_Request + (Printf.sprintf + {|{"type": "error", "message": "Invalid JSON syntax: %s"}|} msg) + | exn -> + cleanup (); + let err_msg = Printexc.to_string exn in + (* TODO: record_metrics "FAIL" start_time err_msg; *) + (* TODO: logger.error err_msg; *) + Dream.json ~status:`Internal_Server_Error + (Printf.sprintf {|{"type": "error", "message": "%s"}|} err_msg)