FileSystem.Handle
A program sometimes needs to keep a file open: to read and write it at chosen offsets, or to go on acting on the file it opened after that file's path has been renamed or replaced. This module does that with a Handle, which stands for one open file. Most programs need only FileSystem, whose tasks, apart from the file streams, each open and close whatever they need; FileSystem.writeFileWith also covers durable and exclusive writes.
A handle's type records what it may be used for. In Handle readAccess writeAccess, the reading
functions accept a handle only if the first parameter is ReadAccess, and the
functions that change the file only if the second is WriteAccess. The function
that opened the file decides both, makeReadOnly and
makeWriteOnly give one of them up, and close takes any handle. Each
task that changes the file succeeds with the handle it was given, so that calls on one file can be
chained.
Reads and writes say where in the file they act, as a byte offset from its start. Apart from
writeFromOffset with a negative offset, nothing here reads or moves a current
position in the file. read reads the whole file and write writes at offset 0,
so two writes overwrite each other rather than appending. To read or write a file from start to
end in pieces, use FileSystem.readFileStream and
FileSystem.writeFileStream.
A handle stays open until close. An operation on it started after that fails with HandleClosed, unless its file descriptor, the operating system's number for an open file, has since been reused for a file opened by this module or by FileSystem.writeFileWith with an option on, and that file is still open. The closed handle then acts on that file.
A task that fails, fails with a FileSystem.Error. When opening fails, errorPath is the path that was being opened; for every operation on a handle it is Path.empty. The path to open reaches the operating system as Path.toPosixString prints it.
On the native backend under Windows, opening a file fails with NotSupported, so no handle can be obtained there.
type Handle readAccess writeAccesstype alias ReadableHandle a =
Handle ReadAccess aA Handle that may be read, whatever its write access.
type alias WriteableHandle a =
Handle a WriteAccessA Handle that may be used to change the file, whatever its read access.
type alias ReadWriteableHandle =
Handle ReadAccess WriteAccessA Handle that may be both read and used to change the file, as openForReadAndWrite returns.
type ReadAccessThe marker, in a Handle's first type parameter, that the handle may be read.
It is used only as a type parameter; a program cannot make a value of it.
type WriteAccessThe marker, in a Handle's second type parameter, that the handle may be used to change the file.
It is used only as a type parameter; a program cannot make a value of it.
makeReadOnly : ReadWriteableHandle -> Handle ReadAccess NeverReturns the same open file as a handle that may only be read, for passing to code that should not change the file. Closing either handle closes the file.
makeWriteOnly : ReadWriteableHandle -> Handle Never WriteAccessReturns the same open file as a handle that may only be used to change the file, for passing to code that should not read it. Closing either handle closes the file.
File open/close
openForRead : Path -> Task Error (Handle ReadAccess Never)Opens the file at the path for reading. The task fails if the file cannot be opened, as when nothing exists at the path.
type OpenForWriteBehaviour
= EnsureEmpty
| ExpectExisting
| ExpectNotExistingWhat opening a file for writing does with whatever is at the path already.
EnsureEmpty creates the file if nothing exists at the path, and empties the file if it exists.
ExpectExisting opens the file with its contents as they are, and fails if nothing exists at the
path. It opens the file for reading as well as writing, whichever function opens it, so the
program must be allowed to read the file too.
ExpectNotExisting creates the file, and fails with FileExists if
anything already exists at the path. The operating system checks and creates in one step (O_EXCL), so of two programs creating
the same file at most one succeeds.
A file that opening creates gets the mode 666, less the bits the process's umask removes.
openForWrite :
OpenForWriteBehaviour
-> Path
-> Task Error (Handle Never WriteAccess)Opens the file at the path for writing, treating what is already there as the OpenForWriteBehaviour says.
openForReadAndWrite :
OpenForWriteBehaviour
-> Path
-> Task Error ReadWriteableHandleOpens the file at the path for both reading and writing, treating what is already there as the OpenForWriteBehaviour says.
close : Handle a b -> Task Error ()Closes the file.
The handle, and every copy of it, counts as closed from then on, even if closing fails. An
operation on it started afterwards, a second close included, fails with
HandleClosed, except after the reuse of its file descriptor that the
module documentation describes.
File metadata
metadata : ReadableHandle a -> Task Error MetadataReturns the metadata of the open file, with the fields FileSystem.Metadata describes.
Its entityType is never Symlink: opening a path that names a symbolic link opens the entity
the link points to.
changeAccess :
{ owner : List AccessPermission
, group : List AccessPermission
, others : List AccessPermission
}
-> WriteableHandle a
-> Task Error (WriteableHandle a)Sets the access permissions of the open file, for its owner, its group and everyone else.
Each list becomes one octal digit of the file mode, as in
FileSystem.changeAccess, so
{ owner = [ Read, Write ], group = [ Read ], others = [ Read ] } sets the mode 644. The mode
has no other bits, so the set-user-ID, set-group-ID and sticky bits are cleared.
changeOwner :
{ userID : Int, groupID : Int }
-> WriteableHandle a
-> Task Error (WriteableHandle a)Sets the user and group that own the open file, given as numeric ids.
changeTimes :
{ lastAccessed : Posix, lastModified : Posix }
-> WriteableHandle a
-> Task Error (WriteableHandle a)Sets the last access and last modification times of the open file.
The times are set in whole seconds, as for FileSystem.changeTimes: each
is sent as its milliseconds divided by 1000 with //, which rounds down for times after 1970 and
toward zero for times before. On the JS backend // gives a 32-bit result, so a time from
2038-01-19T03:14:08Z on, or at or before 1901-12-13T20:45:51Z, is sent wrapped round into that
range, and a different time is set.
Read from file
read : ReadableHandle a -> Task Error BytesReads the whole file, from its first byte to its end.
readFromOffset :
ReadableHandle a
-> { offset : Int, length : Int }
-> Task Error BytesReads up to length bytes, starting offset bytes into the file.
Fewer bytes come back if the end of the file comes first, and none if offset is at or past it.
A negative length reads to the end of the file, and a negative offset reads from the start.
Write to file
write : WriteableHandle a -> Bytes -> Task Error (WriteableHandle a)Writes the bytes at the start of the file.
The bytes already in the file are overwritten where the new ones fall, and any past them are kept: the file is not shortened. writeFromOffset writes elsewhere, and truncate shortens the file.
writeFromOffset :
WriteableHandle a
-> Int
-> Bytes
-> Task Error (WriteableHandle a)Writes the bytes into the file starting offset bytes from its start, overwriting what is
there.
A negative offset writes at the open file's current position instead. That position is the
start of the file when it is opened, and only a write with a negative offset moves it, to just
past the bytes written. So a sequence of such writes, each started after the one before has
succeeded, through any copies of the handle, puts its bytes one after another.
truncate : Int -> WriteableHandle a -> Task Error (WriteableHandle a)Sets the size of the file to length bytes. A longer file loses the bytes past length, and a
shorter one is extended with zero bytes.
A negative length fails with InvalidInput on the native backend, and empties the file on the JS
backend.
sync : WriteableHandle a -> Task Error (WriteableHandle a)Flushes the file's changes, its data and its metadata, from the operating system's buffers to
the storage device (fsync), and succeeds once the operating system reports them written.
syncData : WriteableHandle a -> Task Error (WriteableHandle a)Flushes the file's data to the storage device, as sync does, but with fdatasync,
which may leave metadata that reading the data back does not need, such as the times, unwritten.
On the native backend under macOS it asks instead for F_FULLFSYNC, which also flushes the
storage device's own cache, and uses fsync if that fails; sync there uses fsync
alone.
An open file, together with what its holder may do with it.
A handle comes from openForRead, openForWrite or openForReadAndWrite, or is a copy of one. Its first type parameter can be ReadAccess only if the file was opened for reading, and its second WriteAccess only if it was opened for writing; the open functions put
Neverwhere an access is absent. The converse does not hold:openForWrite ExpectExistingopens the file for reading too, and its handle still has no read access.Holding a handle is all it takes to use it within its access, so pass one only to code that should have that access.
Copies of a handle, including those makeReadOnly and makeWriteOnly return, are the same open file, and closing one closes them all.