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