package caldav

  1. Overview
  2. Docs

Source file webdav_fs.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
type file = [ `File of string list ]

type dir = [ `Dir of string list ]

type file_or_dir = [ file | dir ]

module type S =

sig

  val (>>==) : ('a, 'b) result Lwt.t -> ('a -> ('c, 'b) result Lwt.t) -> ('c, 'b) result Lwt.t

  type t

  type error

  type write_error

  val basename : file_or_dir -> string

  val create_file : dir -> string -> file

  val dir_from_string : string -> dir

  val file_from_string : string -> file

  val from_string : t -> string -> (file_or_dir, error) result Lwt.t

  val to_string : file_or_dir -> string

  val parent : file_or_dir -> dir

  val get_property_map : t -> file_or_dir -> Properties.t Lwt.t

  val write_property_map : t -> file_or_dir -> Properties.t ->
    (unit, write_error) result Lwt.t

  val size : t -> file -> (int64, error) result Lwt.t

  val read : t -> file -> (string * Properties.t, error) result Lwt.t

  val exists : t -> string -> bool Lwt.t

  val dir_exists : t -> dir -> bool Lwt.t

  val listdir : t -> dir -> (file_or_dir list, error) result Lwt.t

  val mkdir : t -> dir -> Properties.t -> (unit, write_error) result Lwt.t

  val write : t -> file -> string -> Properties.t -> (unit, write_error) result Lwt.t

  val destroy : t -> file_or_dir -> (unit, write_error) result Lwt.t

  val pp_error : error Fmt.t

  val pp_write_error : write_error Fmt.t

  val valid : t -> Webdav_config.config -> (unit, [> `Msg of string ]) result Lwt.t

  val last_modified : t -> file_or_dir -> (Ptime.t, error) result Lwt.t

  val etag : t -> file_or_dir -> (string, error) result Lwt.t

  val batch: t -> (t -> 'a Lwt.t) -> ('a, [> `Msg of string ]) result Lwt.t
end

let src = Logs.Src.create "webdav.fs" ~doc:"webdav fs logs"
module Log = (val Logs.src_log src : Logs.LOG)

let propfile_ext = ".prop"

module type KV_RW = sig
  include Mirage_kv.RW
  val batch : t -> (t -> 'a Lwt.t) -> ('a, [> `Msg of string ]) result Lwt.t
end

module Make (Pclock : Mirage_clock.PCLOCK) (Fs:KV_RW) = struct

  open Lwt.Infix

  module Xml = Webdav_xml

  type t = Fs.t
  type error = Fs.error
  type write_error = Fs.write_error
  let pp_error = Fs.pp_error
  let pp_write_error = Fs.pp_write_error

  let (>>==) a f = a >>= function
    | Error e -> Lwt.return (Error e)
    | Ok res  -> f res

  let (>>|=) a f = a >|= function
    | Error e -> Error e
    | Ok res  -> f res

  let basename = function
    | `File path | `Dir path ->
      match List.rev path with
      | base::_ -> base
      | [] -> invalid_arg "basename of root directory not allowed"

  let create_file (`Dir data) name =
    `File (data @ [name])

  (* TODO: no handling of .. done here yet *)
  let data_to_list str =
    String.split_on_char '/' str |>
    List.filter_map (function "" -> None | x -> Some x)
  let data str = Mirage_kv.Key.v str

  let dir_from_string str = `Dir (data_to_list str)

  let file_from_string str = `File (data_to_list str)

  let to_string =
    let a = String.concat "/" in
    function
    | `File data -> "/" ^ a data
    | `Dir data -> "/" ^ a data ^ "/"

  let isdir fs name =
    (* TODO `File is wrong here, we're here to figure out whether it is a file or directory *)
    let key = data @@ to_string (`File name) in
    Fs.exists fs key >|= function
    | Ok None -> Error (`Not_found key)
    | Ok (Some `Value) -> Ok false
    | Ok (Some `Dictionary) -> Ok true
    | Error e -> Error e

  let from_string fs str =
    let key = data_to_list str in
    isdir fs key >>|= fun dir ->
    Ok (if dir then `Dir key else `File key)

  let parent f_or_d =
    let parent p =
      match List.rev p with
      | _ :: tl -> `Dir (List.rev tl)
      | [] -> `Dir []
    in
    match f_or_d with
    | `Dir d -> parent d
    | `File f -> parent f

  let propfilename f_or_d =
    let segments = match f_or_d with
    | `Dir data -> data @ [ propfile_ext ]
    | `File data -> match List.rev data with
      | filename :: path -> List.rev path @ [ filename ^ propfile_ext ]
      | [] -> assert false (* no file without a name *) in
    Mirage_kv.Key.v (String.concat "/" segments)

  let get_properties fs f_or_d =
    let propfile = propfilename f_or_d in
    Fs.get fs propfile

  (* TODO: check call sites, used to do:
      else match Xml.get_prop "resourcetype" map with
        | Some (_, c) when List.exists (function `Node (_, "collection", _) -> true | _ -> false) c -> name ^ "/"
        | _ -> name in
  *)
  let write_property_map fs f_or_d map =
    let map' = Properties.unsafe_remove (Xml.dav_ns, "getetag") map in
    let data = Sexplib.Sexp.to_string_hum (Properties.to_sexp map') in
    let filename = propfilename f_or_d in
    Fs.set fs filename data

  let size fs (`File file) =
    let key = data @@ to_string (`File file) in
    Fs.get fs key >|= function
    | Error e -> Error e
    | Ok data -> Ok (Int64.of_int @@ String.length data)

  let exists fs str =
    let file = data str in
    Fs.exists fs file >|= function
    | Error _e -> (* Error e *) false
    | Ok None -> false
    | Ok (Some _) -> true
    (*Fs.mem fs file*)

  let dir_exists fs (`Dir dir) =
    let key = data @@ to_string (`Dir dir) in
    Fs.exists fs key >|= function
    | Error _e -> (* Error e *) false
    | Ok None -> false
    | Ok (Some `Value) -> false
    | Ok (Some `Dictionary) -> true

  let listdir fs (`Dir dir) =
    let kv_dir = data @@ to_string (`Dir dir) in
    Fs.list fs kv_dir >|= function
    | Error e -> Error e
    | Ok files ->
      let files = List.fold_left (fun acc (file, kind) ->
          let is_propfile =
            let step = Mirage_kv.Key.basename file in
            let slen = String.length step
            and plen = String.length propfile_ext
            in
            slen >= plen && String.(equal (sub step (slen - plen) plen) propfile_ext)
          in
          if is_propfile then
            acc
          else
            let file = Mirage_kv.Key.segments file in
            match kind with
            | `Value -> `File file :: acc
            | `Dictionary -> `Dir file :: acc)
          [] files in
      Ok files

  let get_raw_property_map fs f_or_d =
    get_properties fs f_or_d >|= function
    | Error e ->
      Log.err (fun m -> m "error while getting properties for %s %a" (to_string f_or_d) pp_error e) ;
      None
    | Ok str ->
      Some (Properties.of_sexp (Ptime.v (Pclock.now_d_ps ())) (Sexplib.Sexp.of_string str))

  let etag fs f_or_d =
    let key = data @@ to_string f_or_d in
    Fs.digest fs key >|= function
    | Error e -> Error e
    | Ok d -> Ok (Ohex.encode d)

  (* careful: unsafe_find, unsafe_add *)
  let get_property_map fs f_or_d =
    get_raw_property_map fs f_or_d >>= function
    | None -> Lwt.return Properties.empty
    | Some map ->
      (* insert etag (from Fs.digest) into the propertymap *)
      etag fs f_or_d >|= function
      | Error e ->
        Log.err (fun m -> m "error %a while computing etag for %s"
                    Fs.pp_error e (to_string f_or_d));
        map
      | Ok etag ->
        let etag = ([], [ Xml.Pcdata etag ]) in
        Properties.unsafe_add (Xml.dav_ns, "getetag") etag map

  let last_modified fs f_or_d =
    get_property_map fs f_or_d >|= fun map ->
    let ts =
      match Properties.unsafe_find (Xml.dav_ns, "getlastmodified") map with
      | Some (_, [ Xml.Pcdata ts ]) ->
        begin match Ptime.of_rfc3339 ts with
          | Ok (ts, _, _) -> ts
          | Error (`RFC3339 (_, err)) ->
            Log.err (fun m -> m "error %a parsing %s as RFC3339 time, using current time"
                        Ptime.pp_rfc3339_error err ts);
            Ptime.v (Pclock.now_d_ps ())
        end
      | _ ->
        Log.err (fun m -> m "error while retrieving getlastmodified, not present or wrong XML data, using current time");
        Ptime.v (Pclock.now_d_ps ())
    in
    Ok ts

  let read fs (`File file) =
    let kv_file = data @@ to_string (`File file) in
    Fs.get fs kv_file >>= function
    | Error e -> Lwt.return (Error e)
    | Ok data ->
      get_property_map fs (`File file) >|= fun props ->
      Ok (data, props)

  let mkdir fs (`Dir dir) propmap =
    write_property_map fs (`Dir dir) propmap

  let write fs (`File file) value propmap =
    let kv_file = data @@ to_string (`File file) in
    Fs.set fs kv_file value >>= function
    | Error e -> Lwt.return (Error e)
    | Ok () -> write_property_map fs (`File file) propmap

  let destroy_file_or_empty_dir fs f_or_d =
    let propfile = propfilename f_or_d in
    Fs.remove fs propfile >>= function
    | Error e -> Lwt.return (Error e)
    | Ok () ->
      let file = data @@ to_string f_or_d in
      Fs.remove fs file

  let destroy fs f_or_d =
    destroy_file_or_empty_dir fs f_or_d

  (* TODO check the following invariants:
      - every resource has a .prop.xml file
      - there are no references to non-existing principals (e.g. in <acl><ace>)
      - all principals (apart from groups) have a password and salt (of type Pcdata)
      - all local URLs use the correct hostname *)
  let valid fs config =
    get_property_map fs (`Dir [config.Webdav_config.principals ; "root"]) >|= fun root_map ->
    match
      Properties.unsafe_find (Xml.robur_ns, "password") root_map,
      Properties.unsafe_find (Xml.robur_ns, "salt") root_map
    with
    | Some _, Some _ -> Ok ()
    | _ -> Error (`Msg "root user does not have password and salt")

  let batch = Fs.batch
end