atomic-file-ops (empty) → 0.1.0.0
raw patch · 4 files changed
+251/−0 lines, 4 filesdep +basedep +directorydep +filelocksetup-changed
Dependencies added: base, directory, filelock, filepath, io-string-like
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- atomic-file-ops.cabal +40/−0
- src/System/IO/AtomicFileOps.hs +179/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Clinton Mead (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Clinton Mead nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ atomic-file-ops.cabal view
@@ -0,0 +1,40 @@+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: c8cbca84fbab5cba02ff397261498bcbfec3796c050709961d0f4afe9f982241++name: atomic-file-ops+version: 0.1.0.0+synopsis: Functions to atomically write to files+description: See module "System.IO.AtomicFileOps" for an overview and documentation.+category: System+homepage: https://github.com/clintonmead/atomic-file-ops#readme+bug-reports: https://github.com/clintonmead/atomic-file-ops/issues+author: Clinton Mead+maintainer: clintonmead@gmail.com+copyright: Copyright: (c) 2017 Clinton Mead+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10++source-repository head+ type: git+ location: https://github.com/clintonmead/atomic-file-ops++library+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ base >=4.7 && <5+ , directory+ , filelock+ , filepath+ , io-string-like+ exposed-modules:+ System.IO.AtomicFileOps+ other-modules:+ Paths_atomic_file_ops+ default-language: Haskell2010
+ src/System/IO/AtomicFileOps.hs view
@@ -0,0 +1,179 @@+{-# LANGUAGE NamedFieldPuns #-}++{-|+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.+-}+module System.IO.AtomicFileOps (+ atomicReplaceFile, atomicModifyFile,+ AtomicTempOptions(tempFileDir, tempFileTemplate)+ )+where++import System.IO.StringLike.PutStr (CanPutStr, hPutStr)+import System.IO.StringLike.GetContents (CanGetContents, hGetContents)+import System.IO (openTempFile, hClose, Handle, withFile)+import System.Directory (copyPermissions, renameFile)+import System.FilePath (splitFileName)+import Data.Semigroup ((<>))+import System.IO.Error (catchIOError, ioError, isDoesNotExistError)+import Data.Maybe (fromMaybe)+import System.FileLock (withFileLock, SharedExclusive(Exclusive))+{-|+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.+-}+data AtomicTempOptions = AtomicTempOptions { tempFileDir :: FilePath, tempFileTemplate :: String }++defaultTempOptions :: FilePath -> AtomicTempOptions+defaultTempOptions filePath = AtomicTempOptions{tempFileDir, tempFileTemplate}+ where+ (tempFileDir, filename) = splitFileName filePath+ tempFileTemplate = filename <> ".tmp"++-- Internal function that just spits out the default temp options if you pass+-- it 'Nothing' but otherwise removes the 'Just'.+fromMaybeTempOptions :: FilePath -> Maybe AtomicTempOptions -> AtomicTempOptions+fromMaybeTempOptions filePath = fromMaybe (defaultTempOptions filePath)++{-|+> 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.+-}+atomicReplaceFile :: CanPutStr contents => Maybe AtomicTempOptions -> FilePath -> contents -> IO ()+atomicReplaceFile maybeTempOptions fileToReplace contents = do+ let AtomicTempOptions{tempFileDir, tempFileTemplate} = fromMaybeTempOptions fileToReplace maybeTempOptions+ (tmpFileName, tmpHandle) <- openTempFile tempFileDir tempFileTemplate+ copyPermissions fileToReplace tmpFileName+ hPutStr tmpHandle contents+ hClose tmpHandle+ renameFile tmpFileName fileToReplace++{-|+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 the file, 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.++But it's worth mentioning that locking is advisory in many file systems. If you+don't consistently use 'atomicModifyFile' with all your writers you may get the+timing issues discussed previously. Indeed if you mix 'atomicReplaceFile' with+'atomicModifyFile' you may still get timing issues.++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:++1. From writers that use 'atomicModifyFile': Writes occur sequentially+2. 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:++1. The file will be exclusively locked.+2. The entire contents of the file will be read.+3. The IO @duringAction@ will be executed, as @f@ is run against the contents of the file.+4. If @whatToWrite@ is @Just contents@, @contents@ will be written to to a temp file and+ then moved over the target file.+ But if @whatToWrite@ is @Nothing@, do nothing.+5. The file will be unlocked. In theory this is unnecessary as we're now only+ locking the now dead file we just replaced. However that depends on whether+ the filesystem locks based on filename or filehandle. Anyway lets we play it safe+ and unlock, which we would need to do if we did nothing in part 4 anyway.+6. @afterAction@ will be executed as the IO return value of 'atomicModifyFile'.++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.+-}+atomicModifyFile :: (CanGetContents contents, CanPutStr contents)+ => Maybe AtomicTempOptions -> FilePath -> (contents -> IO (IO a, Maybe contents)) -> IO a+atomicModifyFile maybeTempOptions fileToReplace contentsFunction =+ withFileLock fileToReplace Exclusive actionDuringLock where+ actionDuringLock _ = do+ contents <- hGetContents fileToReplace+ (result, maybeNewContents) <- contentsFunction contents+ case maybeNewContents of+ Just newContents -> atomicReplaceFile maybeTempOptions fileToReplace newContents+ Nothing -> pure ()+ result