Hello community, here is the log from the commit of package ghc-unliftio for openSUSE:Factory checked in at 2019-07-29 17:27:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ghc-unliftio (Old) and /work/SRC/openSUSE:Factory/.ghc-unliftio.new.4126 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ghc-unliftio" Mon Jul 29 17:27:10 2019 rev:8 rq:715423 version:0.2.12 Changes: -------- --- /work/SRC/openSUSE:Factory/ghc-unliftio/ghc-unliftio.changes 2019-06-12 13:18:58.240560658 +0200 +++ /work/SRC/openSUSE:Factory/.ghc-unliftio.new.4126/ghc-unliftio.changes 2019-07-29 17:27:15.726282871 +0200 @@ -1,0 +2,18 @@ +Sat Jul 13 02:01:41 UTC 2019 - psim...@suse.com + +- Update unliftio to version 0.2.12. + ## 0.2.12 + + * Dropped support for ghc-7.8 + * Addition of `UnliftIO.IO.File` module and atomic+durable file writes: + + * `writeBinaryFile` + * `writeBinaryFileAtomic` + * `writeBinaryFileDurable` + * `writeBinaryFileDurableAtomic` + * `withBinaryFileAtomic` + * `withBinaryFileDurable` + * `withBinaryFileDurableAtomic` + * `ensureFileDurable` + +------------------------------------------------------------------- Old: ---- unliftio-0.2.11.tar.gz New: ---- unliftio-0.2.12.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ghc-unliftio.spec ++++++ --- /var/tmp/diff_new_pack.bUWrLF/_old 2019-07-29 17:27:18.554281825 +0200 +++ /var/tmp/diff_new_pack.bUWrLF/_new 2019-07-29 17:27:18.582281814 +0200 @@ -19,7 +19,7 @@ %global pkg_name unliftio %bcond_with tests Name: ghc-%{pkg_name} -Version: 0.2.11 +Version: 0.2.12 Release: 0 Summary: The MonadUnliftIO typeclass for unlifting monads to IO (batteries included) License: MIT @@ -28,6 +28,7 @@ Source0: https://hackage.haskell.org/package/%{pkg_name}-%{version}/%{pkg_name}-%{version}.tar.gz BuildRequires: ghc-Cabal-devel BuildRequires: ghc-async-devel +BuildRequires: ghc-bytestring-devel BuildRequires: ghc-deepseq-devel BuildRequires: ghc-directory-devel BuildRequires: ghc-filepath-devel ++++++ unliftio-0.2.11.tar.gz -> unliftio-0.2.12.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unliftio-0.2.11/ChangeLog.md new/unliftio-0.2.12/ChangeLog.md --- old/unliftio-0.2.11/ChangeLog.md 2019-06-07 12:37:42.000000000 +0200 +++ new/unliftio-0.2.12/ChangeLog.md 2019-07-12 06:21:01.000000000 +0200 @@ -1,5 +1,19 @@ # Changelog for unliftio +## 0.2.12 + +* Dropped support for ghc-7.8 +* Addition of `UnliftIO.IO.File` module and atomic+durable file writes: + + * `writeBinaryFile` + * `writeBinaryFileAtomic` + * `writeBinaryFileDurable` + * `writeBinaryFileDurableAtomic` + * `withBinaryFileAtomic` + * `withBinaryFileDurable` + * `withBinaryFileDurableAtomic` + * `ensureFileDurable` + ## 0.2.11 * Deprecate `forkWithUnmask` in favor of the newly added `forkIOWithUnmask` to diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unliftio-0.2.11/cbits/file-posix.c new/unliftio-0.2.12/cbits/file-posix.c --- old/unliftio-0.2.11/cbits/file-posix.c 1970-01-01 01:00:00.000000000 +0100 +++ new/unliftio-0.2.12/cbits/file-posix.c 2019-07-12 06:21:01.000000000 +0200 @@ -0,0 +1,34 @@ +#include <fcntl.h> +#include <unistd.h> +#include <sys/stat.h> + +int unliftio_o_tmpfile( void ) +{ +#ifdef __O_TMPFILE + return __O_TMPFILE; +#else + return 0; +#endif +} + +int unliftio_at_fdcwd( void ) +{ + return AT_FDCWD; +} + +int unliftio_at_symlink_follow( void ) +{ + return AT_SYMLINK_FOLLOW; +} + + +int unliftio_s_irusr( void ) +{ + return S_IRUSR; +} + +int unliftio_s_iwusr( void ) +{ + return S_IWUSR; +} + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unliftio-0.2.11/src/UnliftIO/IO/File/Posix.hs new/unliftio-0.2.12/src/UnliftIO/IO/File/Posix.hs --- old/unliftio-0.2.11/src/UnliftIO/IO/File/Posix.hs 1970-01-01 01:00:00.000000000 +0100 +++ new/unliftio-0.2.12/src/UnliftIO/IO/File/Posix.hs 2019-07-12 06:21:01.000000000 +0200 @@ -0,0 +1,582 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE ForeignFunctionInterface #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ViewPatterns #-} +module UnliftIO.IO.File.Posix + ( withBinaryFileDurable + , withBinaryFileDurableAtomic + , withBinaryFileAtomic + , ensureFileDurable + ) + where + +#if __GLASGOW_HASKELL__ < 710 +import Control.Applicative +#endif +import Control.Monad (forM_, guard, unless, void, when) +import Control.Monad.IO.Unlift +import Data.Bits (Bits, (.|.)) +import Data.ByteString (ByteString) +import Data.Maybe (fromMaybe) +import Data.Typeable (cast) +import Foreign (allocaBytes) +import Foreign.C (CInt(..), throwErrnoIfMinus1, throwErrnoIfMinus1Retry, + throwErrnoIfMinus1Retry_) +import GHC.IO.Device (IODeviceType(RegularFile)) +import qualified GHC.IO.Device as Device +import GHC.IO.Exception (IOErrorType(UnsupportedOperation)) +import qualified GHC.IO.FD as FD +import qualified GHC.IO.Handle.FD as HandleFD +import qualified GHC.IO.Handle.Types as HandleFD (Handle(..), Handle__(..)) +import System.Directory (removeFile) +import System.FilePath (takeDirectory, takeFileName) +import System.IO (Handle, IOMode(..), SeekMode(..), hGetBuf, hPutBuf, + openBinaryTempFile) +import System.IO.Error (ioeGetErrorType, isAlreadyExistsError, + isDoesNotExistError) +import qualified System.Posix.Files as Posix +import System.Posix.Internals (CFilePath, c_close, c_safe_open, withFilePath) +import System.Posix.Types (CMode(..), Fd(..), FileMode) +import UnliftIO.Exception +import UnliftIO.IO +import UnliftIO.MVar + +-- NOTE: System.Posix.Internal doesn't re-export this constants so we have to +-- recreate-them here + +newtype CFlag = + CFlag CInt + deriving (Eq, Show, Bits) + +foreign import ccall unsafe "HsBase.h __hscore_o_rdonly" o_RDONLY :: CFlag +foreign import ccall unsafe "HsBase.h __hscore_o_wronly" o_WRONLY :: CFlag +foreign import ccall unsafe "HsBase.h __hscore_o_rdwr" o_RDWR :: CFlag +foreign import ccall unsafe "HsBase.h __hscore_o_append" o_APPEND :: CFlag +foreign import ccall unsafe "HsBase.h __hscore_o_creat" o_CREAT :: CFlag +foreign import ccall unsafe "HsBase.h __hscore_o_noctty" o_NOCTTY :: CFlag + +-- After here, we have our own imports + +-- On non-Linux operating systems that do not support `O_TMPFILE` the value of +-- `o_TMPFILE` will be 0, which is then used to fallback onto a different +-- implementation of temporary files. +foreign import ccall unsafe "file-posix.c unliftio_o_tmpfile" o_TMPFILE :: CFlag + + +-- | Whenever Operating System does not support @O_TMPFILE@ flag and anonymous +-- temporary files then `o_TMPFILE` flag will be set to @0@ +o_TMPFILE_not_supported :: CFlag +o_TMPFILE_not_supported = CFlag 0 + +newtype CAt = CAt + { unCAt :: CInt + } deriving (Eq, Show, Bits) + +foreign import ccall unsafe "file-posix.c unliftio_at_fdcwd" at_FDCWD :: CAt +foreign import ccall unsafe "file-posix.c unliftio_at_symlink_follow" at_SYMLINK_FOLLOW :: CAt +foreign import ccall unsafe "file-posix.c unliftio_s_irusr" s_IRUSR :: CMode +foreign import ccall unsafe "file-posix.c unliftio_s_iwusr" s_IWUSR :: CMode + +c_open :: CFilePath -> CFlag -> CMode -> IO CInt +c_open fp (CFlag flags) = c_safe_open fp flags + +foreign import ccall safe "fcntl.h openat" + c_safe_openat :: CInt -> CFilePath -> CInt -> CMode -> IO CInt + +c_openat :: DirFd -> CFilePath -> CFlag -> CMode -> IO CInt +c_openat (DirFd (Fd fd)) fp (CFlag flags) = c_safe_openat fd fp flags + +foreign import ccall safe "fcntl.h renameat" + c_safe_renameat :: CInt -> CFilePath -> CInt -> CFilePath -> IO CInt + +c_renameat :: DirFd -> CFilePath -> DirFd -> CFilePath -> IO CInt +c_renameat (DirFd (Fd fdFrom)) cFpFrom (DirFd (Fd fdTo)) cFpTo = + c_safe_renameat fdFrom cFpFrom fdTo cFpTo + +foreign import ccall safe "unistd.h fsync" + c_safe_fsync :: CInt -> IO CInt + +c_fsync :: Fd -> IO CInt +c_fsync (Fd fd) = c_safe_fsync fd + +foreign import ccall safe "unistd.h linkat" + c_safe_linkat :: CInt -> CFilePath -> CInt -> CFilePath -> CInt -> IO CInt + +c_linkat :: CAt -> CFilePath -> Either DirFd CAt -> CFilePath -> CAt -> IO CInt +c_linkat cat oldPath eNewDir newPath (CAt flags) = + c_safe_linkat (unCAt cat) oldPath newDir newPath flags + where + unFd (Fd fd) = fd + newDir = either (unFd . unDirFd) unCAt eNewDir + +std_flags, output_flags, read_flags, write_flags, rw_flags, + append_flags :: CFlag +std_flags = o_NOCTTY +output_flags = std_flags .|. o_CREAT +read_flags = std_flags .|. o_RDONLY +write_flags = output_flags .|. o_WRONLY +rw_flags = output_flags .|. o_RDWR +append_flags = write_flags .|. o_APPEND + +ioModeToFlags :: IOMode -> CFlag +ioModeToFlags iomode = + case iomode of + ReadMode -> read_flags + WriteMode -> write_flags + ReadWriteMode -> rw_flags + AppendMode -> append_flags + +newtype DirFd = DirFd + { unDirFd :: Fd + } + +-- | Returns a low-level file descriptor for a directory path. This function +-- exists given the fact that 'openFile' does not work with directories. +-- +-- If you use this function, make sure you are working on a masked state, +-- otherwise async exceptions may leave file descriptors open. +openDir :: MonadIO m => FilePath -> m Fd +openDir fp + -- TODO: Investigate what is the situation with Windows FS in regards to non_blocking + -- NOTE: File operations _do not support_ non_blocking on various kernels, more + -- info can be found here: https://ghc.haskell.org/trac/ghc/ticket/15153 + = + liftIO $ + withFilePath fp $ \cFp -> + Fd <$> + throwErrnoIfMinus1Retry + "openDir" + (c_open cFp (ioModeToFlags ReadMode) 0o660) + +-- | Closes a 'Fd' that points to a Directory. +closeDirectory :: MonadIO m => DirFd -> m () +closeDirectory (DirFd (Fd dirFd)) = + liftIO $ + throwErrnoIfMinus1Retry_ "closeDirectory" $ c_close dirFd + +-- | Executes the low-level C function fsync on a C file descriptor +fsyncFileDescriptor + :: MonadIO m + => String -- ^ Meta-description for error messages + -> Fd -- ^ C File Descriptor + -> m () +fsyncFileDescriptor name fd = + liftIO $ void $ throwErrnoIfMinus1 ("fsync - " ++ name) $ c_fsync fd + +-- | Call @fsync@ on the file handle. Accepts an arbitary string for error reporting. +fsyncFileHandle :: String -> Handle -> IO () +fsyncFileHandle fname hdl = withHandleFd hdl (fsyncFileDescriptor (fname ++ "/File")) + + +-- | Call @fsync@ on the opened directory file descriptor. Accepts an arbitary +-- string for error reporting. +fsyncDirectoryFd :: String -> DirFd -> IO () +fsyncDirectoryFd fname = fsyncFileDescriptor (fname ++ "/Directory") . unDirFd + + +-- | Opens a file from a directory, using this function in favour of a regular +-- 'openFile' guarantees that any file modifications are kept in the same +-- directory where the file was opened. An edge case scenario is a mount +-- happening in the directory where the file was opened while your program is +-- running. +-- +-- If you use this function, make sure you are working on an masked state, +-- otherwise async exceptions may leave file descriptors open. +-- +openFileFromDir :: MonadIO m => DirFd -> FilePath -> IOMode -> m Handle +openFileFromDir dirFd filePath@(takeFileName -> fileName) iomode = + liftIO $ + withFilePath fileName $ \cFileName -> + bracketOnError + (do fileFd <- + throwErrnoIfMinus1Retry "openFileFromDir" $ + c_openat dirFd cFileName (ioModeToFlags iomode) 0o666 + {- Can open directory with read only -} + FD.mkFD + fileFd + iomode + Nothing {- no stat -} + False {- not a socket -} + False {- non_blocking -} + `onException` + c_close fileFd) + (liftIO . Device.close . fst) + (\(fD, fd_type) + -- we want to truncate() if this is an open in WriteMode, but only if the + -- target is a RegularFile. ftruncate() fails on special files like + -- /dev/null. + -> do + when (iomode == WriteMode && fd_type == RegularFile) $ + Device.setSize fD 0 + HandleFD.mkHandleFromFD fD fd_type filePath iomode False Nothing) + + +-- | Similar to `openFileFromDir`, but will open an anonymous (nameless) +-- temporary file in the supplied directory +openAnonymousTempFileFromDir :: + MonadIO m => + Maybe DirFd + -- ^ If a file descriptor is given for the directory where the target file is/will be + -- located in, then it will be used for opening an anonymous file. Otherwise + -- anonymous will be opened unattached to any file path. + -> FilePath + -- ^ File path of the target file that we are working on. + -> IOMode + -> m Handle +openAnonymousTempFileFromDir mDirFd filePath iomode = + liftIO $ + case mDirFd of + Just dirFd -> withFilePath "." (openAnonymousWith . c_openat dirFd) + Nothing -> + withFilePath (takeDirectory filePath) (openAnonymousWith . c_open) + where + fdName = "openAnonymousTempFileFromDir - " ++ filePath + ioModeToTmpFlags :: IOMode -> CFlag + ioModeToTmpFlags = + \case + ReadMode -> o_RDWR -- It is an error to create a O_TMPFILE with O_RDONLY + ReadWriteMode -> o_RDWR + _ -> o_WRONLY + openAnonymousWith fopen = + bracketOnError + (do fileFd <- + throwErrnoIfMinus1Retry "openAnonymousTempFileFromDir" $ + fopen (o_TMPFILE .|. ioModeToTmpFlags iomode) (s_IRUSR .|. s_IWUSR) + FD.mkFD + fileFd + iomode + Nothing {- no stat -} + False {- not a socket -} + False {- non_blocking -} + `onException` + c_close fileFd) + (liftIO . Device.close . fst) + (\(fD, fd_type) -> + HandleFD.mkHandleFromFD fD fd_type fdName iomode False Nothing) + + +atomicDurableTempFileRename :: + DirFd -> Maybe FileMode -> Handle -> Maybe FilePath -> FilePath -> IO () +atomicDurableTempFileRename dirFd mFileMode tmpFileHandle mTmpFilePath filePath = do + fsyncFileHandle "atomicDurableTempFileCreate" tmpFileHandle + -- at this point we know that the content has been persisted to the storage it + -- is safe to do the atomic move/replace + let eTmpFile = maybe (Left tmpFileHandle) Right mTmpFilePath + atomicTempFileRename (Just dirFd) mFileMode eTmpFile filePath + -- Important to close the handle, so the we can fsync the directory + hClose tmpFileHandle + -- file path is updated, now we can fsync the directory + fsyncDirectoryFd "atomicDurableTempFileCreate" dirFd + + +-- | There will be an attempt to atomically convert an invisible temporary file +-- into a target file at the supplied file path. In case when there is already a +-- file at that file path, a new visible temporary file will be created in the +-- same folder and then atomically renamed into the target file path, replacing +-- any existing file. This is necessary since `c_safe_linkat` cannot replace +-- files atomically and we have to fall back onto `c_safe_renameat`. This should +-- not be a problem in practice, since lifetime of such visible file is +-- extremely short and it will be cleaned up regardless of the outcome of the +-- rename. +-- +-- It is important to note, that whenever a file descriptor for the containing +-- directory is supplied, renaming and linking will be done in its context, +-- thus allowing to do proper fsyncing if durability is necessary. +-- +-- __NOTE__: this function will work only on Linux. +-- +atomicTempFileCreate :: + Maybe DirFd + -- ^ Possible handle for the directory where the target file is located. Which + -- means that the file is already in that directory, just without a name. In other + -- words it was opened before with `openAnonymousTempFileFromDir` + -> Maybe FileMode + -- ^ If file permissions are supplied they will be set on the new file prior + -- to atomic rename. + -> Handle + -- ^ Handle to the anonymous temporary file created with `c_openat` and + -- `o_TMPFILE` + -> FilePath + -- ^ File path for the target file. + -> IO () +atomicTempFileCreate mDirFd mFileMode tmpFileHandle filePath = + withHandleFd tmpFileHandle $ \fd@(Fd cFd) -> + withFilePath ("/proc/self/fd/" ++ show cFd) $ \cFromFilePath -> + withFilePath filePathName $ \cToFilePath -> do + let fileMode = fromMaybe Posix.stdFileMode mFileMode + -- work around for the glibc bug: https://sourceware.org/bugzilla/show_bug.cgi?id=17523 + Posix.setFdMode fd fileMode + let safeLink which to = + throwErrnoIfMinus1Retry_ + ("atomicFileCreate - c_safe_linkat - " ++ which) $ + -- see `man linkat` and `man openat` for more info + c_linkat at_FDCWD cFromFilePath cDirFd to at_SYMLINK_FOLLOW + eExc <- + tryJust (guard . isAlreadyExistsError) $ + safeLink "anonymous" cToFilePath + case eExc of + Right () -> pure () + Left () -> + withBinaryTempFileFor filePath $ \visTmpFileName visTmpFileHandle -> do + hClose visTmpFileHandle + removeFile visTmpFileName + case mDirFd of + Nothing -> do + withFilePath visTmpFileName (safeLink "visible") + Posix.rename visTmpFileName filePath + Just dirFd -> + withFilePath (takeFileName visTmpFileName) $ \cVisTmpFile -> do + safeLink "visible" cVisTmpFile + throwErrnoIfMinus1Retry_ + "atomicFileCreate - c_safe_renameat" $ + c_renameat dirFd cVisTmpFile dirFd cToFilePath + where + (cDirFd, filePathName) = + case mDirFd of + Nothing -> (Right at_FDCWD, filePath) + Just dirFd -> (Left dirFd, takeFileName filePath) + +atomicTempFileRename :: + Maybe DirFd + -- ^ Possible handle for the directory where the target file is located. + -> Maybe FileMode + -- ^ If file permissions are supplied they will be set on the new file prior + -- to atomic rename. + -> Either Handle FilePath + -- ^ Temporary file. If a handle is supplied, it means it was opened with + -- @O_TMPFILE@ flag and thus we are on the Linux OS and can safely call + -- `atomicTempFileCreate` + -> FilePath + -- ^ File path for the target file. Whenever `DirFd` is supplied, it must be + -- the containgin directory fo this file, but that invariant is not enforced + -- within this function. + -> IO () +atomicTempFileRename mDirFd mFileMode eTmpFile filePath = + case eTmpFile of + Left tmpFileHandle -> + atomicTempFileCreate mDirFd mFileMode tmpFileHandle filePath + Right tmpFilePath -> do + forM_ mFileMode $ \fileMode -> Posix.setFileMode tmpFilePath fileMode + case mDirFd of + Nothing -> Posix.rename tmpFilePath filePath + Just dirFd -> + withFilePath (takeFileName filePath) $ \cToFilePath -> + withFilePath (takeFileName tmpFilePath) $ \cTmpFilePath -> + throwErrnoIfMinus1Retry_ "atomicFileCreate - c_safe_renameat" $ + c_renameat dirFd cTmpFilePath dirFd cToFilePath + + +withDirectory :: MonadUnliftIO m => FilePath -> (DirFd -> m a) -> m a +withDirectory dirPath = bracket (DirFd <$> openDir dirPath) closeDirectory + +withFileInDirectory :: + MonadUnliftIO m => DirFd -> FilePath -> IOMode -> (Handle -> m a) -> m a +withFileInDirectory dirFd filePath iomode = + bracket (openFileFromDir dirFd filePath iomode) hClose + + +-- | Create a temporary file for a matching possibly exiting target file that +-- will be replaced in the future. Temporary file is meant to be renamed +-- afterwards, thus it is only deleted upon error. +-- +-- __Important__: Temporary file is not removed and file handle is not closed if +-- there was no exception thrown by the supplied action. +withBinaryTempFileFor :: + MonadUnliftIO m + => FilePath + -- ^ "For" file. It may exist or may not. + -> (FilePath -> Handle -> m a) + -> m a +withBinaryTempFileFor filePath action = + bracketOnError + (liftIO (openBinaryTempFile dirPath tmpFileName)) + (\(tmpFilePath, tmpFileHandle) -> + hClose tmpFileHandle >> liftIO (tryIO (removeFile tmpFilePath))) + (uncurry action) + where + dirPath = takeDirectory filePath + fileName = takeFileName filePath + tmpFileName = "." ++ fileName ++ ".tmp" + +-- | Returns `Nothing` if anonymous temporary file is not supported by the OS or +-- the underlying file system can't handle that feature. +withAnonymousBinaryTempFileFor :: + MonadUnliftIO m + => Maybe DirFd + -- ^ It is possible to open the temporary file in the context of a directory, + -- in such case supply its file descriptor. i.e. @openat@ will be used instead + -- of @open@ + -> FilePath + -- ^ "For" file. The file may exist or may not. + -> IOMode + -> (Handle -> m a) + -> m (Maybe a) +withAnonymousBinaryTempFileFor mDirFd filePath iomode action + | o_TMPFILE == o_TMPFILE_not_supported = pure Nothing + | otherwise = + trySupported $ + bracket (openAnonymousTempFileFromDir mDirFd filePath iomode) hClose action + where + trySupported m = + tryIO m >>= \case + Right res -> pure $ Just res + Left exc + | ioeGetErrorType exc == UnsupportedOperation -> pure Nothing + Left exc -> throwIO exc + +withNonAnonymousBinaryTempFileFor :: + MonadUnliftIO m + => Maybe DirFd + -- ^ It is possible to open the temporary file in the context of a directory, + -- in such case supply its file descriptor. i.e. @openat@ will be used instead + -- of @open@ + -> FilePath + -- ^ "For" file. The file may exist or may not. + -> IOMode + -> (FilePath -> Handle -> m a) + -> m a +withNonAnonymousBinaryTempFileFor mDirFd filePath iomode action = + withBinaryTempFileFor filePath $ \tmpFilePath tmpFileHandle -> do + hClose tmpFileHandle + case mDirFd of + Nothing -> withBinaryFile tmpFilePath iomode (action tmpFilePath) + Just dirFd -> withFileInDirectory dirFd tmpFilePath iomode (action tmpFilePath) + +-- | Copy the contents of the file into the handle, but only if that file exists +-- and either `ReadWriteMode` or `AppendMode` is specified. Returned are the +-- file permissions of the original file so it can be set later when original +-- gets overwritten atomically. +copyFileHandle :: + MonadUnliftIO f => IOMode -> FilePath -> Handle -> f (Maybe FileMode) +copyFileHandle iomode fromFilePath toHandle = + either (const Nothing) Just <$> + tryJust + (guard . isDoesNotExistError) + (do fileStatus <- liftIO $ Posix.getFileStatus fromFilePath + -- Whenever we are not overwriting an existing file, we also need a + -- copy of the file's contents + unless (iomode == WriteMode) $ do + withBinaryFile fromFilePath ReadMode (`copyHandleData` toHandle) + unless (iomode == AppendMode) $ hSeek toHandle AbsoluteSeek 0 + -- Get the copy of source file permissions, but only whenever it exists + pure $ Posix.fileMode fileStatus) + + +-- This is a copy of the internal function from `directory-1.3.3.2`. It became +-- available only in directory-1.3.3.0 and is still internal, hence the +-- duplication. +copyHandleData :: MonadIO m => Handle -> Handle -> m () +copyHandleData hFrom hTo = liftIO $ allocaBytes bufferSize go + where + bufferSize = 131072 -- 128 KiB, as coreutils `cp` uses as of May 2014 (see ioblksize.h) + go buffer = do + count <- hGetBuf hFrom buffer bufferSize + when (count > 0) $ do + hPutBuf hTo buffer count + go buffer + +-- | Thread safe access to the file descriptor in the file handle +withHandleFd :: Handle -> (Fd -> IO a) -> IO a +withHandleFd h cb = + case h of + HandleFD.FileHandle _ mv -> + withMVar mv $ \HandleFD.Handle__{HandleFD.haDevice = dev} -> + case cast dev of + Just fd -> cb $ Fd $ FD.fdFD fd + Nothing -> error "withHandleFd: not a file handle" + HandleFD.DuplexHandle {} -> error "withHandleFd: not a file handle" + +-- | See `ensureFileDurable` +ensureFileDurable :: MonadIO m => FilePath -> m () +ensureFileDurable filePath = + liftIO $ + withDirectory (takeDirectory filePath) $ \dirFd -> + withFileInDirectory dirFd filePath ReadMode $ \fileHandle -> + liftIO $ do + fsyncFileHandle "ensureFileDurablePosix" fileHandle + -- NOTE: Here we are purposefully not fsyncing the directory if the file fails to fsync + fsyncDirectoryFd "ensureFileDurablePosix" dirFd + + + +-- | See `withBinaryFileDurable` +withBinaryFileDurable :: + MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r +withBinaryFileDurable filePath iomode action = + case iomode of + ReadMode + -- We do not need to consider durable operations when we are in a + -- 'ReadMode', so we can use a regular `withBinaryFile` + -> withBinaryFile filePath iomode action + _ {- WriteMode, ReadWriteMode, AppendMode -} + -> + withDirectory (takeDirectory filePath) $ \dirFd -> + withFileInDirectory dirFd filePath iomode $ \tmpFileHandle -> do + res <- action tmpFileHandle + liftIO $ do + fsyncFileHandle "withBinaryFileDurablePosix" tmpFileHandle + -- NOTE: Here we are purposefully not fsyncing the directory if the file fails to fsync + fsyncDirectoryFd "withBinaryFileDurablePosix" dirFd + pure res + +-- | See `withBinaryFileDurableAtomic` +withBinaryFileDurableAtomic :: + MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r +withBinaryFileDurableAtomic filePath iomode action = + case iomode of + ReadMode + -- We do not need to consider an atomic operation when we are in a + -- 'ReadMode', so we can use a regular `withBinaryFile` + -> withBinaryFile filePath iomode action + _ {- WriteMode, ReadWriteMode, AppendMode -} + -> + withDirectory (takeDirectory filePath) $ \dirFd -> do + mRes <- withAnonymousBinaryTempFileFor (Just dirFd) filePath iomode $ + durableAtomicAction dirFd Nothing + case mRes of + Just res -> pure res + Nothing -> + withNonAnonymousBinaryTempFileFor (Just dirFd) filePath iomode $ \tmpFilePath -> + durableAtomicAction dirFd (Just tmpFilePath) + where + durableAtomicAction dirFd mTmpFilePath tmpFileHandle = do + mFileMode <- copyFileHandle iomode filePath tmpFileHandle + res <- action tmpFileHandle + liftIO $ + atomicDurableTempFileRename + dirFd + mFileMode + tmpFileHandle + mTmpFilePath + filePath + pure res + +-- | See `withBinaryFileAtomic` +withBinaryFileAtomic :: + MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r +withBinaryFileAtomic filePath iomode action = + case iomode of + ReadMode + -- We do not need to consider an atomic operation when we are in a + -- 'ReadMode', so we can use a regular `withBinaryFile` + -> withBinaryFile filePath iomode action + _ {- WriteMode, ReadWriteMode, AppendMode -} + -> do + mRes <- + withAnonymousBinaryTempFileFor Nothing filePath iomode $ + atomicAction Nothing + case mRes of + Just res -> pure res + Nothing -> + withNonAnonymousBinaryTempFileFor Nothing filePath iomode $ \tmpFilePath -> + atomicAction (Just tmpFilePath) + where + atomicAction mTmpFilePath tmpFileHandle = do + let eTmpFile = maybe (Left tmpFileHandle) Right mTmpFilePath + mFileMode <- copyFileHandle iomode filePath tmpFileHandle + res <- action tmpFileHandle + liftIO $ atomicTempFileRename Nothing mFileMode eTmpFile filePath + pure res + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unliftio-0.2.11/src/UnliftIO/IO/File.hs new/unliftio-0.2.12/src/UnliftIO/IO/File.hs --- old/unliftio-0.2.11/src/UnliftIO/IO/File.hs 1970-01-01 01:00:00.000000000 +0100 +++ new/unliftio-0.2.12/src/UnliftIO/IO/File.hs 2019-07-12 06:21:01.000000000 +0200 @@ -0,0 +1,278 @@ +{-# LANGUAGE CPP #-} +{-| + +== Rationale + +This module offers functions to handle files that offer better durability and/or +atomicity. + +== When to use functions in this module? + +Given the usage of this functions comes at a cost in performance, it is important to +consider what are the use cases that are ideal for each of the functions. + +=== Not Durable and not Atomic + +For this use case, you want to use the regular functions: + +* 'withBinaryFile' +* 'writeBinaryFile' + +The regular use case for this scenario happens when your program is dealing with outputs +that are never going to be consumed again by your program. For example, imagine you have a +program that generates sales reports for the last month, this is a report that can be +generated quickly; you don't really care if the output file gets corrupted or lost at one +particular execution of your program given that is cheap to execute the data export +program a second time. In other words, your program doesn't /rely/ on the data contained +in this file in order to work. + +=== Atomic but not Durable + + Imagine a scenario where your program builds a temporary file that serves as an +intermediate step to a bigger task, like Object files (@.o@) in a compilation process. The +program will use an existing @.o@ file if it is present, or it will build one from scratch +if it is not. The file is not really required, but if it is present, it *must* be valid +and consistent. In this situation, you care about atomicity, but not durability. You can +use the functions for such scenario: + +* 'withBinaryFileAtomic' +* 'writeBinaryFileAtomic' + +__Note__ - there is a peculiar difference between regular file writing functionality and +the one that is done atomically. Even if the orignal file is removed while it is being +modified, because of atomicity, it will be restored with all modifications, if any. The +reason for this is because a copy of the file was made prior to modifications and at the +end the existing is atomically replaced. An important consequence of this fact is that +whenever the folder containing the file which is being modified is removed, all bets are +off and all atomic functions will result in an exception. + +=== Durable but not Atomic + +For this use case, you want to use the functions: + +* 'withBinaryFileDurable' +* 'writeBinaryFileDurable' + +The regular use case for this scenario happens when your program deals with file +modifications that must be guaranteed to be durable, but you don't care that changes are +consistent. If you use this function, more than likely your program is ensuring +consistency guarantees through other means, for example, SQLite uses the Write Ahead Log +(WAL) algorithm to ensure changes are atomic at an application level. + +=== Durable and Atomic + +For this use case, you can use the functions: + +* 'withBinaryFileDurableAtomic' +* 'writeBinaryFileDurableAtomic' + +The regular use case for this scenario happens when you want to ensure that after a +program is executed, the modifications done to a file are guaranteed to be saved, and also +that changes are rolled-back in case there is a failure (e.g. hard reboot, shutdown, +etc). + +-} +module UnliftIO.IO.File + ( writeBinaryFile + , writeBinaryFileAtomic + , writeBinaryFileDurable + , writeBinaryFileDurableAtomic + , withBinaryFile + , withBinaryFileAtomic + , withBinaryFileDurable + , withBinaryFileDurableAtomic + , ensureFileDurable + ) + where + +import Data.ByteString as B (ByteString, writeFile) +import Control.Monad.IO.Unlift +import UnliftIO.IO (Handle, IOMode(..), withBinaryFile) + +#if WINDOWS + + +ensureFileDurable = (`seq` pure ()) + +writeBinaryFileDurable = writeBinaryFile +writeBinaryFileDurableAtomic = writeBinaryFile +writeBinaryFileAtomic = writeBinaryFile + +withBinaryFileDurable = withBinaryFile +withBinaryFileDurableAtomic = withBinaryFile +withBinaryFileAtomic = withBinaryFile + +#else + +import qualified Data.ByteString as B (hPut) +import qualified UnliftIO.IO.File.Posix as Posix + +ensureFileDurable = Posix.ensureFileDurable + +writeBinaryFileDurable fp bytes = + liftIO $ withBinaryFileDurable fp WriteMode (`B.hPut` bytes) +writeBinaryFileDurableAtomic fp bytes = + liftIO $ withBinaryFileDurableAtomic fp WriteMode (`B.hPut` bytes) +writeBinaryFileAtomic fp bytes = + liftIO $ withBinaryFileAtomic fp WriteMode (`B.hPut` bytes) + +withBinaryFileDurable = Posix.withBinaryFileDurable +withBinaryFileDurableAtomic = Posix.withBinaryFileDurableAtomic +withBinaryFileAtomic = Posix.withBinaryFileAtomic +#endif + +-- | After a file is closed, this function opens it again and executes @fsync()@ +-- internally on both the file and the directory that contains it. Note that this function +-- is intended to work around the non-durability of existing file APIs, as opposed to +-- being necessary for the API functions provided in this module. +-- +-- [The effectiveness of calling this function is +-- debatable](https://stackoverflow.com/questions/37288453/calling-fsync2-after-close2/50158433#50158433), +-- as it relies on internal implementation details at the Kernel level that might +-- change. We argue that, despite this fact, calling this function may bring benefits in +-- terms of durability. +-- +-- This function does not provide the same guarantee as if you would open and modify a +-- file using `withBinaryFileDurable` or `writeBinaryFileDurable`, since they ensure that +-- the @fsync()@ is called before the file is closed, so if possible use those instead. +-- +-- === Cross-Platform support +-- +-- This function is a noop on Windows platforms. +-- +-- @since 0.2.12 +ensureFileDurable :: MonadIO m => FilePath -> m () +-- Implementation is at the top of the module + + +-- | Similar to 'writeBinaryFile', but it also ensures that changes executed to the file +-- are guaranteed to be durable. It internally uses @fsync()@ and makes sure it +-- synchronizes the file on disk. +-- +-- === Cross-Platform support +-- +-- This function behaves the same as 'RIO.writeBinaryFile' on Windows platforms. +-- +-- @since 0.2.12 +writeBinaryFileDurable :: MonadIO m => FilePath -> ByteString -> m () +-- Implementation is at the top of the module + +-- | Similar to 'writeBinaryFile', but it also guarantes that changes executed to the file +-- are durable, also, in case of failure, the modified file is never going to get +-- corrupted. It internally uses @fsync()@ and makes sure it synchronizes the file on +-- disk. +-- +-- === Cross-Platform support +-- +-- This function behaves the same as 'writeBinaryFile' on Windows platforms. +-- +-- @since 0.2.12 +writeBinaryFileDurableAtomic :: MonadIO m => FilePath -> ByteString -> m () +-- Implementation is at the top of the module + +-- | Same as 'writeBinaryFileDurableAtomic', except it does not guarantee durability. +-- +-- === Cross-Platform support +-- +-- This function behaves the same as 'writeBinaryFile' on Windows platforms. +-- +-- @since 0.2.12 +writeBinaryFileAtomic :: MonadIO m => FilePath -> ByteString -> m () +-- Implementation is at the top of the module + +-- | Opens a file with the following guarantees: +-- +-- * It successfully closes the file in case of an asynchronous exception +-- +-- * It reliably saves the file in the correct directory; including edge case situations +-- like a different device being mounted to the current directory, or the current +-- directory being renamed to some other name while the file is being used. +-- +-- * It ensures durability by executing an @fsync()@ call before closing the file handle +-- +-- === Cross-Platform support +-- +-- This function behaves the same as 'System.IO.withBinaryFile' on Windows platforms. +-- +-- @since 0.2.12 +withBinaryFileDurable :: + MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r +-- Implementation is at the top of the module + +-- | Opens a file with the following guarantees: +-- +-- * It successfully closes the file in case of an asynchronous exception +-- +-- * It reliably saves the file in the correct directory; including edge case situations +-- like a different device being mounted to the current directory, or the current +-- directory being renamed to some other name while the file is being used. +-- +-- * It ensures durability by executing an @fsync()@ call before closing the file handle +-- +-- * It keeps all changes in a temporary file, and after it is closed it atomically moves +-- the temporary file to the original filepath, in case of catastrophic failure, the +-- original file stays unaffected. +-- +-- If you do not need durability but only atomicity, use `withBinaryFileAtomic` instead, +-- which is faster as it does not perform @fsync()@. +-- +-- __Important__ - Make sure not to close the `Handle`, it will be closed for you, +-- otherwise it will result in @invalid argument (Bad file descriptor)@ exception. +-- +-- === Performance Considerations +-- +-- When using a writable but non-truncating 'IOMode' (i.e. 'ReadWriteMode' and +-- 'AppendMode'), this function performs a copy operation of the specified input file to +-- guarantee the original file is intact in case of a catastrophic failure (no partial +-- writes). This approach may be prohibitive in scenarios where the input file is expected +-- to be large in size. +-- +-- === Cross-Platform support +-- +-- This function behaves the same as 'System.IO.withBinaryFile' on Windows platforms. +-- +-- @since 0.2.12 +withBinaryFileDurableAtomic :: + MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r +-- Implementation is at the top of the module + + +-- | Perform an action on a new or existing file at the destination file path. If +-- previously the file existed at the supplied file path then: +-- +-- * in case of `WriteMode` it will be overwritten +-- +-- * upon `ReadWriteMode` or `AppendMode` files contents will be copied over into a +-- temporary file, thus making sure no corruption can happen to an existing file upon any +-- failures, even catastrophic one, yet its contents are availble for modification. +-- +-- * There is nothing atomic about `ReadMode`, so no special treatment there. +-- +-- It is similar to `withBinaryFileDurableAtomic`, but without the durability part. It +-- means that all modification can still disappear after it has been succesfully written +-- due to some extreme event like an abrupt power loss, but the contents will not be +-- corrupted in case when the file write did not end successfully. +-- +-- The same performance caveats apply as for `withBinaryFileDurableAtomic` due to making a +-- copy of the content of existing files during non-truncating writes. +-- +-- __Important__ - Do not close the handle, otherwise it will result in @invalid argument +-- (Bad file descriptor)@ exception +-- +-- __Note__ - on Linux operating system and only with supported file systems an anonymous +-- temporary file will be used while working on the file (see @O_TMPFILE@ in @man +-- openat@). In case when such feature is not available or not supported a temporary file +-- ".target-file-nameXXX.ext.tmp", where XXX is some random number, will be created +-- alongside the target file in the same directory +-- +-- @since 0.2.12 +withBinaryFileAtomic :: + MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r +-- Implementation is at the top of the module + + +-- | Lifted version of `B.writeFile` +-- +-- @since 0.2.12 +writeBinaryFile :: MonadIO m => FilePath -> ByteString -> m () +writeBinaryFile fp = liftIO . B.writeFile fp diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unliftio-0.2.11/test/UnliftIO/IO/FileSpec.hs new/unliftio-0.2.12/test/UnliftIO/IO/FileSpec.hs --- old/unliftio-0.2.11/test/UnliftIO/IO/FileSpec.hs 1970-01-01 01:00:00.000000000 +0100 +++ new/unliftio-0.2.12/test/UnliftIO/IO/FileSpec.hs 2019-07-12 06:21:01.000000000 +0200 @@ -0,0 +1,201 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +module UnliftIO.IO.FileSpec where + +import Test.Hspec +-- Atomic/durable file writing is not supported on Windows. +#ifndef WINDOWS +import Control.Monad (forM_) +import Data.Bool (bool) +import System.FilePath ((</>)) +import Test.QuickCheck +import UnliftIO.Directory +import UnliftIO.Exception +import UnliftIO.IO +import UnliftIO.IO.File as File +import UnliftIO.Temporary (withSystemTempDirectory) +import qualified Data.ByteString as B +import qualified Data.ByteString.Builder as BB +import qualified Data.ByteString.Lazy as BL +#if __GLASGOW_HASKELL__ < 820 +import Data.Monoid +#endif + +data ExpectedException = + ExpectedException + deriving (Show) + +instance Exception ExpectedException + +spec :: Spec +spec = do + describe "ensureFileDurable" $ + it "ensures a file is durable with an fsync" $ + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> "ensure_file_durable" + writeFile fp "Hello World" + File.ensureFileDurable fp + contents <- B.readFile fp + contents `shouldBe` "Hello World" + withBinaryFileSpec False "withBinaryFile" withBinaryFile + writeBinaryFileSpec "writeBinaryFile" writeBinaryFile + -- Above two specs are validating the specs behavior by applying to + -- known good implementations + withBinaryFileSpec True "withBinaryFileAtomic" File.withBinaryFileAtomic + writeBinaryFileSpec "writeBinaryFileAtomic" File.writeBinaryFileAtomic + withBinaryFileSpec False "withBinaryFileDurable" File.withBinaryFileDurable + writeBinaryFileSpec "writeBinaryFileDurable" File.writeBinaryFileDurable + withBinaryFileSpec True "withBinaryFileDurableAtomic" File.withBinaryFileDurableAtomic + writeBinaryFileSpec "writeBinaryFileDurableAtomic" File.writeBinaryFileDurableAtomic + +writeFileUtf8 fp str = withBinaryFile fp WriteMode (`BB.hPutBuilder` BB.stringUtf8 str) + +withBinaryFileSpec :: + Bool -- ^ Should we test atomicity + -> String + -> (forall a. FilePath -> IOMode -> (Handle -> IO a) -> IO a) + -> Spec +withBinaryFileSpec atomic fname withFileTestable = do + let hello = "Hello World" + helloString = "Hello World" + writeHello fp = writeFileUtf8 fp helloString + -- Create a file, write "Hello World" into it and apply the action. + withHelloFileTestable fp iomode action = do + writeHello fp + withFileTestable fp iomode action + goodbye = "Goodbye yall" + modifiedPermissions = + setOwnerExecutable True $ + setOwnerReadable True $ setOwnerWritable True emptyPermissions + describe fname $ do + it "read" $ + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-read" + withHelloFileTestable fp ReadWriteMode (`B.hGet` B.length hello) `shouldReturn` + hello + it "write" $ + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-write" + withHelloFileTestable fp WriteMode (`B.hPut` goodbye) + B.readFile fp `shouldReturn` goodbye + it "read/write" $ + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-read-write" + withHelloFileTestable fp ReadWriteMode $ \h -> do + B.hGetLine h `shouldReturn` hello + B.hPut h goodbye + B.readFile fp `shouldReturn` (hello <> goodbye) + it "append" $ + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-append" + privet = "Привет Мир" -- some unicode won't hurt + encodeUtf8 = BL.toStrict . BB.toLazyByteString . BB.stringUtf8 + writeFileUtf8 fp privet + setPermissions fp modifiedPermissions + withFileTestable fp AppendMode $ \h -> B.hPut h goodbye + B.readFile fp `shouldReturn` (encodeUtf8 privet <> goodbye) + it "sub-directory" $ + withSystemTempDirectory "rio" $ \dir -> do + let subDir = dir </> fname ++ "-sub-directory" + fp = subDir </> "test.file" + createDirectoryIfMissing True subDir + withHelloFileTestable fp ReadWriteMode $ \h -> do + B.hGetLine h `shouldReturn` hello + B.hPut h goodbye + B.readFile fp `shouldReturn` (hello <> goodbye) + it "relative-directory" $ + withSystemTempDirectory "rio" $ \dir -> do + let relDir = fname ++ "-relative-directory" + subDir = dir </> relDir + fp = relDir </> "test.file" + createDirectoryIfMissing True subDir + withCurrentDirectoryCompat dir $ do + withHelloFileTestable fp ReadWriteMode $ \h -> do + B.hGetLine h `shouldReturn` hello + B.hPut h goodbye + B.readFile fp `shouldReturn` (hello <> goodbye) + it "modified-permissions" $ + forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode -> + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-modified-permissions" + writeHello fp + setPermissions fp modifiedPermissions + withFileTestable fp iomode $ \h -> B.hPut h goodbye + getPermissions fp `shouldReturn` modifiedPermissions + it "exception - Does not corrupt files" $ + bool expectFailure property atomic $ -- should fail for non-atomic + forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode -> + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-exception" + _ :: Either ExpectedException () <- + try $ + withHelloFileTestable fp iomode $ \h -> do + B.hPut h goodbye + throwIO ExpectedException + B.readFile fp `shouldReturn` hello + it "exception - Does not leave files behind" $ + bool expectFailure property atomic $ -- should fail for non-atomic + forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode -> + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-exception" + _ :: Either ExpectedException () <- + try $ + withFileTestable fp iomode $ \h -> do + B.hPut h goodbye + throwIO ExpectedException + doesFileExist fp `shouldReturn` False + listDirectoryCompat dir `shouldReturn` [] + it "delete - file" $ + bool expectFailure property atomic $ -- should fail for non-atomic + forM_ [WriteMode, ReadWriteMode, AppendMode] $ \iomode -> + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-delete" + withHelloFileTestable fp iomode $ \h -> do + removeFile fp + B.hPut h goodbye + doesFileExist fp `shouldReturn` True + +writeBinaryFileSpec :: String -> (FilePath -> B.ByteString -> IO ()) -> SpecWith () +writeBinaryFileSpec fname writeFileTestable = do + let hello = "Hello World" + describe fname $ do + it "write" $ + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-write" + writeFileTestable fp hello + B.readFile fp `shouldReturn` hello + it "default-permissions" $ + withSystemTempDirectory "rio" $ \dir -> do + let fp = dir </> fname ++ "-default-permissions" + defaultPermissions = + setOwnerReadable True $ setOwnerWritable True emptyPermissions + writeFileTestable fp hello + getPermissions fp `shouldReturn` defaultPermissions + + +listDirectoryCompat :: FilePath -> IO [FilePath] +#if MIN_VERSION_directory(1,2,5) +listDirectoryCompat = listDirectory +#else +listDirectoryCompat path = + filter f <$> getDirectoryContents path + where f filename = filename /= "." && filename /= ".." +#endif + +withCurrentDirectoryCompat :: FilePath -> IO a -> IO a +#if MIN_VERSION_directory(1,2,3) +withCurrentDirectoryCompat = withCurrentDirectory +#else +withCurrentDirectoryCompat dir action = + bracket getCurrentDirectory setCurrentDirectory $ \ _ -> do + setCurrentDirectory dir + action +#endif + +#else +spec :: Spec +spec = pure () +#endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/unliftio-0.2.11/unliftio.cabal new/unliftio-0.2.12/unliftio.cabal --- old/unliftio-0.2.11/unliftio.cabal 2019-06-07 12:40:40.000000000 +0200 +++ new/unliftio-0.2.12/unliftio.cabal 2019-07-12 06:21:15.000000000 +0200 @@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.1. +-- This file has been generated from package.yaml by hpack version 0.31.2. -- -- see: https://github.com/sol/hpack -- --- hash: 9b54d473debcf0958351a2c8a7924ed9a8d3504243c348545109ffe72b005683 +-- hash: 55a1160a6cc39aa1529ed95fe90e85ac31d632ee8ea1f31f06cb77bd2502de5b name: unliftio -version: 0.2.11 +version: 0.2.12 synopsis: The MonadUnliftIO typeclass for unlifting monads to IO (batteries included) description: Please see the documentation and README at <https://www.stackage.org/package/unliftio> category: Control @@ -28,7 +28,8 @@ ghc-options: -fwarn-incomplete-uni-patterns build-depends: async >2.1.1 - , base >=4.7 && <5 + , base >=4.8 && <5 + , bytestring , deepseq , directory , filepath @@ -46,14 +47,20 @@ build-depends: nats if os(darwin) + other-modules: + UnliftIO.IO.File.Posix c-sources: cbits/time-osx.c + cbits/file-posix.c else if os(windows) c-sources: cbits/time-windows.c else + other-modules: + UnliftIO.IO.File.Posix c-sources: + cbits/file-posix.c cbits/time-posix.c exposed-modules: UnliftIO @@ -66,6 +73,7 @@ UnliftIO.Foreign UnliftIO.Internals.Async UnliftIO.IO + UnliftIO.IO.File UnliftIO.IORef UnliftIO.Memoize UnliftIO.MVar @@ -85,7 +93,8 @@ build-depends: QuickCheck , async >2.1.1 - , base >=4.7 && <5 + , base >=4.8 && <5 + , bytestring , containers , deepseq , directory @@ -105,6 +114,7 @@ other-modules: UnliftIO.AsyncSpec UnliftIO.ExceptionSpec + UnliftIO.IO.FileSpec UnliftIO.IOSpec UnliftIO.MemoizeSpec UnliftIO.PooledAsyncSpec @@ -121,7 +131,8 @@ ghc-options: -O2 -threaded -rtsopts build-depends: async >2.1.1 - , base >=4.7 && <5 + , base >=4.8 && <5 + , bytestring , deepseq , directory , filepath