Skip to content

Commit 166118b

Browse files
authored
Merge pull request #625 from talex5/mkdirs
Add Path.mkdirs and Path.split
2 parents 082bf00 + fa5bc53 commit 166118b

File tree

5 files changed

+203
-25
lines changed

5 files changed

+203
-25
lines changed

lib_eio/path.ml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,38 @@ let native_exn t =
2121
| Some p -> p
2222
| None -> raise (Fs.err (Not_native (Fmt.str "%a" pp t)))
2323

24+
(* Drop the first [n] characters from [s]. *)
25+
let string_drop s n =
26+
String.sub s n (String.length s - n)
27+
28+
(* "/foo/bar//" -> "/foo/bar"
29+
"///" -> "/"
30+
"foo/bar" -> "foo/bar"
31+
*)
32+
let remove_trailing_slashes s =
33+
let rec aux i =
34+
if i <= 1 || s.[i - 1] <> '/' then (
35+
if i = String.length s then s
36+
else String.sub s 0 i
37+
) else aux (i - 1)
38+
in
39+
aux (String.length s)
40+
41+
let split (dir, p) =
42+
match remove_trailing_slashes p with
43+
| "" -> None
44+
| "/" -> None
45+
| p ->
46+
match String.rindex_opt p '/' with
47+
| None -> Some ((dir, ""), p)
48+
| Some idx ->
49+
let basename = string_drop p (idx + 1) in
50+
let dirname =
51+
if idx = 0 then "/"
52+
else remove_trailing_slashes (String.sub p 0 idx)
53+
in
54+
Some ((dir, dirname), basename)
55+
2456
let open_in ~sw t =
2557
let (Resource.T (dir, ops), path) = t in
2658
let module X = (val (Resource.get ops Fs.Pi.Dir)) in
@@ -139,3 +171,16 @@ let rename t1 t2 =
139171
with Exn.Io _ as ex ->
140172
let bt = Printexc.get_raw_backtrace () in
141173
Exn.reraise_with_context ex bt "renaming %a to %a" pp t1 pp t2
174+
175+
let rec mkdirs ?(exists_ok=false) ~perm t =
176+
(* Check parent exists first. *)
177+
split t |> Option.iter (fun (parent, _) ->
178+
match is_directory parent with
179+
| true -> ()
180+
| false -> mkdirs ~perm ~exists_ok:true parent
181+
| exception (Exn.Io _ as ex) ->
182+
let bt = Printexc.get_raw_backtrace () in
183+
Exn.reraise_with_context ex bt "creating directory %a" pp t
184+
);
185+
try mkdir ~perm t
186+
with Exn.Io (Fs.E Already_exists _, _) when exists_ok && is_directory t -> ()

lib_eio/path.mli

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ val native : _ t -> string option
6161
val native_exn : _ t -> string
6262
(** Like {!native}, but raise a suitable exception if the path is not a native path. *)
6363

64+
val split : 'a t -> ('a t * string) option
65+
(** [split t] returns [Some (dir, basename)], where [basename] is the last path component in [t]
66+
and [dir] is [t] without [basename].
67+
68+
[dir / basename] refers to the same path as [t].
69+
70+
[split t = None] if there is nothing to split.
71+
72+
For example:
73+
74+
- [split (root, "foo/bar") = Some ((root, "foo"), "bar")]
75+
- [split (root, "/foo/bar") = Some ((root, "/foo"), "bar")]
76+
- [split (root, "/foo/bar/baz") = Some ((root, "/foo/bar"), "baz")]
77+
- [split (root, "/foo/bar//baz/") = Some ((root, "/foo/bar"), "baz")]
78+
- [split (root, "bar") = Some ((root, ""), "bar")]
79+
- [split (root, ".") = Some ((root, ""), ".")]
80+
- [split (root, "") = None]
81+
- [split (root, "/") = None]
82+
*)
83+
6484
(** {1 Reading files} *)
6585

6686
val load : _ t -> string
@@ -112,6 +132,13 @@ val with_open_out :
112132
val mkdir : perm:File.Unix_perm.t -> _ t -> unit
113133
(** [mkdir ~perm t] creates a new directory [t] with permissions [perm]. *)
114134

135+
val mkdirs : ?exists_ok:bool -> perm:File.Unix_perm.t -> _ t -> unit
136+
(** [mkdirs ~perm t] creates directory [t] along with any missing ancestor directories, recursively.
137+
138+
All created directories get permissions [perm], but existing directories do not have their permissions changed.
139+
140+
@param exist_ok If [false] (the default) then we raise {! Fs.Already_exists} if [t] is already a directory. *)
141+
115142
val open_dir : sw:Switch.t -> _ t -> [`Close | dir_ty] t
116143
(** [open_dir ~sw t] opens [t].
117144

lib_eio_posix/fs.ml

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,14 @@ end = struct
7272
if t.sandbox then (
7373
if t.closed then Fmt.invalid_arg "Attempt to use closed directory %S" t.dir_path;
7474
let dir, leaf = Filename.dirname path, Filename.basename path in
75-
if leaf = ".." then (
76-
(* We could be smarter here and normalise the path first, but '..'
77-
doesn't make sense for any of the current uses of [with_parent_dir]
78-
anyway. *)
79-
raise (Eio.Fs.err (Permission_denied (Err.Invalid_leaf leaf)))
80-
) else (
81-
let dir = resolve t dir in
82-
Switch.run @@ fun sw ->
83-
let dirfd = Low_level.openat ~sw ~mode:0 dir Low_level.Open_flags.(directory + rdonly + nofollow) in
84-
fn (Some dirfd) leaf
85-
)
75+
let dir, leaf =
76+
if leaf = ".." then path, "."
77+
else dir, leaf
78+
in
79+
let dir = resolve t dir in
80+
Switch.run @@ fun sw ->
81+
let dirfd = Low_level.openat ~sw ~mode:0 dir Low_level.Open_flags.(directory + rdonly + nofollow) in
82+
fn (Some dirfd) leaf
8683
) else fn None path
8784

8885
let v ~label ~sandbox dir_path = { dir_path; sandbox; label; closed = false }

lib_eio_windows/test/test_fs.ml

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ let try_mkdir path =
2323
| () -> traceln "mkdir %a -> ok" Path.pp path
2424
| exception ex -> raise ex
2525

26+
let try_mkdirs ?exists_ok path =
27+
match Path.mkdirs ?exists_ok path ~perm:0o700 with
28+
| () -> traceln "mkdirs %a -> ok" Path.pp path
29+
| exception ex -> traceln "@[<h>%a@]" Eio.Exn.pp ex
30+
2631
let try_rename p1 p2 =
2732
match Path.rename p1 p2 with
2833
| () -> traceln "rename %a to %a -> ok" Path.pp p1 Path.pp p2
@@ -75,7 +80,7 @@ let test_exclusive env () =
7580
Eio.traceln "fiest";
7681
Path.save ~create:(`Exclusive 0o666) path "first-write";
7782
Eio.traceln "next";
78-
try
83+
try
7984
Path.save ~create:(`Exclusive 0o666) path "first-write";
8085
Eio.traceln "nope";
8186
failwith "Should have failed"
@@ -84,15 +89,15 @@ let test_exclusive env () =
8489
let test_if_missing env () =
8590
let cwd = Eio.Stdenv.cwd env in
8691
let test_file = (cwd / "test-file") in
87-
with_temp_file test_file @@ fun test_file ->
92+
with_temp_file test_file @@ fun test_file ->
8893
Path.save ~create:(`If_missing 0o666) test_file "1st-write-original";
8994
Path.save ~create:(`If_missing 0o666) test_file "2nd-write";
9095
Alcotest.(check string) "same contents" "2nd-write-original" (Path.load test_file)
9196

9297
let test_trunc env () =
9398
let cwd = Eio.Stdenv.cwd env in
9499
let test_file = (cwd / "test-file") in
95-
with_temp_file test_file @@ fun test_file ->
100+
with_temp_file test_file @@ fun test_file ->
96101
Path.save ~create:(`Or_truncate 0o666) test_file "1st-write-original";
97102
Path.save ~create:(`Or_truncate 0o666) test_file "2nd-write";
98103
Alcotest.(check string) "same contents" "2nd-write" (Path.load test_file)
@@ -125,20 +130,33 @@ let test_mkdir env () =
125130
Unix.rmdir "subdir\\nested";
126131
Unix.rmdir "subdir"
127132

133+
let test_mkdirs env () =
134+
let cwd = Eio.Stdenv.cwd env in
135+
let nested = cwd / "subdir1" / "subdir2" / "subdir3" in
136+
try_mkdirs nested;
137+
let one_more = Path.(nested / "subdir4") in
138+
(try
139+
try_mkdirs one_more
140+
with Eio.Io (Eio.Fs.E (Already_exists _), _) -> ());
141+
try_mkdirs ~exists_ok:true one_more;
142+
try
143+
try_mkdirs (cwd / ".." / "outside")
144+
with Eio.Io (Eio.Fs.E (Permission_denied _), _) -> ()
145+
128146
let test_symlink env () =
129-
(*
147+
(*
130148
Important note: assuming that neither "another" nor
131149
"to-subdir" exist, the following program will behave
132150
differently if you don't have the ~to_dir flag.
133151
134152
With [to_dir] set to [true] we get the desired UNIX behaviour,
135153
without it [Unix.realpath] will actually show the parent directory
136154
of "another". Presumably this is because Windows distinguishes
137-
between file symlinks and directory symlinks. Fun.
155+
between file symlinks and directory symlinks. Fun.
138156
139157
{[ Unix.symlink ~to_dir:true "another" "to-subdir";
140158
Unix.mkdir "another" 0o700;
141-
print_endline @@ Unix.realpath "to-subdir" |}
159+
print_endline @@ Unix.realpath "to-subdir" |}
142160
*)
143161
let cwd = Eio.Stdenv.cwd env in
144162
try_mkdir (cwd / "sandbox");
@@ -186,13 +204,13 @@ let test_unlink env () =
186204
try_unlink (cwd / "file");
187205
try_unlink (cwd / "subdir\\file2");
188206
let () =
189-
try
207+
try
190208
try_read_file (cwd / "file");
191209
failwith "file should not exist"
192210
with Eio.Io (Eio.Fs.E (Not_found _), _) -> ()
193211
in
194212
let () =
195-
try
213+
try
196214
try_read_file (cwd / "subdir\\file2");
197215
failwith "file should not exist"
198216
with Eio.Io (Eio.Fs.E (Not_found _), _) -> ()
@@ -201,7 +219,7 @@ let test_unlink env () =
201219
(* Supposed to use symlinks here. *)
202220
try_unlink (cwd / "subdir\\file2");
203221
let () =
204-
try
222+
try
205223
try_read_file (cwd / "subdir\\file2");
206224
failwith "file should not exist"
207225
with Eio.Io (Eio.Fs.E (Not_found _), _) -> ()
@@ -211,13 +229,13 @@ let test_unlink env () =
211229
let try_failing_unlink env () =
212230
let cwd = Eio.Stdenv.cwd env in
213231
let () =
214-
try
232+
try
215233
try_unlink (cwd / "missing");
216234
failwith "Expected not found!"
217235
with Eio.Io (Eio.Fs.E (Not_found _), _) -> ()
218236
in
219237
let () =
220-
try
238+
try
221239
try_unlink (cwd / "..\\foo");
222240
failwith "Expected permission denied!"
223241
with Eio.Io (Eio.Fs.E (Permission_denied _), _) -> ()
@@ -233,13 +251,13 @@ let test_remove_dir env () =
233251
try_rmdir (cwd / "d1");
234252
try_rmdir (cwd / "subdir\\d2");
235253
let () =
236-
try
254+
try
237255
try_read_dir (cwd / "d1");
238256
failwith "Expected not found"
239257
with Eio.Io (Eio.Fs.E (Not_found _), _) -> ()
240-
in
258+
in
241259
let () =
242-
try
260+
try
243261
try_read_dir (cwd / "subdir\\d2");
244262
failwith "Expected not found"
245263
with Eio.Io (Eio.Fs.E (Not_found _), _) -> ()

tests/fs.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ let try_mkdir path =
3636
| () -> traceln "mkdir %a -> ok" Path.pp path
3737
| exception ex -> traceln "@[<h>%a@]" Eio.Exn.pp ex
3838
39+
let try_mkdirs ?exists_ok path =
40+
match Path.mkdirs ?exists_ok path ~perm:0o700 with
41+
| () -> traceln "mkdirs %a -> ok" Path.pp path
42+
| exception ex -> traceln "@[<h>%a@]" Eio.Exn.pp ex
43+
3944
let try_rename p1 p2 =
4045
match Path.rename p1 p2 with
4146
| () -> traceln "rename %a to %a -> ok" Path.pp p1 Path.pp p2
@@ -208,6 +213,90 @@ Creating directories with nesting, symlinks, etc:
208213
- : unit = ()
209214
```
210215

216+
# Split
217+
218+
```ocaml
219+
let fake_dir : Eio.Fs.dir_ty r = Eio.Resource.T ((), Eio.Resource.handler [])
220+
let split path = Eio.Path.split (fake_dir, path) |> Option.map (fun ((_, dirname), basename) -> dirname, basename)
221+
```
222+
223+
```ocaml
224+
# split "foo/bar";
225+
- : (string * string) option = Some ("foo", "bar")
226+
227+
# split "/foo/bar";
228+
- : (string * string) option = Some ("/foo", "bar")
229+
230+
# split "/foo/bar/baz";
231+
- : (string * string) option = Some ("/foo/bar", "baz")
232+
233+
# split "/foo/bar//baz/";
234+
- : (string * string) option = Some ("/foo/bar", "baz")
235+
236+
# split "bar";
237+
- : (string * string) option = Some ("", "bar")
238+
239+
# split "/bar";
240+
- : (string * string) option = Some ("/", "bar")
241+
242+
# split ".";
243+
- : (string * string) option = Some ("", ".")
244+
245+
# split "./";
246+
- : (string * string) option = Some ("", ".")
247+
248+
# split "";
249+
- : (string * string) option = None
250+
251+
# split "/";
252+
- : (string * string) option = None
253+
254+
# split "///";
255+
- : (string * string) option = None
256+
```
257+
258+
# Mkdirs
259+
260+
Recursively creating directories with `mkdirs`.
261+
262+
```ocaml
263+
# run @@ fun env ->
264+
let cwd = Eio.Stdenv.cwd env in
265+
let nested = cwd / "subdir1" / "subdir2" / "subdir3" in
266+
try_mkdirs nested;
267+
assert (Eio.Path.is_directory nested);
268+
let one_more = Path.(nested / "subdir4") in
269+
try_mkdirs one_more;
270+
try_mkdirs ~exists_ok:true one_more;
271+
try_mkdirs one_more;
272+
assert (Eio.Path.is_directory one_more);
273+
try_mkdirs (cwd / ".." / "outside");
274+
+mkdirs <cwd:subdir1/subdir2/subdir3> -> ok
275+
+mkdirs <cwd:subdir1/subdir2/subdir3/subdir4> -> ok
276+
+mkdirs <cwd:subdir1/subdir2/subdir3/subdir4> -> ok
277+
+Eio.Io Fs Already_exists _, creating directory <cwd:subdir1/subdir2/subdir3/subdir4>
278+
+Eio.Io Fs Permission_denied _, examining <cwd:..>, creating directory <cwd:../outside>
279+
- : unit = ()
280+
```
281+
282+
Some edge cases for `mkdirs`.
283+
284+
```ocaml
285+
# run @@ fun env ->
286+
let cwd = Eio.Stdenv.cwd env in
287+
try_mkdirs (cwd / ".");
288+
try_mkdirs (cwd / "././");
289+
let lots_of_slashes = "./test//////////////test" in
290+
try_mkdirs (cwd / lots_of_slashes);
291+
assert (Eio.Path.is_directory (cwd / lots_of_slashes));
292+
try_mkdirs (cwd / "..");;
293+
+Eio.Io Fs Already_exists _, creating directory <cwd:.>
294+
+Eio.Io Fs Already_exists _, creating directory <cwd:././>
295+
+mkdirs <cwd:./test//////////////test> -> ok
296+
+Eio.Io Fs Permission_denied _, creating directory <cwd:..>
297+
- : unit = ()
298+
```
299+
211300
# Unlink
212301

213302
You can remove a file using unlink:
@@ -561,6 +650,7 @@ Fstatat:
561650
try_stat (cwd / "broken-symlink");
562651
try_stat cwd;
563652
try_stat (cwd / "..");
653+
try_stat (cwd / "stat_subdir2/..");
564654
Unix.symlink ".." "parent-symlink";
565655
try_stat (cwd / "parent-symlink");
566656
try_stat (cwd / "missing1" / "missing2");
@@ -570,6 +660,7 @@ Fstatat:
570660
+<cwd:broken-symlink> -> symbolic link / Fs Not_found _
571661
+<cwd> -> directory
572662
+<cwd:..> -> Fs Permission_denied _
663+
+<cwd:stat_subdir2/..> -> directory
573664
+<cwd:parent-symlink> -> symbolic link / Fs Permission_denied _
574665
+<cwd:missing1/missing2> -> Fs Not_found _
575666
- : unit = ()

0 commit comments

Comments
 (0)