packages feed

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 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