Safe Haskell | Safe |
---|---|
Language | Haskell2010 |
Often there's no clear way to preform atomic file system writes. The usual way around this is to lock the file exclusively before writing. But file system locks are generally advisory, so if an independent process which does not attempt to gain a lock attempts to read the file, it may read an intermediate inconsistent state.
This package contains some functions for avoid this state of affairs. It does this by creating a temp file, performing the writes to the temp file, and then moving the temp file over the existing file. Generally in filesystems, moves are atomic, so using this scheme a reading process should only see the old version of the file or the new version of the file, and not an intermediate state.
One may be concerned if a file is being read as it is replaced that the start of the old file is read and the end of the new file is read. However, this is not likely to be the case, as when a file is opened, that same file is kept open and readable even if it is say, deleted. As these functions do not modify the file, but instead replace it, opening the file before it is atomically modified will just result in old data being read, not an intermediate state.
The functions in this package reference classes from the package "io-string-like",
which means one can write String
s, ByteString
s or even Text
s to the file
using the same function.
- atomicReplaceFile :: CanPutStr contents => Maybe AtomicTempOptions -> FilePath -> contents -> IO ()
- atomicModifyFile :: (CanGetContents contents, CanPutStr contents) => Maybe AtomicTempOptions -> FilePath -> FilePath -> (contents -> IO (IO a, Maybe contents)) -> IO a
- data AtomicTempOptions
Documentation
atomicReplaceFile :: CanPutStr contents => Maybe AtomicTempOptions -> FilePath -> contents -> IO () Source #
atomicReplaceFile options @file@ contents
atomically replaces file
's with contents
.
options
should be Just
some AtomicTempOptions
, which detail where the
temporary file is placed, or Nothing
for the default of creating a temp file
in the existing directory.
If the target file must exist.
As moving is atomic on filesystems generally, the target file should only ever be in two states, either what it was like before it was written or what it was like after.
atomicModifyFile :: (CanGetContents contents, CanPutStr contents) => Maybe AtomicTempOptions -> FilePath -> FilePath -> (contents -> IO (IO a, Maybe contents)) -> IO a Source #
Atomically modifies a file, by reading it and applying a function to it's contents.
This is different to atomicReplaceFile
, which is best illistrated by an example.
Lets say a file contains a single integer, say "1". An IO statement is writen that
reads the file, adds one to the number, and then calls atomicReplaceFile
to
write it back out.
Running this should result the file being replaced with the contents "2".
Now lets say we run this from two different processes.
If the processes run sequentially, we will read "1", then write "2", then read "2", then write "3".
But if the processes run overlapping, the first process may read "1", and before it writes "2", the second process may read "1". The first process will then write "2" but then also the second process will write "2".
We've clobbered a write, and the behavour is inconsistent based on timing.
This is where atomicModifyFile
comes in.
atomicModifyFile
gets passed a function, and will lock a lock file,
(passed as an argument) to ensure two processes can not run on it at the same time.
If the two processes in the above case both used atomicModifyFile
, then the
result would be "3" in call cases.
Make sure the lock file is different to the file you're modifying, or else you'll get permission denied errors on windows.
One may ask, if you're going to lock the files, why bother with temp files at all?
Well, you wouldn't need to if all your processes asked for locks. But atomicModifyFile
achieves two things:
- From writers that use
atomicModifyFile
: Writes occur sequentially - From all readers (including those who do not attempt to gain a lock): Will only ever see a consistent state.
Just using locks and not an "atomic style" temp file and replace does not achieve point 2.
The arguments to atomicModifyFile
are similar to atomicReplaceFile
, except for
the third argument, which details how to replace the contents of the file.
This replacement function argument is quite complex. Lets say we have the following:
f x = duringAction >> pure (afterAction, whatToWrite) atomicModifyFile options file f
The whole result of f
must be an IO action. Often this will be just pure something
,
but in the above case I've included an actual IO action: duringAction
.
The following sequences of events will occur:
- Exclusively lock the lock file.
- The entire contents of the file will be read.
- The IO
duringAction
will be executed, asf
is run against the contents of the file. - If
whatToWrite
isJust contents
,contents
will be written to to a temp file and then moved over the target file. But ifwhatToWrite
isNothing
, do nothing. - Unlock the lock file.
afterAction
will be executed as the IO return value ofatomicModifyFile
.
So this function has a lot of flexibility, however, in many simple use cases it will look something like this:
atomicModifyFile options file (\contents -> pure (pure (), Just (f contents)))
Which just says apply f
to the contents of the contents of the file and do nothing else.
Like atomicReplaceFile
, atomicModifyFile
will fail if the file
does not exist.
data AtomicTempOptions Source #
This data type is passed to atomicReplaceFile
and atomicModifyFile
to change
the behaviour of where the temp file is created.
The following will be called inside the atomic write functions:
openTempFile tempFileDir tempFileTemplate
See the documentation for openTempFile
to see how these arguments are used.