packages feed

cow-0.1.0.0: src/System/IO/Copy.hs

{-# LANGUAGE CPP #-}
-- Cross-platform copy-on-write file semantics.
module System.IO.Copy 
  (copyFile
#if linux_HOST_OS || darwin_HOST_OS
   ,copyFileOnlyIfCopyOnWrite
   ,CopyError(..)
#endif
  ) where
import Foreign.C.Error (Errno(..))
#if defined(mingw32_HOST_OS)
-- WINDOWS
import qualified System.Win32.File as Win32
# if defined(i386_HOST_ARCH)
#  define WINDOWS_CCONV stdcall
# elif defined(x86_64_HOST_ARCH)
#  define WINDOWS_CCONV ccall
# endif
#else
-- LINUX
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.C.Types
import System.IO (Handle, IOMode(..), openFile)
import Control.Exception (throwIO)
import System.Posix.Types (Fd(..))
import System.Posix.IO (handleToFd, closeFd)
import Foreign.C.Error (getErrno, eBADF, eFAULT, eNOTTY, eTXTBSY, eISDIR, eXDEV, eOPNOTSUPP, ePERM, eINVAL, errnoToIOError)
#endif

#if linux_HOST_OS
-- | True if the function should fallback on non-copy-on-write semantics if copy-on-write is not possible.
type CopyFallback = Bool

-- | Copy data from one handle to the other hopefully using copy-on-write semantics, but fallback to generic copy if copy-on-write fails.
copyHandle :: CopyFallback -> Handle -> Handle -> IO (Either CopyError ())
copyHandle fallback source dest = do
  sourceFd <- handleToFd source
  destFd <- handleToFd dest
  ret <- ficlone destFd sourceFd
  errno <- getErrno
  let convertError errno'
        | errno' `elem` [eBADF, ePERM, eTXTBSY] =
          InvalidFileDescriptorError errno'
      convertError errno'
        | errno' `elem` [eXDEV] =
          CrossFSWriteError errno'
      convertError errno'
        | errno' `elem` [eINVAL, eISDIR, eOPNOTSUPP] =
          NotSupportedError errno'
      convertError errno' =
        InvalidFileDescriptorError errno'
      cleanup = do
        closeFd sourceFd
        closeFd destFd
  if ret /= 0 then do
    if fallback then do
      if errno `elem` [eBADF, eFAULT, eNOTTY, eTXTBSY, eISDIR, ePERM] then do
        cleanup
        pure (Left (convertError errno))
      else do --EINVAL or EOPNOTSUPP or EXDEV, so perform fallback
        result <- copy_file_range sourceFd destFd
        if result < 0 then do
          err <- getErrno
          cleanup
          pure (Left (FallbackCopyError err))
        else do
          cleanup
          pure (Right ())
    else do
      cleanup
      pure (Left (convertError errno))
  else do
    cleanup
    pure (Right ())
#elif darwin_HOST_OS
  res <- clonefile source dest
  if res < 0 then do
    -- on darwin, there is no fallback- we just assume APFS as the filesystem
    pure (Left (FallbackCopyError (Errno 0)))
  else
    pure (Right ())
#elif mingw32_HOST_OS
#endif

copyFile :: FilePath -> FilePath -> IO ()
copyFile sourcePath destPath = do
  -- call copyHandle on Linux and Darwin
#if linux_HOST_OS || darwin_HOST_OS
  sourceHandle <- openFile sourcePath ReadMode 
  destHandle <- openFile destPath WriteMode 
  res <- copyHandle True sourceHandle destHandle
  case res of
    Left err -> throwIO $ errnoToIOError "copyFile (CoW)" (errnoFromCopyError err) Nothing Nothing
    Right () -> pure ()
#else 
  -- on Windows, copyFile will use CoW where available which is only on ReFS
  Win32.copyFile sourcePath destPath False
#endif
  
data CopyError =
  CrossFSWriteError Errno |
  InvalidFileDescriptorError Errno |
  NotSupportedError Errno |
  FallbackCopyError Errno
  deriving (Eq)

instance Show CopyError where
  show e =
    case e of
      CrossFSWriteError (Errno errno) -> "CrossFSWriteError " <> show errno
      InvalidFileDescriptorError (Errno errno) -> "InvalidFileDescriptorError " <> show errno
      NotSupportedError (Errno errno) -> "NotSupportedError " <> show errno
      FallbackCopyError (Errno errno) -> "FallbackCopyError " <> show errno

#if linux_HOST_OS || darwin_HOST_OS
errnoFromCopyError :: CopyError -> Errno
errnoFromCopyError e =
  case e of
    CrossFSWriteError e' -> e'
    InvalidFileDescriptorError e' -> e'
    NotSupportedError e' -> e'
    FallbackCopyError e' -> e'

copyFileOnlyIfCopyOnWrite :: FilePath -> FilePath -> IO (Either CopyError ())
copyFileOnlyIfCopyOnWrite sourcePath destPath = do
  sourceHandle <- openFile sourcePath ReadMode 
  destHandle <- openFile destPath WriteMode 
  copyHandle False sourceHandle destHandle
#endif

#ifdef linux_HOST_OS
-- LINUX ONLY
-- FICLONE ioctl number. Common value for x86_64 Linux (from linux/fs.h):
-- #define FICLONE        _IOW(0x94, 9, int)
-- Encoded here as 0x40049409 (unsigned long)
-- If this doesn't work on your architecture, check <linux/fs.h>.
ficlone_ioctl :: CULong
ficlone_ioctl = 0x40049409

foreign import ccall unsafe "ioctl"
  c_ioctl :: CInt -> CULong -> CInt -> IO CInt

-- wrapper: perform ioctl with fdDst, FICLONE, fdSrc
ficlone :: Fd -> Fd -> IO CInt
ficlone (Fd fdDst) (Fd fdSrc) = do
  -- third arg should be the source fd cast to unsigned long / int as expected by kernel
  let arg = fromIntegral fdSrc :: CInt
  --throwErrnoIfMinus1_ "ioctl(FICLONE)" $
  c_ioctl (fromIntegral fdDst) ficlone_ioctl arg

foreign import ccall unsafe "copy_file_range"
  c_copy_file_range :: CInt -> Ptr CULong -> CInt -> Ptr CULong -> CULong -> CUInt -> IO CLong

copy_file_range :: Fd -> Fd -> IO CLong
copy_file_range (Fd fdSource) (Fd fdDest) =
  c_copy_file_range (fromIntegral fdSource) nullPtr (fromIntegral fdDest) nullPtr maxBound 0

#elif darwin_HOST_OS
foreign import ccall unsafe "clonefile"
  c_clonefile :: CString -> CString -> IO CInt

clonefile :: FilePath -> FilePath -> IO CInt
clonefile sourcePath destPath =
  withCString sourcePath $ \cSourcePath ->
    withCString destPath $ \cDestPath ->
      c_clonefile cSourcePath cDestPath

#endif