How to register a post service to an Eliom client/server app?

Hi !
I encounter some difficulties trying to articulate well client and server code in eliom. I fail registering a Post service to an Eliom app and I do not figure why.
Here some code that I tried, as an example to start the discussion :

open%shared Eliom_content.Html.D
              
let%server service = Eliom_service.create
    ~path:(Eliom_service.Path ["upload"])
    ~meth:(Eliom_service.Get Eliom_parameter.unit)
    ()

let%client service = ~%service

let%shared name () = "demo-upload"

let%shared page_class = "dg-page-demo-upload"

let%server upload_service = Eliom_service.create
   ~path:(Eliom_service.Path ["upload"])
   ~meth:(Eliom_service.Post (Eliom_parameter.unit,
                              Eliom_parameter.file "file"))
   ()

let%client upload_service = ~%upload_service
    
let%shared () =
  Docgenius_base.App.register
    ~service:upload_service
    (fun () file ->
       let () = [%server ( some_code_that_does_smth_server_side : unit ) ] in
       Lwt.return
         (html
            (head (title (txt "Upload")) [])
            (body [h1 [txt "ok"]])))

let%shared page () =
  let f =
    (Form.post_form upload_service
       (fun file ->
          [p [Form.file_input ~name:file ();
              br ();
              Form.input ~input_type:`Submit ~value:"Send" Form.string
             ]]) ()) in
  Lwt.return
    [ h1 [txt "Upload"]
    ; p [txt "form upload"]
    ; f
    ]

Above code is included in ocsigen-start as a demo page, and leads to following error message :

File "demo_upload.eliom", line 28, characters 19-25:
Error: Uninterpreted extension 'server'.

I know from https://ocsigen.org/tuto/6.4/manual/misc that I can create a post service with Eliom_service.create and then register an action. This is fine, but I cannot figure out how to notify back easily that a server side computation were fine (the document were properly uploaded), or notify back a result. Maybe should I register an ocaml handler instead of an action for this purpose ?

Best !

It seems like a Makefile problem.
The error message says that let%server is not recognized.
Are you using the default Makefiles from Ocsigen Start?

Hi Vincent, thanks for your answer.
I use default Makefile from ocsigen start. In particular, Makefile.options contains following lines:

SERVER_FILES          := $(wildcard *.eliomi *.eliom *.ml) \
                         docgenius_i18n.eliom
CLIENT_FILES          := $(wildcard *.eliomi *.eliom) \
                         docgenius_i18n.eliom

And my file is called demo_upload.eliom.
Also, if I comment lines from “let () = [%server” to “: unit ) ] in”, the project compiles and builds up.
My complete error is indeed:

File "demo_upload.eliom", line 28, characters 19-25:
Error: Uninterpreted extension 'server'.
Makefile.os:213: recipe for target '_server/demo_upload.type_mli' failed
make: *** [_server/demo_upload.type_mli] Error 2

Oh yes sorry I didn’t see the [%server ] part.
This does not exist.

If you have a function declared in shared section and want to have a different behaviour on server and client, you can call another function with different implementations on both sides.

If you expect the [%server ] section to be executed on the server even if your page is generated on the client, you need to define a server function.

1 Like

Thanks a lot ! In order to do so, I imagine I have to write something like this:

let%server upload_handler () file =
  some_code_that_does_smth_server_side;
  Lwt.return ()

let%client upload_handler () file =
  ~%(Eliom_client.server_function [%derive.json: Ocsigen_extensions.file_info] upload_handler)

But Ocsigen_extensions.file_info cannot be derived in json. Is there a way to work arround this ?

I would probably do something like this:

let%server do_sthg param = ...
let%client do_sthg param =
  ~%(Eliom_client.server_function ~name:"do_sthg"
       [%derive.json: my_type]
       (Os_session.connected_wrapper do_sthg)
    param

let%shared () =
  Docgenius_base.App.register
    ~service:upload_service
    (fun () file ->
       let%lwt () = do_sthg param in
       Lwt.return
         (html
            (head (title (txt "Upload")) [])
            (body [h1 [txt "ok"]])))

Be careful that the call to the server function will take time.
You may want to do the call asynchronously, for example by displaying some part of the page with Ot_spinner (see example in Ocsigen Start).

1 Like

Thanks a lot for your help Vincent ! Now it works for me :slight_smile:
As I had some difficulties implementing the code, I post below my reasoning and final code (do not hesitate to criticize it):
As I wanted to upload, I had to use Eliom_request_info.get_all_files (but maybe someone as a more straightforward way to do so ?). Indeed param cannot contain a file (a value of type Ocsigen_extensions.file_info) because

  • my_type is the type of param and must be derivable into json
  • type json_file = Ocsigen_extensions.file_info [@@deriving json] is not legal code

Here is the code I came with:

let%server upload_handler () =
  match Eliom_request_info.get_all_files () with
    | None -> Lwt.return ()
    | Some l -> let file = snd (List.hd l) in
      let newname = "path_to/thefile" in
      (try
         Unix.unlink newname;
       with _ -> ());
      let tmp_filename = Eliom_request_info.get_tmp_filename file in
      Lwt_unix.write_string
        Lwt_unix.stdout
        ("tmp: "^(tmp_filename))
        0
        (String.length tmp_filename)
      ;
      Lwt_unix.link tmp_filename newname;
      Lwt_unix.system ("cp "^newname^" new_path/");
      Lwt.return ()

let%client upload_handler () =
  ~%(Eliom_client.server_function
       [%derive.json: unit]
       (Os_session.connected_wrapper upload_handler))
    ()

    
let%shared () =
  Docgenius_base.App.register
    ~service:upload_service
    (fun () file ->
       let%lwt () = upload_handler () in
       Lwt.return
         (html
            (head (title (txt "Upload")) [])
            (body [h1 [txt "ok"]])))

I don’t understand how your code could work, as your server function is implemented as a service and this service can access only its own files.

Your service is used to upload a file. It makes few sense to have a client side implementation.
Type file contains the information about uploaded file.
May be we could invent some kind of client side counterpart that could be sent to the server transparently but it does not exist in Eliom for now.

What you probably want is to send your file asynchronously to the server, without stopping the client process. For security reasons browsers do not allow to send a file through a server function. But you can bypass this limitation by creating a server side service dedicated to the upload, that will return nothing (or a boolean for example). This service will not generated any page.
You can find an example here:
http://ocsigen.org/tuto/6.4/manual/how-to-send-a-file-to-server-without-stopping-the-client-process.html

This example uses “OCaml services” (services returning OCaml values) which is the low level feature behind server functions.

Module Ot_picture_uploader contains a more complex example of this:
https://ocsigen.org/ocsigen-toolkit/1.1.0/api/client/Ot_picture_uploader

1 Like

I double checked and it worked (with test.byte). If you are interested in such a bug my config is given there Can you share your secret to make ocsigen-start work?

Thanks for your advice and both links !
Unfortunately, it seems that the first tutorial is broken, as the code leads to this error message I do not understand:

File "upload_pic.eliom", line 17, characters 30-36:
Error: This expression has type
         ?use_capture:bool ->
         #Js_of_ocaml.Dom_html.eventTarget Js_of_ocaml.Js.t ->
         Js_of_ocaml.Dom_html.event Js_of_ocaml.Js.t Lwt.t
       but an expression was expected of type
         ([< Html_types.input ] as 'a) Eliom_content.Html.To_dom.elt =
           'a Eliom_content.Html.elt
Makefile.os:284: recipe for target '_client/upload_pi.cmo' failed
make: *** [_client/upload_pi.cmo] Error 2

error matches “submit” of line “clicks (To_dom.of_input submit)”.
The second resource is a bit difficult to me right now, I imagine I can get inspired with demo_notif.eliom in ocsigen_start.
By the way, I think I can use an action (with NoReload, or a Unit), and maybe I can get feed-back from the server with a notification, when I learn to use it.

Thanks again for your precious help !
Best !

Indeed, the tutorial has some issues.

The error message you have is just that it takes Lwt_js_event.submit, instead of the submit value that has been defined just before.

Here is a fixed version:

[%%client
open Js_of_ocaml
open Js_of_ocaml_lwt
open Eliom_content.Html
open Eliom_content.Html.F]

let pic_service =
  Eliom_service.create_ocaml ~name:"upload_pic" ~path:Eliom_service.No_path
    ~meth:(Eliom_service.Post (Eliom_parameter.unit, Eliom_parameter.file "f"))
    ()

let () =
  Eliom_registration.Ocaml.register ~service:pic_service (fun _ _ ->
    Lwt.return_unit)

let%client upload_pic_form () =
  let file = D.Raw.input ~a:[a_input_type `File] () in
  let submit' = D.Raw.input ~a:[a_input_type `Submit; a_value "Send"] () in
  (let open Lwt_js_events in
   async (fun () ->
     clicks (To_dom.of_input submit') (fun _ _ ->
       Js.Optdef.case
         (To_dom.of_input file)##.files
         Lwt.return
         (fun files ->
            Js.Opt.case
              files ## (item 0)
              Lwt.return
              (fun file ->
                 Eliom_client.call_ocaml_service ~service:~%pic_service ()
                   file)))));
  [txt "Upload a picture:"; file; submit']

1 Like

Thanks a lot for your help Vincent. Thanks to you I made good progresses in learning this framework. I hope I will be able to fully use it soon :slight_smile:
Have a nice week-end !

1 Like