Skip to content

Commit 91ff69d

Browse files
authored
[Files.save_as]: just write directly to special files (#60)
1 parent f77980a commit 91ff69d

4 files changed

Lines changed: 92 additions & 7 deletions

File tree

dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868

6969
(executable
7070
(name test)
71-
(libraries lwt lwt.unix devkit extlib extunix libevent ocamlnet_lite ounit2 unix yojson)
71+
(libraries lwt lwt.unix devkit extlib extunix libevent ocamlnet_lite ounit2 threads unix yojson)
7272
(modules test test_httpev))
7373

7474
; uses 8GB+ RAM, so do not run as part of test suite

files.ml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ let mkdir_p ?(perm=0o755) path =
6767
in
6868
aux path
6969

70-
let save_as name ?(mode=0o644) f =
70+
let save_as_regular name ?(mode=0o644) f =
7171
(* not using make_temp_file cause same dir is needed for atomic rename *)
7272
let temp = Printf.sprintf "%s.save.%d.tmp" name (U.gettid ()) in
7373
bracket (Unix.openfile temp [Unix.O_WRONLY;Unix.O_CREAT] mode) Unix.close begin fun fd ->
@@ -81,3 +81,9 @@ let save_as name ?(mode=0o644) f =
8181
with
8282
exn -> Exn.suppress Unix.unlink temp; raise exn
8383
end
84+
85+
let rec save_as name ?mode f =
86+
match (Unix.lstat name).st_kind with
87+
| Unix.S_LNK -> save_as (Unix.realpath name) ?mode f
88+
| Unix.S_REG | (exception Unix.Unix_error (Unix.ENOENT, _, _)) -> save_as_regular name ?mode f
89+
| _ -> Out_channel.with_open_gen [ Open_wronly ] 0 name f

files.mli

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@ val open_out_append_text : string -> out_channel
2222
val mkdir_p : ?perm:Unix.file_perm -> string -> unit
2323

2424
(** [save_as filename ?mode f] is similar to
25-
[Control.with_open_file_bin] except that writing is done to a
26-
temporary file that will be renamed to [filename] after [f] has
27-
succesfully terminated. Therefore this guarantee that either
28-
[filename] will not be modified or will contain whatever [f] was
29-
writing to it as a side-effect.
25+
[Control.with_open_file_bin] for regular files, except that
26+
writing is done to a temporary file that will be renamed to
27+
[filename] after [f] has succesfully terminated. Therefore this
28+
guarantee that either [filename] will not be modified or will
29+
contain whatever [f] was writing to it as a side-effect.
30+
31+
There is no such special treatment for special files (Unix.stat
32+
kind not S_REG, e.g. devices, pipes, etc), instead they are
33+
written to directly. Symlinks are followed (not overwritten in
34+
place). Throws {!Unix.Unix_error} on broken symlinks.
3035
3136
FIXME windows *)
3237
val save_as : string -> ?mode:Unix.file_perm -> (out_channel -> unit) -> unit

test.ml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,80 @@ let () =
595595
assert_equal !accumulator 4;
596596
()
597597

598+
let with_temp_path name f =
599+
let path = Filename.concat (Filename.get_temp_dir_name ()) name in
600+
(try Sys.remove path with _ -> ());
601+
Fun.protect ~finally:(fun () -> try Sys.remove path with _ -> ()) (fun () -> f path)
602+
603+
let () = test "Files.save_as writes to new regular file" @@ fun () ->
604+
with_temp_path "test_save_as_new.txt" @@ fun path ->
605+
Files.save_as path (fun oc -> output_string oc "hello\n");
606+
let content = In_channel.with_open_text path In_channel.input_all in
607+
assert_equal ~printer:id "hello\n" content
608+
609+
let () = test "Files.save_as overwrites existing regular file" @@ fun () ->
610+
with_temp_path "test_save_as_overwrite.txt" @@ fun path ->
611+
Out_channel.with_open_text path (fun oc -> output_string oc "old\n");
612+
Files.save_as path (fun oc -> output_string oc "new\n");
613+
let content = In_channel.with_open_text path In_channel.input_all in
614+
assert_equal ~printer:id "new\n" content
615+
616+
let () = test "Files.save_as no temp file left on success" @@ fun () ->
617+
with_temp_path "test_save_as_no_temp.txt" @@ fun path ->
618+
Files.save_as path (fun oc -> output_string oc "data\n");
619+
let temp = Printf.sprintf "%s.save.%d.tmp" path (U.gettid ()) in
620+
assert_bool "temp file should not exist" (not (Sys.file_exists temp))
621+
622+
let () = test "Files.save_as no temp file left on failure" @@ fun () ->
623+
with_temp_path "test_save_as_fail.txt" @@ fun path ->
624+
(try Files.save_as path (fun _oc -> failwith "boom") with Failure _ -> ());
625+
let temp = Printf.sprintf "%s.save.%d.tmp" path (U.gettid ()) in
626+
assert_bool "temp file should not exist" (not (Sys.file_exists temp));
627+
assert_bool "target file should not exist" (not (Sys.file_exists path))
628+
629+
let () = test "Files.save_as writes to /dev/null without error" @@ fun () ->
630+
Files.save_as "/dev/null" (fun oc -> output_string oc "discarded\n");
631+
let temp = Printf.sprintf "/dev/null.save.%d.tmp" (U.gettid ()) in
632+
assert_bool "temp file should not exist" (not (Sys.file_exists temp));
633+
assert_bool "/dev/null should be a char device" ((Unix.stat "/dev/null").st_kind = Unix.S_CHR)
634+
635+
let () = test "Files.save_as writes to named pipe (FIFO)" @@ fun () ->
636+
with_temp_path "test_save_as_fifo" @@ fun fifo_path ->
637+
Unix.mkfifo fifo_path 0o644;
638+
let received = ref "" in
639+
let reader = Thread.create (fun () -> received := In_channel.with_open_text fifo_path In_channel.input_all) () in
640+
Files.save_as fifo_path (fun oc -> output_string oc "fifo data\n");
641+
Thread.join reader;
642+
assert_equal ~printer:id "fifo data\n" !received
643+
644+
let () = test "Files.save_as no temp file created for FIFO" @@ fun () ->
645+
with_temp_path "test_save_as_fifo2" @@ fun fifo_path ->
646+
Unix.mkfifo fifo_path 0o644;
647+
let reader = Thread.create (fun () -> ignore (In_channel.with_open_text fifo_path In_channel.input_all)) () in
648+
Files.save_as fifo_path (fun oc -> output_string oc "data\n");
649+
Thread.join reader;
650+
let temp = Printf.sprintf "%s.save.%d.tmp" fifo_path (U.gettid ()) in
651+
assert_bool "temp file should not exist" (not (Sys.file_exists temp))
652+
653+
let () = test "Files.save_as writes through symlink without clobbering it" @@ fun () ->
654+
with_temp_path "test_save_as_symlink_target.txt" @@ fun target ->
655+
with_temp_path "test_save_as_symlink_link.txt" @@ fun link ->
656+
Out_channel.with_open_text target (fun oc -> output_string oc "old\n");
657+
Unix.symlink target link;
658+
Files.save_as link (fun oc -> output_string oc "new\n");
659+
assert_bool "symlink should still be a symlink" ((Unix.lstat link).st_kind = Unix.S_LNK);
660+
let content = In_channel.with_open_text target In_channel.input_all in
661+
assert_equal ~printer:id "new\n" content
662+
663+
let () = test "Files.save_as fails on broken symlink" @@ fun () ->
664+
with_temp_path "test_save_as_symlink_target.txt" @@ fun target ->
665+
with_temp_path "test_save_as_symlink_link.txt" @@ fun link ->
666+
Unix.symlink target link;
667+
try
668+
Files.save_as link (fun oc -> output_string oc "new\n");
669+
assert_failure "should fail on broken symlink"
670+
with Unix.Unix_error(Unix.ENOENT, "realpath", _) -> ()
671+
598672
let () = test "Logfmt" begin fun () ->
599673
let eq name expected got =
600674
assert_equal ~msg:name ~printer:(fun s -> sprintf "%S" s) expected got

0 commit comments

Comments
 (0)