1{-# LANGUAGE CPP #-} 2{-# LANGUAGE ForeignFunctionInterface #-} 3{-# LANGUAGE GeneralizedNewtypeDeriving #-} 4{-# LANGUAGE LambdaCase #-} 5{-# LANGUAGE OverloadedStrings #-} 6{-# LANGUAGE ViewPatterns #-} 7module UnliftIO.IO.File.Posix 8 ( withBinaryFileDurable 9 , withBinaryFileDurableAtomic 10 , withBinaryFileAtomic 11 , ensureFileDurable 12 ) 13 where 14 15#if __GLASGOW_HASKELL__ < 710 16import Control.Applicative 17#endif 18import Control.Monad (forM_, guard, unless, void, when) 19import Control.Monad.IO.Unlift 20import Data.Bits (Bits, (.|.)) 21import Data.ByteString (ByteString) 22import Data.Maybe (fromMaybe) 23import Data.Typeable (cast) 24import Foreign (allocaBytes) 25import Foreign.C (CInt(..), throwErrnoIfMinus1, throwErrnoIfMinus1Retry, 26 throwErrnoIfMinus1Retry_) 27import GHC.IO.Device (IODeviceType(RegularFile)) 28import qualified GHC.IO.Device as Device 29import GHC.IO.Exception (IOErrorType(UnsupportedOperation)) 30import qualified GHC.IO.FD as FD 31import qualified GHC.IO.Handle.FD as HandleFD 32import qualified GHC.IO.Handle.Types as HandleFD (Handle(..), Handle__(..)) 33import System.Directory (removeFile) 34import System.FilePath (takeDirectory, takeFileName) 35import System.IO (Handle, IOMode(..), SeekMode(..), hGetBuf, hPutBuf, 36 openBinaryTempFile) 37import System.IO.Error (ioeGetErrorType, isAlreadyExistsError, 38 isDoesNotExistError) 39import qualified System.Posix.Files as Posix 40import System.Posix.Internals (CFilePath, c_close, c_safe_open, withFilePath) 41import System.Posix.Types (CMode(..), Fd(..), FileMode) 42import UnliftIO.Exception 43import UnliftIO.IO 44import UnliftIO.MVar 45 46-- NOTE: System.Posix.Internal doesn't re-export this constants so we have to 47-- recreate-them here 48 49newtype CFlag = 50 CFlag CInt 51 deriving (Eq, Show, Bits) 52 53foreign import ccall unsafe "HsBase.h __hscore_o_rdonly" o_RDONLY :: CFlag 54foreign import ccall unsafe "HsBase.h __hscore_o_wronly" o_WRONLY :: CFlag 55foreign import ccall unsafe "HsBase.h __hscore_o_rdwr" o_RDWR :: CFlag 56foreign import ccall unsafe "HsBase.h __hscore_o_append" o_APPEND :: CFlag 57foreign import ccall unsafe "HsBase.h __hscore_o_creat" o_CREAT :: CFlag 58foreign import ccall unsafe "HsBase.h __hscore_o_noctty" o_NOCTTY :: CFlag 59 60-- After here, we have our own imports 61 62-- On non-Linux operating systems that do not support `O_TMPFILE` the value of 63-- `o_TMPFILE` will be 0, which is then used to fallback onto a different 64-- implementation of temporary files. 65foreign import ccall unsafe "file-posix.c unliftio_o_tmpfile" o_TMPFILE :: CFlag 66 67 68-- | Whenever Operating System does not support @O_TMPFILE@ flag and anonymous 69-- temporary files then `o_TMPFILE` flag will be set to @0@ 70o_TMPFILE_not_supported :: CFlag 71o_TMPFILE_not_supported = CFlag 0 72 73newtype CAt = CAt 74 { unCAt :: CInt 75 } deriving (Eq, Show, Bits) 76 77foreign import ccall unsafe "file-posix.c unliftio_at_fdcwd" at_FDCWD :: CAt 78foreign import ccall unsafe "file-posix.c unliftio_at_symlink_follow" at_SYMLINK_FOLLOW :: CAt 79foreign import ccall unsafe "file-posix.c unliftio_s_irusr" s_IRUSR :: CMode 80foreign import ccall unsafe "file-posix.c unliftio_s_iwusr" s_IWUSR :: CMode 81 82c_open :: CFilePath -> CFlag -> CMode -> IO CInt 83c_open fp (CFlag flags) = c_safe_open fp flags 84 85foreign import ccall safe "fcntl.h openat" 86 c_safe_openat :: CInt -> CFilePath -> CInt -> CMode -> IO CInt 87 88c_openat :: DirFd -> CFilePath -> CFlag -> CMode -> IO CInt 89c_openat (DirFd (Fd fd)) fp (CFlag flags) = c_safe_openat fd fp flags 90 91foreign import ccall safe "fcntl.h renameat" 92 c_safe_renameat :: CInt -> CFilePath -> CInt -> CFilePath -> IO CInt 93 94c_renameat :: DirFd -> CFilePath -> DirFd -> CFilePath -> IO CInt 95c_renameat (DirFd (Fd fdFrom)) cFpFrom (DirFd (Fd fdTo)) cFpTo = 96 c_safe_renameat fdFrom cFpFrom fdTo cFpTo 97 98foreign import ccall safe "unistd.h fsync" 99 c_safe_fsync :: CInt -> IO CInt 100 101c_fsync :: Fd -> IO CInt 102c_fsync (Fd fd) = c_safe_fsync fd 103 104foreign import ccall safe "unistd.h linkat" 105 c_safe_linkat :: CInt -> CFilePath -> CInt -> CFilePath -> CInt -> IO CInt 106 107c_linkat :: CAt -> CFilePath -> Either DirFd CAt -> CFilePath -> CAt -> IO CInt 108c_linkat cat oldPath eNewDir newPath (CAt flags) = 109 c_safe_linkat (unCAt cat) oldPath newDir newPath flags 110 where 111 unFd (Fd fd) = fd 112 newDir = either (unFd . unDirFd) unCAt eNewDir 113 114std_flags, output_flags, read_flags, write_flags, rw_flags, 115 append_flags :: CFlag 116std_flags = o_NOCTTY 117output_flags = std_flags .|. o_CREAT 118read_flags = std_flags .|. o_RDONLY 119write_flags = output_flags .|. o_WRONLY 120rw_flags = output_flags .|. o_RDWR 121append_flags = write_flags .|. o_APPEND 122 123ioModeToFlags :: IOMode -> CFlag 124ioModeToFlags iomode = 125 case iomode of 126 ReadMode -> read_flags 127 WriteMode -> write_flags 128 ReadWriteMode -> rw_flags 129 AppendMode -> append_flags 130 131newtype DirFd = DirFd 132 { unDirFd :: Fd 133 } 134 135-- | Returns a low-level file descriptor for a directory path. This function 136-- exists given the fact that 'openFile' does not work with directories. 137-- 138-- If you use this function, make sure you are working on a masked state, 139-- otherwise async exceptions may leave file descriptors open. 140openDir :: MonadIO m => FilePath -> m Fd 141openDir fp 142 -- TODO: Investigate what is the situation with Windows FS in regards to non_blocking 143 -- NOTE: File operations _do not support_ non_blocking on various kernels, more 144 -- info can be found here: https://ghc.haskell.org/trac/ghc/ticket/15153 145 = 146 liftIO $ 147 withFilePath fp $ \cFp -> 148 Fd <$> 149 throwErrnoIfMinus1Retry 150 "openDir" 151 (c_open cFp (ioModeToFlags ReadMode) 0o660) 152 153-- | Closes a 'Fd' that points to a Directory. 154closeDirectory :: MonadIO m => DirFd -> m () 155closeDirectory (DirFd (Fd dirFd)) = 156 liftIO $ 157 throwErrnoIfMinus1Retry_ "closeDirectory" $ c_close dirFd 158 159-- | Executes the low-level C function fsync on a C file descriptor 160fsyncFileDescriptor 161 :: MonadIO m 162 => String -- ^ Meta-description for error messages 163 -> Fd -- ^ C File Descriptor 164 -> m () 165fsyncFileDescriptor name fd = 166 liftIO $ void $ throwErrnoIfMinus1 ("fsync - " ++ name) $ c_fsync fd 167 168-- | Call @fsync@ on the file handle. Accepts an arbitary string for error reporting. 169fsyncFileHandle :: String -> Handle -> IO () 170fsyncFileHandle fname hdl = withHandleFd hdl (fsyncFileDescriptor (fname ++ "/File")) 171 172 173-- | Call @fsync@ on the opened directory file descriptor. Accepts an arbitary 174-- string for error reporting. 175fsyncDirectoryFd :: String -> DirFd -> IO () 176fsyncDirectoryFd fname = fsyncFileDescriptor (fname ++ "/Directory") . unDirFd 177 178 179-- | Opens a file from a directory, using this function in favour of a regular 180-- 'openFile' guarantees that any file modifications are kept in the same 181-- directory where the file was opened. An edge case scenario is a mount 182-- happening in the directory where the file was opened while your program is 183-- running. 184-- 185-- If you use this function, make sure you are working on an masked state, 186-- otherwise async exceptions may leave file descriptors open. 187-- 188openFileFromDir :: MonadIO m => DirFd -> FilePath -> IOMode -> m Handle 189openFileFromDir dirFd filePath@(takeFileName -> fileName) iomode = 190 liftIO $ 191 withFilePath fileName $ \cFileName -> 192 bracketOnError 193 (do fileFd <- 194 throwErrnoIfMinus1Retry "openFileFromDir" $ 195 c_openat dirFd cFileName (ioModeToFlags iomode) 0o666 196 {- Can open directory with read only -} 197 FD.mkFD 198 fileFd 199 iomode 200 Nothing {- no stat -} 201 False {- not a socket -} 202 False {- non_blocking -} 203 `onException` 204 c_close fileFd) 205 (liftIO . Device.close . fst) 206 (\(fD, fd_type) 207 -- we want to truncate() if this is an open in WriteMode, but only if the 208 -- target is a RegularFile. ftruncate() fails on special files like 209 -- /dev/null. 210 -> do 211 when (iomode == WriteMode && fd_type == RegularFile) $ 212 Device.setSize fD 0 213 HandleFD.mkHandleFromFD fD fd_type filePath iomode False Nothing) 214 215 216-- | Similar to `openFileFromDir`, but will open an anonymous (nameless) 217-- temporary file in the supplied directory 218openAnonymousTempFileFromDir :: 219 MonadIO m => 220 Maybe DirFd 221 -- ^ If a file descriptor is given for the directory where the target file is/will be 222 -- located in, then it will be used for opening an anonymous file. Otherwise 223 -- anonymous will be opened unattached to any file path. 224 -> FilePath 225 -- ^ File path of the target file that we are working on. 226 -> IOMode 227 -> m Handle 228openAnonymousTempFileFromDir mDirFd filePath iomode = 229 liftIO $ 230 case mDirFd of 231 Just dirFd -> withFilePath "." (openAnonymousWith . c_openat dirFd) 232 Nothing -> 233 withFilePath (takeDirectory filePath) (openAnonymousWith . c_open) 234 where 235 fdName = "openAnonymousTempFileFromDir - " ++ filePath 236 ioModeToTmpFlags :: IOMode -> CFlag 237 ioModeToTmpFlags = 238 \case 239 ReadMode -> o_RDWR -- It is an error to create a O_TMPFILE with O_RDONLY 240 ReadWriteMode -> o_RDWR 241 _ -> o_WRONLY 242 openAnonymousWith fopen = 243 bracketOnError 244 (do fileFd <- 245 throwErrnoIfMinus1Retry "openAnonymousTempFileFromDir" $ 246 fopen (o_TMPFILE .|. ioModeToTmpFlags iomode) (s_IRUSR .|. s_IWUSR) 247 FD.mkFD 248 fileFd 249 iomode 250 Nothing {- no stat -} 251 False {- not a socket -} 252 False {- non_blocking -} 253 `onException` 254 c_close fileFd) 255 (liftIO . Device.close . fst) 256 (\(fD, fd_type) -> 257 HandleFD.mkHandleFromFD fD fd_type fdName iomode False Nothing) 258 259 260atomicDurableTempFileRename :: 261 DirFd -> Maybe FileMode -> Handle -> Maybe FilePath -> FilePath -> IO () 262atomicDurableTempFileRename dirFd mFileMode tmpFileHandle mTmpFilePath filePath = do 263 fsyncFileHandle "atomicDurableTempFileCreate" tmpFileHandle 264 -- at this point we know that the content has been persisted to the storage it 265 -- is safe to do the atomic move/replace 266 let eTmpFile = maybe (Left tmpFileHandle) Right mTmpFilePath 267 atomicTempFileRename (Just dirFd) mFileMode eTmpFile filePath 268 -- Important to close the handle, so the we can fsync the directory 269 hClose tmpFileHandle 270 -- file path is updated, now we can fsync the directory 271 fsyncDirectoryFd "atomicDurableTempFileCreate" dirFd 272 273 274-- | There will be an attempt to atomically convert an invisible temporary file 275-- into a target file at the supplied file path. In case when there is already a 276-- file at that file path, a new visible temporary file will be created in the 277-- same folder and then atomically renamed into the target file path, replacing 278-- any existing file. This is necessary since `c_safe_linkat` cannot replace 279-- files atomically and we have to fall back onto `c_safe_renameat`. This should 280-- not be a problem in practice, since lifetime of such visible file is 281-- extremely short and it will be cleaned up regardless of the outcome of the 282-- rename. 283-- 284-- It is important to note, that whenever a file descriptor for the containing 285-- directory is supplied, renaming and linking will be done in its context, 286-- thus allowing to do proper fsyncing if durability is necessary. 287-- 288-- __NOTE__: this function will work only on Linux. 289-- 290atomicTempFileCreate :: 291 Maybe DirFd 292 -- ^ Possible handle for the directory where the target file is located. Which 293 -- means that the file is already in that directory, just without a name. In other 294 -- words it was opened before with `openAnonymousTempFileFromDir` 295 -> Maybe FileMode 296 -- ^ If file permissions are supplied they will be set on the new file prior 297 -- to atomic rename. 298 -> Handle 299 -- ^ Handle to the anonymous temporary file created with `c_openat` and 300 -- `o_TMPFILE` 301 -> FilePath 302 -- ^ File path for the target file. 303 -> IO () 304atomicTempFileCreate mDirFd mFileMode tmpFileHandle filePath = 305 withHandleFd tmpFileHandle $ \fd@(Fd cFd) -> 306 withFilePath ("/proc/self/fd/" ++ show cFd) $ \cFromFilePath -> 307 withFilePath filePathName $ \cToFilePath -> do 308 let fileMode = fromMaybe Posix.stdFileMode mFileMode 309 -- work around for the glibc bug: https://sourceware.org/bugzilla/show_bug.cgi?id=17523 310 Posix.setFdMode fd fileMode 311 let safeLink which to = 312 throwErrnoIfMinus1Retry_ 313 ("atomicFileCreate - c_safe_linkat - " ++ which) $ 314 -- see `man linkat` and `man openat` for more info 315 c_linkat at_FDCWD cFromFilePath cDirFd to at_SYMLINK_FOLLOW 316 eExc <- 317 tryJust (guard . isAlreadyExistsError) $ 318 safeLink "anonymous" cToFilePath 319 case eExc of 320 Right () -> pure () 321 Left () -> 322 withBinaryTempFileFor filePath $ \visTmpFileName visTmpFileHandle -> do 323 hClose visTmpFileHandle 324 removeFile visTmpFileName 325 case mDirFd of 326 Nothing -> do 327 withFilePath visTmpFileName (safeLink "visible") 328 Posix.rename visTmpFileName filePath 329 Just dirFd -> 330 withFilePath (takeFileName visTmpFileName) $ \cVisTmpFile -> do 331 safeLink "visible" cVisTmpFile 332 throwErrnoIfMinus1Retry_ 333 "atomicFileCreate - c_safe_renameat" $ 334 c_renameat dirFd cVisTmpFile dirFd cToFilePath 335 where 336 (cDirFd, filePathName) = 337 case mDirFd of 338 Nothing -> (Right at_FDCWD, filePath) 339 Just dirFd -> (Left dirFd, takeFileName filePath) 340 341atomicTempFileRename :: 342 Maybe DirFd 343 -- ^ Possible handle for the directory where the target file is located. 344 -> Maybe FileMode 345 -- ^ If file permissions are supplied they will be set on the new file prior 346 -- to atomic rename. 347 -> Either Handle FilePath 348 -- ^ Temporary file. If a handle is supplied, it means it was opened with 349 -- @O_TMPFILE@ flag and thus we are on the Linux OS and can safely call 350 -- `atomicTempFileCreate` 351 -> FilePath 352 -- ^ File path for the target file. Whenever `DirFd` is supplied, it must be 353 -- the containgin directory fo this file, but that invariant is not enforced 354 -- within this function. 355 -> IO () 356atomicTempFileRename mDirFd mFileMode eTmpFile filePath = 357 case eTmpFile of 358 Left tmpFileHandle -> 359 atomicTempFileCreate mDirFd mFileMode tmpFileHandle filePath 360 Right tmpFilePath -> do 361 forM_ mFileMode $ \fileMode -> Posix.setFileMode tmpFilePath fileMode 362 case mDirFd of 363 Nothing -> Posix.rename tmpFilePath filePath 364 Just dirFd -> 365 withFilePath (takeFileName filePath) $ \cToFilePath -> 366 withFilePath (takeFileName tmpFilePath) $ \cTmpFilePath -> 367 throwErrnoIfMinus1Retry_ "atomicFileCreate - c_safe_renameat" $ 368 c_renameat dirFd cTmpFilePath dirFd cToFilePath 369 370 371withDirectory :: MonadUnliftIO m => FilePath -> (DirFd -> m a) -> m a 372withDirectory dirPath = bracket (DirFd <$> openDir dirPath) closeDirectory 373 374withFileInDirectory :: 375 MonadUnliftIO m => DirFd -> FilePath -> IOMode -> (Handle -> m a) -> m a 376withFileInDirectory dirFd filePath iomode = 377 bracket (openFileFromDir dirFd filePath iomode) hClose 378 379 380-- | Create a temporary file for a matching possibly exiting target file that 381-- will be replaced in the future. Temporary file is meant to be renamed 382-- afterwards, thus it is only deleted upon error. 383-- 384-- __Important__: Temporary file is not removed and file handle is not closed if 385-- there was no exception thrown by the supplied action. 386withBinaryTempFileFor :: 387 MonadUnliftIO m 388 => FilePath 389 -- ^ "For" file. It may exist or may not. 390 -> (FilePath -> Handle -> m a) 391 -> m a 392withBinaryTempFileFor filePath action = 393 bracketOnError 394 (liftIO (openBinaryTempFile dirPath tmpFileName)) 395 (\(tmpFilePath, tmpFileHandle) -> 396 hClose tmpFileHandle >> liftIO (tryIO (removeFile tmpFilePath))) 397 (uncurry action) 398 where 399 dirPath = takeDirectory filePath 400 fileName = takeFileName filePath 401 tmpFileName = "." ++ fileName ++ ".tmp" 402 403-- | Returns `Nothing` if anonymous temporary file is not supported by the OS or 404-- the underlying file system can't handle that feature. 405withAnonymousBinaryTempFileFor :: 406 MonadUnliftIO m 407 => Maybe DirFd 408 -- ^ It is possible to open the temporary file in the context of a directory, 409 -- in such case supply its file descriptor. i.e. @openat@ will be used instead 410 -- of @open@ 411 -> FilePath 412 -- ^ "For" file. The file may exist or may not. 413 -> IOMode 414 -> (Handle -> m a) 415 -> m (Maybe a) 416withAnonymousBinaryTempFileFor mDirFd filePath iomode action 417 | o_TMPFILE == o_TMPFILE_not_supported = pure Nothing 418 | otherwise = 419 trySupported $ 420 bracket (openAnonymousTempFileFromDir mDirFd filePath iomode) hClose action 421 where 422 trySupported m = 423 tryIO m >>= \case 424 Right res -> pure $ Just res 425 Left exc 426 | ioeGetErrorType exc == UnsupportedOperation -> pure Nothing 427 Left exc -> throwIO exc 428 429withNonAnonymousBinaryTempFileFor :: 430 MonadUnliftIO m 431 => Maybe DirFd 432 -- ^ It is possible to open the temporary file in the context of a directory, 433 -- in such case supply its file descriptor. i.e. @openat@ will be used instead 434 -- of @open@ 435 -> FilePath 436 -- ^ "For" file. The file may exist or may not. 437 -> IOMode 438 -> (FilePath -> Handle -> m a) 439 -> m a 440withNonAnonymousBinaryTempFileFor mDirFd filePath iomode action = 441 withBinaryTempFileFor filePath $ \tmpFilePath tmpFileHandle -> do 442 hClose tmpFileHandle 443 case mDirFd of 444 Nothing -> withBinaryFile tmpFilePath iomode (action tmpFilePath) 445 Just dirFd -> withFileInDirectory dirFd tmpFilePath iomode (action tmpFilePath) 446 447-- | Copy the contents of the file into the handle, but only if that file exists 448-- and either `ReadWriteMode` or `AppendMode` is specified. Returned are the 449-- file permissions of the original file so it can be set later when original 450-- gets overwritten atomically. 451copyFileHandle :: 452 MonadUnliftIO f => IOMode -> FilePath -> Handle -> f (Maybe FileMode) 453copyFileHandle iomode fromFilePath toHandle = 454 either (const Nothing) Just <$> 455 tryJust 456 (guard . isDoesNotExistError) 457 (do fileStatus <- liftIO $ Posix.getFileStatus fromFilePath 458 -- Whenever we are not overwriting an existing file, we also need a 459 -- copy of the file's contents 460 unless (iomode == WriteMode) $ do 461 withBinaryFile fromFilePath ReadMode (`copyHandleData` toHandle) 462 unless (iomode == AppendMode) $ hSeek toHandle AbsoluteSeek 0 463 -- Get the copy of source file permissions, but only whenever it exists 464 pure $ Posix.fileMode fileStatus) 465 466 467-- This is a copy of the internal function from `directory-1.3.3.2`. It became 468-- available only in directory-1.3.3.0 and is still internal, hence the 469-- duplication. 470copyHandleData :: MonadIO m => Handle -> Handle -> m () 471copyHandleData hFrom hTo = liftIO $ allocaBytes bufferSize go 472 where 473 bufferSize = 131072 -- 128 KiB, as coreutils `cp` uses as of May 2014 (see ioblksize.h) 474 go buffer = do 475 count <- hGetBuf hFrom buffer bufferSize 476 when (count > 0) $ do 477 hPutBuf hTo buffer count 478 go buffer 479 480-- | Thread safe access to the file descriptor in the file handle 481withHandleFd :: Handle -> (Fd -> IO a) -> IO a 482withHandleFd h cb = 483 case h of 484 HandleFD.FileHandle _ mv -> 485 withMVar mv $ \HandleFD.Handle__{HandleFD.haDevice = dev} -> 486 case cast dev of 487 Just fd -> cb $ Fd $ FD.fdFD fd 488 Nothing -> error "withHandleFd: not a file handle" 489 HandleFD.DuplexHandle {} -> error "withHandleFd: not a file handle" 490 491-- | See `ensureFileDurable` 492ensureFileDurable :: MonadIO m => FilePath -> m () 493ensureFileDurable filePath = 494 liftIO $ 495 withDirectory (takeDirectory filePath) $ \dirFd -> 496 withFileInDirectory dirFd filePath ReadMode $ \fileHandle -> 497 liftIO $ do 498 fsyncFileHandle "ensureFileDurablePosix" fileHandle 499 -- NOTE: Here we are purposefully not fsyncing the directory if the file fails to fsync 500 fsyncDirectoryFd "ensureFileDurablePosix" dirFd 501 502 503 504-- | See `withBinaryFileDurable` 505withBinaryFileDurable :: 506 MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r 507withBinaryFileDurable filePath iomode action = 508 case iomode of 509 ReadMode 510 -- We do not need to consider durable operations when we are in a 511 -- 'ReadMode', so we can use a regular `withBinaryFile` 512 -> withBinaryFile filePath iomode action 513 _ {- WriteMode, ReadWriteMode, AppendMode -} 514 -> 515 withDirectory (takeDirectory filePath) $ \dirFd -> 516 withFileInDirectory dirFd filePath iomode $ \tmpFileHandle -> do 517 res <- action tmpFileHandle 518 liftIO $ do 519 fsyncFileHandle "withBinaryFileDurablePosix" tmpFileHandle 520 -- NOTE: Here we are purposefully not fsyncing the directory if the file fails to fsync 521 fsyncDirectoryFd "withBinaryFileDurablePosix" dirFd 522 pure res 523 524-- | See `withBinaryFileDurableAtomic` 525withBinaryFileDurableAtomic :: 526 MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r 527withBinaryFileDurableAtomic filePath iomode action = 528 case iomode of 529 ReadMode 530 -- We do not need to consider an atomic operation when we are in a 531 -- 'ReadMode', so we can use a regular `withBinaryFile` 532 -> withBinaryFile filePath iomode action 533 _ {- WriteMode, ReadWriteMode, AppendMode -} 534 -> 535 withDirectory (takeDirectory filePath) $ \dirFd -> do 536 mRes <- withAnonymousBinaryTempFileFor (Just dirFd) filePath iomode $ 537 durableAtomicAction dirFd Nothing 538 case mRes of 539 Just res -> pure res 540 Nothing -> 541 withNonAnonymousBinaryTempFileFor (Just dirFd) filePath iomode $ \tmpFilePath -> 542 durableAtomicAction dirFd (Just tmpFilePath) 543 where 544 durableAtomicAction dirFd mTmpFilePath tmpFileHandle = do 545 mFileMode <- copyFileHandle iomode filePath tmpFileHandle 546 res <- action tmpFileHandle 547 liftIO $ 548 atomicDurableTempFileRename 549 dirFd 550 mFileMode 551 tmpFileHandle 552 mTmpFilePath 553 filePath 554 pure res 555 556-- | See `withBinaryFileAtomic` 557withBinaryFileAtomic :: 558 MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m r) -> m r 559withBinaryFileAtomic filePath iomode action = 560 case iomode of 561 ReadMode 562 -- We do not need to consider an atomic operation when we are in a 563 -- 'ReadMode', so we can use a regular `withBinaryFile` 564 -> withBinaryFile filePath iomode action 565 _ {- WriteMode, ReadWriteMode, AppendMode -} 566 -> do 567 mRes <- 568 withAnonymousBinaryTempFileFor Nothing filePath iomode $ 569 atomicAction Nothing 570 case mRes of 571 Just res -> pure res 572 Nothing -> 573 withNonAnonymousBinaryTempFileFor Nothing filePath iomode $ \tmpFilePath -> 574 atomicAction (Just tmpFilePath) 575 where 576 atomicAction mTmpFilePath tmpFileHandle = do 577 let eTmpFile = maybe (Left tmpFileHandle) Right mTmpFilePath 578 mFileMode <- copyFileHandle iomode filePath tmpFileHandle 579 res <- action tmpFileHandle 580 liftIO $ atomicTempFileRename Nothing mFileMode eTmpFile filePath 581 pure res 582 583