safeio 0.0.1.0 → 0.0.2.0
raw patch · 5 files changed
+122/−11 lines, 5 filesdep +bytestringdep +conduitdep +conduit-combinators
Dependencies added: bytestring, conduit, conduit-combinators, resourcet
Files
- ChangeLog +3/−0
- Data/Conduit/SafeWrite.hs +46/−0
- README.md +16/−1
- System/IO/SafeWrite/Tests.hs +45/−8
- safeio.cabal +12/−2
ChangeLog view
@@ -1,2 +1,5 @@+Version 0.0.2.0 2017-05-30 by luispedro+ * Add conduit version+ Version 0.0.1.0 2017-05-29 by luispedro * First version
+ Data/Conduit/SafeWrite.hs view
@@ -0,0 +1,46 @@+module Data.Conduit.SafeWrite+ ( safeSinkFile+ ) where++import qualified Data.Conduit as C+import qualified Data.Conduit.Combinators as CC+import qualified Data.ByteString as B+import Data.IORef (newIORef, writeIORef, readIORef)+import System.FilePath (takeDirectory)+import System.IO (hClose, openTempFile)+import System.Directory (renameFile, removeFile)+import Control.Monad (unless)+import Control.Monad.Trans.Resource+import Control.Monad.IO.Class (liftIO)++import System.IO.SafeWrite (syncFile)++-- | Write to file |finalname| using a temporary file and atomic move.+--+-- The file is only written if the sink runs to completion without errors. Any+-- form of early termination will cause the output to be removed.+safeSinkFile :: (MonadResource m) =>+ FilePath -- ^ Final filename+ -> C.Sink B.ByteString m ()+safeSinkFile finalname = C.bracketP+ acquire + deleteTempOnError+ writeMove+ where+ acquire = do+ (tname, th) <- openTempFile (takeDirectory finalname) finalname+ completed <- newIORef False+ return (tname, th, completed)+ writeMove (tname, th, completed) = do+ CC.sinkHandle th+ liftIO $ do+ hClose th+ syncFile tname+ renameFile tname finalname+ writeIORef completed True+ deleteTempOnError (tname, th, completed) = do+ completed' <- readIORef completed+ unless completed' $ do+ hClose th+ removeFile tname+
README.md view
@@ -8,15 +8,30 @@ 3. Close and sync the file. 4. Atomically rename the file to its final destination. + ## Example +Direct use:+ import System.IO.SafeWrite ... main = do withOutputFile "output.txt" $ \hout -> do hPutStrLn hout "Hello World" +Through [conduit](https://www.stackage.org/package/conduit): + import qualified Data.Conduit as C+ import Data.Conduit ((.|))+ import Data.Conduit.SafeWrite+ + main = C.runConduitRes $+ C.yield "Hello World" .| safeSinkFile "hello.txt"++In any case, only successful termination of the process will result in the+output file being written. Early termination by throwing an exception will+cause the temporary file to be removed and no output will be produced.+ ## Author -Luis Pedro Coelho [Email](mailto:luis@luispedro.org)+Luis Pedro Coelho | [Email](mailto:luis@luispedro.org) | [Twitter](https://twitter.com/luispedrocoelho)
System/IO/SafeWrite/Tests.hs view
@@ -1,17 +1,21 @@-{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes #-} module Main where -import Test.Framework.TH-import Test.HUnit-import Test.Framework.Providers.HUnit+import Test.Framework.TH+import Test.HUnit+import Test.Framework.Providers.HUnit -import Control.Exception (throwIO)-import System.IO.Error (isDoesNotExistError, catchIOError)-import System.Directory (doesFileExist, removeFile)-import System.IO (hPutStrLn)+import qualified Data.Conduit as C+import Data.Conduit ((.|))+import Control.Monad.IO.Class (liftIO)+import Control.Exception (throwIO)+import System.IO.Error (isDoesNotExistError, catchIOError)+import System.Directory (doesFileExist, removeFile)+import System.IO (hPutStrLn) import System.IO.SafeWrite+import Data.Conduit.SafeWrite main :: IO () main = do@@ -39,3 +43,36 @@ throwIO $ userError "Something bad happened") `catchIOError` \_ -> return () (not <$> doesFileExist outname) >>= assertBool "Output file was created despite exception being raised" +case_no_intermediate_output = do+ withOutputFile outname $ \h -> do+ hPutStrLn h "Hello Worlld"+ partial <- doesFileExist outname+ assertBool "Partial file should not exist before internal action ends" (not partial)+ (doesFileExist outname) >>= assertBool "Output file was not created"+ removeFile outname++case_conduit_create_output = do+ C.runConduitRes $+ C.yield "Hello World" .| safeSinkFile outname+ (doesFileExist outname) >>= assertBool "Output file was not created"+ removeFile outname++case_conduit_not_create_on_exception = do+ (C.runConduitRes $+ (do+ C.yield "Hello World"+ liftIO . throwIO $ userError "Something bad happened"+ ) .| safeSinkFile outname+ ) `catchIOError` \_ -> return ()+ (not <$> doesFileExist outname) >>= assertBool "Output file was created despite exception being raised"++case_conduit_no_intermediate_output = do+ C.runConduitRes $+ (do+ C.yield "Hello World"+ liftIO $ do+ partial <- doesFileExist outname+ assertBool "Partial file should not exist before conduit upstream ends" (not partial)+ ) .| safeSinkFile outname+ (doesFileExist outname) >>= assertBool "Output file was not created at the end of conduit processing"+ removeFile outname
safeio.cabal view
@@ -1,5 +1,5 @@ name: safeio-version: 0.0.1.0+version: 0.0.2.0 synopsis: Write output to disk atomically description: This package implements utilities to perform atomic output so as to avoid the problem of partial intermediate files.@@ -15,12 +15,17 @@ library default-language: Haskell2010- exposed-modules: System.IO.SafeWrite+ exposed-modules: System.IO.SafeWrite,+ Data.Conduit.SafeWrite ghc-options: -Wall build-depends: base > 4 && < 5,+ bytestring,+ conduit >= 1.0,+ conduit-combinators, directory, filepath,+ resourcet, unix Test-Suite safeiotest@@ -32,8 +37,13 @@ hs-source-dirs: . build-depends: base > 4 && < 5,+ base > 4 && < 5,+ bytestring,+ conduit >= 1.0,+ conduit-combinators, directory, filepath,+ resourcet, unix, HUnit, test-framework,