123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813(** Make an abstraction over a file. The file does not need to be on disk; it
can be a remote file accessible through a URL, or in-memory.
{3 Usage}
{[
module BasicIo = struct
module P = MlFront_Thunk.Promises.PromiseMinimal
include MlFront_Thunk.ThunkIo.Make (P)
end
]}
and using it with ThunkCst:
{[
let module Parser = MlFront_Thunk.ThunkCst.Io (BasicIo.P) in
let parse_promise =
Parser.parse (module ResultObserver) (BasicIo.disk_file thunkfile)
in
let parse_result = BasicIo.P.run_promise parse_promise in
match parse_result with _ -> ()
]}
{3 Extending the abstraction}
You will want an extension if you do asynchronous file reading or read
"cloud" files from the Internet.
Here is a functional equivalent of reading from disk:
{[
module ExtendedIo = struct
module P = MlFront_Thunk.Promises.PromiseMinimal
(** You need some promise implementation. *)
include MlFront_Thunk.ThunkIo.Make (P)
(** You need a creator of file objects. Your implementation will use
[generic_file] to define all the messages that a file object must
response to. *)
let a_disk_file origin =
generic_file ~origin ~read_all:(fun () ->
let content =
In_channel.with_open_bin origin In_channel.input_all
in
P.return (`Content content))
end
]}
and using it with ThunkCst:
{[
let module Parser = MlFront_Thunk.ThunkCst.Io (ExtendedIo.P) in
let parse_promise =
Parser.parse (module ResultObserver) (ExtendedIo.disk_file thunkfile)
in
let parse_result = ExtendedIo.P.run_promise parse_promise in
match parse_result with _ -> ()
]} *)moduleMake(M:BuildConstraints.MONAD_PROMISE):sigtypefile_object=private{file_origin:string;(** The location of the file. It should be clickable if possible by
modern IDEs in a terminal. For example, a file should just be a path
to the file (no ["file://"] prefix), while a http URL should be a
URL. *)is_local_file:bool;(** [is_local_file] is [true] if and only if the native operating system
tools like {!In_channel} can manipulate the file. *)open_for_writing:unit->[`NodeofInt64.t|`IsDirectoryofdirectory_object|`Errorofstring]M.t;(** [open_for_writing path] opens the file at [path] and returns a
promise that resolves to a file node (ex. inode) if successful, or
an error message if not. *)open_for_reading:unit->[`NodeofInt64.t|`IsDirectoryofdirectory_object|`Errorofstring]M.t;(** [open_for_reading path] opens the file at [path] and returns a
promise that resolves to a file node (ex. inode) if successful, or
an error message if not. *)read_some:Int64.t->[`Bytesofbytes*int*int|`Eof|`Errorofstring]M.t;write_all:Int64.t->string->int->int->[`WroteBytes|`Errorofstring]M.t;close:Int64.t->unitM.t;(** [close node] closes the file opened with {!open_for_writing}. *)read_all:unit->[`Contentofstring|`Errorofstring|`ExceededSizeLimitofint64]M.t;(** [read_all source] gets the file, but may not give back the file if
it exceeds memory.
The [error] in [`Error error], if possible, should complete the
phrase ["Cannot read the file because ___."]. For example,
[`Error "the file was not found"]. *)prepare_as_copy_destination:unit->[`Ready|`Errorofstring]M.t;(** [prepare_as_copy_destination ()] prepares the file for being the
destination target of a copy. On Windows the file may be set to
read-write. *)delete_file:unit->[`Deleted|`Errorofstring]M.t;(** [delete_file node] deletes the file. *)checksum_file:strip_carriage_returns:bool->algo:[`Sha1|`Sha256]->unit->[`Checksumofstring*Int64.t|`Errorofstring]M.t;}anddirectory_object=private{dir_origin:string;(** The location of the directory. It should be clickable if possible by
modern IDEs in a terminal. For example, a file should just be a path
to the file (no ["file://"] prefix), while a http URL should be a
URL. *)create_directory:unit->[`Created|`Errorofstring]M.t;(** Create the directory and all parent directories if they do not
exist. *)delete_directory:unit->[`Deleted|`Errorofstring]M.t;(** Delete the directory and all of its content. *)zip_directory:?intermediate:unit->staging_dir:directory_object->unit->[`ZipFileoffile_object|`Errorofstring]M.t;(** Create a zip archive of the directory, placing it in the
[staging_dir] directory. *)spawn_in_directory:command:MlFront_Core.FilePath.t->args:stringlist->envmods:MlFront_Core.EnvMods.t->stdout:file_object->stderr:file_object->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t;(** Spawns the command in the directory. If the directory is remote, the
semantics are to run the command remotely. *)interactive_shell_in_directory:?promptname:string->envmods:MlFront_Core.EnvMods.t->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t;(** Spawns a shell in the directory. If the directory is remote, it is
an error.
It is implementation dependent if this function returns or if the
current program is replaced by the shell. *)}(** {1 Constructors and Accessors} *)valgeneric_file:origin:string->is_local_file:bool->open_for_writing:(unit->[`NodeofInt64.t|`IsDirectoryofdirectory_object|`Errorofstring]M.t)->open_for_reading:(unit->[`NodeofInt64.t|`IsDirectoryofdirectory_object|`Errorofstring]M.t)->read_some:(Int64.t->[`Bytesofbytes*int*int|`Eof|`Errorofstring]M.t)->write_all:(Int64.t->string->int->int->[`WroteBytes|`Errorofstring]M.t)->close:(Int64.t->unitM.t)->read_all:(unit->[`Contentofstring|`Errorofstring|`ExceededSizeLimitofint64]M.t)->prepare_as_copy_destination:(unit->[`Ready|`Errorofstring]M.t)->delete_file:(unit->[`Deleted|`Errorofstring]M.t)->checksum_file:(strip_carriage_returns:bool->algo:[`Sha1|`Sha256]->unit->[`Checksumofstring*Int64.t|`Errorofstring]M.t)->unit->file_object(** [generic_file ~origin ~is_local_file ~open_for_writing ~open_for_reading
~read_some ~write_all ~close ~read_all ~prepare_as_copy_destination
~delete_file ~checksum_file] creates a new file object. *)valgeneric_dir:origin:string->create_directory:(unit->[`Created|`Errorofstring]M.t)->delete_directory:(unit->[`Deleted|`Errorofstring]M.t)->zip_directory:(?intermediate:unit->staging_dir:directory_object->unit->[`ZipFileoffile_object|`Errorofstring]M.t)->spawn_in_directory:(command:MlFront_Core.FilePath.t->args:stringlist->envmods:MlFront_Core.EnvMods.t->stdout:file_object->stderr:file_object->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t)->interactive_shell_in_directory:(?promptname:string->envmods:MlFront_Core.EnvMods.t->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t)->unit->directory_object(** [generic_dir ~origin ~create_directory] creates a new directory object. *)valinmemory_file:origin:MlFront_Core.FilePath.t->string->file_object(** [inmemory_file ~origin contents] creates a new in-memory file object whose
origin is [origin] and whose contents are [contents]. *)valinmemory_dir:origin:MlFront_Core.FilePath.t->unit->directory_object(** [inmemory_dir ~origin ()] creates a new in-memory directory object whose
origin is [origin]. *)valfile_origin:file_object->string(** The location of the file. It should be clickable if possible by modern
IDEs in a terminal. For example, a file should just be a path to the file
(no ["file://"] prefix), while a http URL should be a URL. *)valdirectory_origin:directory_object->string(** The location of the directory. It should be clickable if possible by
modern IDEs in a terminal. For example, a file should just be a path to
the file (no ["file://"] prefix), while a http URL should be a URL. *)(** {2 File Operations} *)valread_all:file_object->[`Contentofstring|`Errorofstring|`ExceededSizeLimitofint64]M.t(** [read_all source] gets the file, but may not give back the file if it
exceeds memory.
The [error] in [`Error error], if possible, should complete the phrase
["Cannot read the file because ___."]. For example,
[`Error "the file was not found"]. *)valcopy:src:file_object->dest:file_object->unit->[`Copied|`DestinationIsDirectoryofdirectory_object|`Errorofstring|`SourceIsDirectoryofdirectory_object]M.t(** [copy ~src ~dest ()] copies the file [src] to the destination file [dest].
On success, the file will have the executable bit set on Unix. *)valcopy_or_fail:src:file_object->dest:file_object->on_error:(string->'aM.t)->'aM.t->'aM.t(** [copy_or_fail ~src ~dest ~on_error successvalue] copies the file [src] to
the destination file [dest].
[successvalue] is returned on success.
On success, the file will have the executable bit set on Unix.
The result of [on_error because] is returned on error, where [because] is
a ["<subject phrase> was <adjective phrase>"] sentence fragment. *)valcopy_but_error_if_dest_is_dir:src:file_object->dest:file_object->unit->[`Copied|`Errorofstring|`SourceIsDirectoryofdirectory_object]M.t(** [copy_but_error_if_dest_is_dir ~src ~dest ()] copies the file [src] to the
destination file [dest]. If [dest] is a directory, an error is returned.
On success, the file will have the executable bit set on Unix. *)valchecksum_file:?strip_carriage_returns:unit->algo:[`Sha1|`Sha256]->file_object->[`Errorofstring|`Checksumofstring*Int64.t]M.t(** [checksum_file ?strip_carriage_returns file] gets the SHA256 or SH1
checksum of the file [file].
If the flag [~strip_carriage_returns:()] is used, the checksum will be of
the file {b after} it has been stripped of carriage returns (CR) with
strip_carriage_returns. Only use this if you can guarantee removing
carriage returns does not affect the semantics of the file. For example,
OCaml source code can have multi-line strings where carriage returns
should not be stripped. But JSON, where strings must have escaped carriage
returns, can have all CR (ASCII 13) stripped. *)valreplace_all_bytes:file_object->bytes->int->int->[`Errorofstring|`IsDirectoryofdirectory_object|`WroteBytes]M.t(** [replace_all_bytes file bytes pos len] replaces the contents of the file
[file] with the bytes [bytes] starting at position [pos] for length [len].
On success, the file will have the executable bit set on Unix. *)valreplace_all_string:file_object->string->int->int->[`Errorofstring|`IsDirectoryofdirectory_object|`WroteBytes]M.t(** [replace_all_string file str pos len] replaces the contents of the file
[file] with the string [str] starting at position [pos] for length [len].
On success, the file will have the executable bit set on Unix. *)valdelete_file:file_object->[`Deleted|`Errorofstring]M.tvalis_local_file:file_object->bool(** {2 Directory Operations} *)valcreate_directory:directory_object->[`Created|`Errorofstring]M.tvaldelete_directory:directory_object->[`Deleted|`Errorofstring]M.tvalspawn_in_directory:command:MlFront_Core.FilePath.t->args:stringlist->cwd:directory_object->envmods:MlFront_Core.EnvMods.t->stdout:file_object->stderr:file_object->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t(** [spawn_in_directory ~command ~args ~cwd ~envmods ~stdout ~stderr] runs the
command [command] with arguments [args] and environment modifications
[envmods] in the working directory [cwd].
A log of the standard output and standard error are written to the files
[stdout] and [stderr], respectively.
It returns a continuation that is one of:
+ [`Error error] if there was an error trying to run the command.
+ [`Exited code] if the process exited normally with exit code [code]
+ [`Signaled signal] if the process was killed by signal [signal]
+ [`Stopped signal] if the process was stopped by signal [signal]
The [cwd] is the current working directory of the command. It may be a
remote directory; if so, the command should run remotely.
The [command] should be an absolute path to the command, or a path
relative to the current working directory [cwd].
The first element of [args] should be the path to the command or the
basename of the command.
The [envmods] are modifications to the environment variables. See
{!MlFront_Core.EnvMods} for details. *)valinteractive_shell_in_directory:?promptname:string->envmods:MlFront_Core.EnvMods.t->cwd:directory_object->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t(** [interactive_shell_in_directory ?promptname ~envmods ~cwd ()] runs a shell
in the working directory [cwd] with the environment modifications
[envmods].
The shell is interactive with access to the terminal's standard input,
output, and error.
[promptname] is used as as hint for inclusion into the shell prompt.
It is implementation dependent if this function returns or if the current
program is replaced by the shell *)valcopy_file_or_dir_to_file_and_sha256_or_fail:?intermediate:unit->src:file_object->dest:file_object->staging_dir:directory_object->on_error:(string->'bM.t)->(string->int64->'bM.t)->'bM.t(** [copy_file_or_dir_to_file_and_sha256_or_fail ?intermediate ~src ~dest
~staging_dir ~on_error successfunc] copies the file or directory [src] to
the destination file [dest].
The [src] is treated presumptively as a file. If and only if the copy
operation fails specifically because [src] was detected to be a directory,
then:
+ [staging_dir] is created if it does not exist
+ a zip archive file is created as a temporary file in [staging_dir] from
the contents of [src] in the "deterministic" mode of the SPECIFICATION
document
+ the temporary file is copied to [dest]
+ the temporary file is deleted (regardless of success or error)
[successfunc sha256 file_sz] is returned on success, where [sha256] is the
SHA256 hex-encoded hash of the copied file and [file_sz] is the size of
the copied file.
On success, the [dest] file will have the executable bit set on Unix.
The result of [on_error because] is returned on error, where [because] is
a ["<subject phrase> was <adjective phrase>"] sentence fragment. *)end=structtypefile_object={file_origin:string;is_local_file:bool;open_for_writing:unit->[`NodeofInt64.t|`IsDirectoryofdirectory_object|`Errorofstring]M.t;open_for_reading:unit->[`NodeofInt64.t|`IsDirectoryofdirectory_object|`Errorofstring]M.t;read_some:Int64.t->[`Bytesofbytes*int*int|`Eof|`Errorofstring]M.t;write_all:Int64.t->string->int->int->[`WroteBytes|`Errorofstring]M.t;close:Int64.t->unitM.t;read_all:unit->[`Contentofstring|`Errorofstring|`ExceededSizeLimitofint64]M.t;prepare_as_copy_destination:unit->[`Ready|`Errorofstring]M.t;delete_file:unit->[`Deleted|`Errorofstring]M.t;checksum_file:strip_carriage_returns:bool->algo:[`Sha1|`Sha256]->unit->[`Checksumofstring*Int64.t|`Errorofstring]M.t;}anddirectory_object={dir_origin:string;create_directory:unit->[`Created|`Errorofstring]M.t;delete_directory:unit->[`Deleted|`Errorofstring]M.t;zip_directory:?intermediate:unit->staging_dir:directory_object->unit->[`ZipFileoffile_object|`Errorofstring]M.t;spawn_in_directory:command:MlFront_Core.FilePath.t->args:stringlist->envmods:MlFront_Core.EnvMods.t->stdout:file_object->stderr:file_object->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t;interactive_shell_in_directory:?promptname:string->envmods:MlFront_Core.EnvMods.t->unit->[`Errorofstring|`Exitedofint|`Signaledofint|`Stoppedofint]M.t;}letfile_origin{file_origin;_}=file_originletread_all{read_all;_}=read_all()letchecksum_file?strip_carriage_returns~algo{checksum_file;_}=checksum_file~strip_carriage_returns:(strip_carriage_returns=Some())~algo()letdelete_file{delete_file;_}=delete_file()letis_local_file{is_local_file;_}=is_local_fileletdefault_open_bufsize=16_384letcreate_directory{create_directory;_}=create_directory()letdelete_directory{delete_directory;_}=delete_directory()letdirectory_origin{dir_origin;_}=dir_originletspawn_in_directory~command~args~cwd~envmods~stdout~stderr()=cwd.spawn_in_directory~command~args~envmods~stdout~stderr()letinteractive_shell_in_directory?promptname~envmods~cwd()=cwd.interactive_shell_in_directory?promptname~envmods()letreplace_all_string{open_for_writing;write_all;close;_}bytesposlen=let(let*)=M.bindinlet*open_result=open_for_writing()inmatchopen_resultwith|`IsDirectoryd->M.pure(`IsDirectoryd)|`Errore->M.pure(`Errore)|`Noden->let*write_result=write_allnbytesposleninlet*()=closeninM.pure(write_result:>[`Errorofstring|`IsDirectoryofdirectory_object|`WroteBytes])letreplace_all_bytesiobytes=replace_all_stringio(Bytes.to_stringbytes)letgeneric_file~origin~is_local_file~open_for_writing~open_for_reading~read_some~write_all~close~read_all~prepare_as_copy_destination~delete_file~checksum_file()={file_origin=origin;is_local_file;open_for_writing;open_for_reading;read_some;write_all;close;read_all;prepare_as_copy_destination;delete_file;checksum_file;}letgeneric_dir~origin~create_directory~delete_directory~zip_directory~spawn_in_directory~interactive_shell_in_directory()={dir_origin=origin;create_directory;delete_directory;zip_directory;spawn_in_directory;interactive_shell_in_directory;}letinmem_read_records=Hashtbl.create1letinmem_write_records=Hashtbl.create1letinmem_counter=ref0Lletinmemory_file~origincontents=letorigin_s=MlFront_Core.FilePath.to_stringoriginin{file_origin=origin_s;is_local_file=false;open_for_writing=(fun()->letidx=!inmem_counterininmem_counter:=Int64.succ!inmem_counter;Hashtbl.addinmem_write_recordsidx(origin,Buffer.createdefault_open_bufsize);M.return(`Node0L));open_for_reading=(fun()->letidx=!inmem_counterininmem_counter:=Int64.succ!inmem_counter;Hashtbl.addinmem_read_recordsidx(origin,`Started);M.return(`Nodeidx));read_some=(funidx->matchHashtbl.find_optinmem_read_recordsidxwith|Some(_origin,`Started)->Hashtbl.addinmem_read_recordsidx(origin,`Done);M.return(`Bytes(Bytes.of_stringcontents,0,String.lengthcontents))|Some(_origin,`Done)->M.return`Eof|None->M.return(`Error(Printf.sprintf"in-memory file `%s` is not open"origin_s)));write_all=(funidxstrposlen->matchHashtbl.find_optinmem_write_recordsidxwith|Some(_origin,buffer)->Buffer.add_substringbufferstrposlen;M.return`WroteBytes|None->M.return(`Error(Printf.sprintf"in-memory file `%s` is not open"origin_s)));close=(fun_->M.return());read_all=(fun()->M.return(`Contentcontents));prepare_as_copy_destination=(fun()->M.return`Ready);delete_file=(fun()->(* delete matching entries *)Hashtbl.filter_map_inplace(fun_idx(origin',status)->ifMlFront_Core.FilePath.compareoriginorigin'=0thenNoneelseSome(origin',status))inmem_read_records;Hashtbl.filter_map_inplace(fun_idx(origin',buffer)->ifMlFront_Core.FilePath.compareoriginorigin'=0thenNoneelseSome(origin',buffer))inmem_write_records;M.return`Deleted);checksum_file=(letdo_strip_crs=letbuf=Buffer.create(String.lengths)inString.iter(func->ifc<>'\r'thenBuffer.add_charbufcelse())s;Buffer.contentsbufinfun~strip_carriage_returns~algo()->letcontents=ifstrip_carriage_returnsthendo_strip_crcontentselsecontentsinletlen=String.lengthcontentsinmatchalgowith|`Sha1->letcksum=Digestif.SHA1.digest_stringcontentsinM.return(`Checksum(Digestif.SHA1.to_hexcksum,Int64.of_intlen))|`Sha256->letcksum=Digestif.SHA256.digest_stringcontentsinM.return(`Checksum(Digestif.SHA256.to_hexcksum,Int64.of_intlen)));}letinmemory_dir~origin()=letorigin_s=MlFront_Core.FilePath.to_stringoriginin{dir_origin=origin_s;create_directory=(fun()->M.return`Created);delete_directory=(fun()->(* delete descendant file entries *)Hashtbl.filter_map_inplace(fun_idx(origin',status)->ifMlFront_Core.FilePath.is_subpathoriginorigin'thenNoneelseSome(origin',status))inmem_read_records;Hashtbl.filter_map_inplace(fun_idx(origin',buffer)->ifMlFront_Core.FilePath.is_subpathoriginorigin'thenNoneelseSome(origin',buffer))inmem_write_records;M.return`Deleted);zip_directory=(fun?intermediate:_~staging_dir:_()->M.return(`Error(Printf.sprintf"currently there is no support for zipping in-memory \
directories including `%s`"origin_s)));spawn_in_directory=(fun~command:_~args:_~envmods:_~stdout:_~stderr:_()->M.return(`Error"currently there is no support for spawning commands in \
in-memory directories"));interactive_shell_in_directory=(fun?promptname:_~envmods:_()->M.return(`Error"currently there is no support for launching shells in \
in-memory directories"));}letcopy~src~dest()=(* Copy file using buffered copy *)let(let*)=M.bindinlet*prepare_result=dest.prepare_as_copy_destination()inmatchprepare_resultwith|`Errore->M.pure(`Errore)|`Ready->(let*openwrite_result=dest.open_for_writing()inmatchopenwrite_resultwith|`Errore->M.pure(`Errore)|`IsDirectoryd->M.pure(`DestinationIsDirectoryd)|`Nodedest_node->(letfinish()=dest.closedest_nodeinlet*openread_result=src.open_for_reading()inmatchopenread_resultwith|`Errore->let*()=finish()inM.pure(`Errore)|`IsDirectoryd->let*()=finish()inM.pure(`SourceIsDirectoryd)|`Nodesrc_node->(* We have both source and destination nodes open for reading/writing *)letfinish()=let*()=dest.closedest_nodeinlet*()=src.closesrc_nodeinM.pure()inletrecauxacc=let*c=accinmatchcwith|`Errore->M.pure(`Errore)|`Copied->(let*read_result=src.read_somesrc_nodeinmatchread_resultwith|`Errore->M.pure(`Errore)|`Eof->M.pure`Copied|`Bytes(b,off,len)->(let*write_result=dest.write_alldest_node(Bytes.to_stringb)offleninmatchwrite_resultwith|`Errore->M.pure(`Errore)|`WroteBytes->aux(M.pure`Copied)))inlet*final_result=aux(M.return`Copied)inlet*()=finish()inM.purefinal_result))letcopy_or_fail~src~dest~on_errorsuccessvalue=let(let*)=M.bindinlet*copy_result=copy~src~dest()inletr=matchcopy_resultwith|`Copied->`Copied|`DestinationIsDirectory_d->`Error(Printf.sprintf"destination path `%s` is a directory"(file_origindest))|`Errors->`Errors|`SourceIsDirectory_d->`Error(Printf.sprintf"source path `%s` is a directory"(file_originsrc))inmatchrwith`Copied->successvalue|`Errorbecause->on_errorbecauseletcopy_but_error_if_dest_is_dir~src~dest()=let(let*)=M.bindinlet*copy_result=copy~src~dest()inmatchcopy_resultwith|`Copied->M.pure`Copied|`DestinationIsDirectory_d->M.pure(`Error(Printf.sprintf"destination path `%s` is a directory"(file_origindest)))|`Errors->M.pure(`Errors)|`SourceIsDirectoryd->M.pure(`SourceIsDirectoryd)letcopy_file_or_dir_to_file_and_sha256_or_fail=let(let*)=M.bindinfun?intermediate~src~dest~staging_dir~on_errorsuccessvalue->letintermediatefile_pending_deletion=refNoneinFun.protect~finally:(fun()->ifintermediate=NonethentryOption.iterSys.remove!intermediatefile_pending_deletionwithSys_error_->())(fun()->let*reduced_result=copy_but_error_if_dest_is_dir~src~dest()inlet*r=matchreduced_resultwith|`Copied->M.pure`Copied|`Errors->M.pure(`Errors)|`SourceIsDirectorysrcdir->(let*dir_result=staging_dir.create_directory()inmatchdir_resultwith|`Errore->M.pure(`Errore)|`Created->(let*zip_result=srcdir.zip_directory?intermediate~staging_dir()inmatchzip_resultwith|`Errore->M.pure(`Errore)|`ZipFileintermediatezipfile->((* copy the zipfile to the destination.
TODO: move instead of copy, or at least try to move first. *)intermediatefile_pending_deletion:=Some(file_originintermediatezipfile);let*copy_result=copy_but_error_if_dest_is_dir~src:intermediatezipfile~dest()inmatchcopy_resultwith|`Errore->let*()=ifintermediate=Some()thenlet*_delete_result:[`Deleted|`Errorofstring]=intermediatezipfile.delete_file()inM.pure()elseM.pure()inM.pure(`Errore)|`Copied->M.pure`Copied|`SourceIsDirectory_->M.pure(`Error(Printf.sprintf"intermediate zipfile path `%s` is a \
directory"(file_originintermediatezipfile))))))inmatchrwith|`Errorbecause->on_errorbecause|`Copied->(let*sha256_result=checksum_file~algo:`Sha256destinmatchsha256_resultwith|`Errore->on_errore|`Checksum(sha256,filesz)->successvaluesha256filesz))end