Warning: Reason support is experimental. We are looking for beta-tester and contributors.

Saving favorite pictures

We will now add a button to the Graffiti application to save the current image. The images will be saved to the filesystem using the module Lwt_io. We will then make an Atom feed with the saved images using Syndic.

To install it, do:

opam install syndic

We save the images in the directory containing the static contents under the directory images_saved/username. The username directory is created if needed. If it already exists mkdir fails and we do nothing.

We will add this code in a new file:

feed.ml

open Eliom_content
open Lwt.Infix
open Html.F
open Server

let static_dir =
  match Eliom_config.get_config () with
  | [Element ("staticdir", [], [PCData dir])] ->
    dir
  | [] ->
    raise (Ocsigen_extensions.Error_in_config_file
             ("<staticdir> option required for <graffiti>"))
  | _ ->
    raise (Ocsigen_extensions.Error_in_config_file
             ("Unexpected content inside graffiti config"))

let create_dir dir =
  try%lwt Lwt_unix.mkdir dir 0o777 with
  | Unix.Unix_error (Unix.EEXIST, "mkdir", _) -> Lwt.return_unit
  | _ ->
      Eliom_lib.debug "could not create the directory %s" dir;
      Lwt.return_unit

let image_dir name =
  let dir = static_dir ^ "/graffiti_saved/" in
  let%lwt () = create_dir dir in
  let dir = dir ^ Eliom_lib.Url.encode name in
  let%lwt () = create_dir dir in
  Lwt.return dir

let make_filename name number =
  image_dir name >|= ( fun dir -> (dir ^ "/" ^ (string_of_int number) ^ ".png") )

let save image name number =
  let%lwt file_name = make_filename name number in
  let%lwt out_chan = Lwt_io.open_file ~mode:Lwt_io.output file_name in
  Lwt_io.write out_chan image

We number images and associate to each image the time of creation. It is stocked in an Ocsipersist table.

let image_info_table = Ocsipersist.Polymorphic.open_table "image_info_table"

For each user, we stock a value of type
int * CalendarLib.Calendar.t * ((int * CalendarLib.Calendar.t) list). The first integer is the name under which will be saved the image, the first time is the last update for that user and the list contains the names and times of old images. We need those times to timestamp the entries of the feed.

let save_image username =
  let now = CalendarLib.Calendar.now () in
  let%lwt image_info_table = image_info_table in
  let%lwt number,_,list =
    try%lwt Ocsipersist.Polymorphic.find image_info_table username with
    | Not_found -> Lwt.return (0,now,[])
    | e -> Lwt.fail e
  in
  let%lwt () = Ocsipersist.Polymorphic.add image_info_table
      username (number+1,now,(number,now)::list) in
  let (_,image_string) = Hashtbl.find graffiti_info username in
  save (image_string ()) username number

let save_image_service =
  Eliom_service.create
    ~meth:(Eliom_service.Post
             (Eliom_parameter.unit, Eliom_parameter.string "name"))
    ~path:Eliom_service.No_path ()

let () =
  Eliom_registration.Action.register
    ~service:save_image_service (fun () name -> save_image name)

let save_image_box name =
  Lwt.return
    (Html.D.Form.post_form ~service:save_image_service
       (fun param_name ->
         [p [Html.D.Form.input ~input_type:`Hidden ~name:param_name
              ~value:name Html.D.Form.string;
             Html.D.Form.button_no_value ~button_type:`Submit [txt "save"]]])
       ())

We find the url of the images with Eliom_service.static_dir. It is a service taking file path as parameter, serving the content of the static directory. We use Eliom_uri.make_string_uri to get the url as a string.

let feed_service =
  Eliom_service.create
    ~path:(Eliom_service.Path ["feed"])
    ~meth:(Eliom_service.Get (Eliom_parameter.string "name"))
    ()

let local_filename name number =
  ["graffiti_saved"; Eliom_lib.Url.encode name ; (string_of_int number) ^ ".png"]

let rec entries name list = function
  | 0 -> []
  | len ->
    match list with
    | [] -> []
    | (n,saved)::q ->
      let uri =
        Html.D.make_uri ~absolute:true
          ~service:(Eliom_service.static_dir ())
          (local_filename name n)
        |> Xml.string_of_uri
        |> Uri.of_string in
      let content = Syndic.Atom.Src (None, uri) in
      let authors = (Syndic.Atom.author name), [] in
      let title : Syndic.Atom.text_construct =
        Syndic.Atom.Text ("graffiti " ^ name ^ " " ^ (string_of_int n)) in
      let entry =
        Syndic.Atom.entry ~content ~id:uri ~authors ~title ~updated:saved () in
      entry::(entries name q (len - 1))

let feed_of_string_page xml =
  xml
  |> Syndic.Atom.to_xml
  |> Syndic.XML.to_string
  |> fun string -> string, ""

let feed name () =
  let id =
    Xml.string_of_uri
      (Html.D.make_uri ~absolute:true ~service:feed_service name)
    |> Uri.of_string in
  let title : Syndic.Atom.text_construct =
    Syndic.Atom.Text ("nice drawings of " ^ name) in
  Lwt.catch
    (fun () ->
       let%lwt image_info_table = image_info_table in
       Ocsipersist.Polymorphic.find image_info_table name >|=
      (fun (number,updated,list) ->
         Syndic.Atom.feed ~id ~updated ~title (entries name list 10)
       |> feed_of_string_page))
    ( function Not_found ->
      let now = Option.get (Ptime.of_float_s (Unix.gettimeofday ())) in
      Lwt.return (Syndic.Atom.feed ~id ~updated:now ~title []
                  |> feed_of_string_page)
             | e -> Lwt.fail e )

let () = Eliom_registration.String.register ~service:feed_service feed

And then use the new module as follow:

[ a Feed.feed_service [txt "atom feed"] name;
    div (if name = username
      then [Feed.save_image_box name]
      else [txt "no saving"]);
  ]