packages feed

ephemeral-pg-0.1.0.0: src/EphemeralPg/Internal/CopyOnWrite.hs

-- | Copy-on-write detection and operations.
--
-- This module provides copy-on-write support for efficient directory copying.
-- On macOS, this uses @clonefile()@ via @cp -c@. On Linux with Btrfs/XFS,
-- this uses @cp --reflink=always@.
--
-- Detection is done by actually attempting a clone operation on a test file,
-- so it works correctly regardless of filesystem type.
module EphemeralPg.Internal.CopyOnWrite
  ( -- * Capability detection
    CowCapability (..),
    CowMethod (..),
    detectCowCapability,

    -- * Directory operations
    copyDirectory,
    copyDirectoryCoW,
    copyDirectoryRegular,
  )
where

import Control.Exception (SomeException, try)
import Data.Text (Text)
import Data.Text qualified as T
import System.Directory (removeFile)
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import System.Info (os)
import System.Process.Typed (nullStream, proc, runProcess, runProcess_, setStderr)

-- | Copy-on-write capability for a filesystem.
data CowCapability
  = -- | CoW is supported with the given method
    CowSupported CowMethod
  | -- | CoW is not supported, with reason
    CowNotSupported Text
  deriving stock (Eq, Show)

-- | The method used for copy-on-write.
data CowMethod
  = -- | macOS clonefile via cp -c
    CowClonefile
  | -- | Linux reflink via cp --reflink
    CowReflink
  deriving stock (Eq, Show)

-- | Detect copy-on-write capability for a path.
--
-- This tests the actual filesystem by attempting a clone operation.
-- The test is performed once and the result can be cached.
--
-- On macOS: Tests @cp -c@ (uses clonefile syscall)
-- On Linux: Tests @cp --reflink=always@
detectCowCapability :: FilePath -> IO CowCapability
detectCowCapability _testDir = do
  withSystemTempDirectory "cow-test-" $ \tmpDir -> do
    let srcFile = tmpDir </> "src"
    let dstFile = tmpDir </> "dst"

    -- Create a small test file
    writeFile srcFile "test"

    -- Try the platform-specific clone command
    result <- tryClone srcFile dstFile

    -- Clean up
    removeFile srcFile `catch_` pure ()
    removeFile dstFile `catch_` pure ()

    pure result
  where
    catch_ :: IO a -> IO a -> IO a
    catch_ action fallback = do
      result <- try @SomeException action
      case result of
        Left _ -> fallback
        Right a -> pure a

    tryClone :: FilePath -> FilePath -> IO CowCapability
    tryClone src dst = case os of
      "darwin" -> tryMacOSClone src dst
      "linux" -> tryLinuxReflink src dst
      _ -> pure $ CowNotSupported $ "Unsupported OS: " <> T.pack os

    tryMacOSClone :: FilePath -> FilePath -> IO CowCapability
    tryMacOSClone src dst = do
      -- cp -c uses clonefile() on macOS
      exitCode <- runProcess $ setStderr nullStream $ proc "cp" ["-c", src, dst]
      case exitCode of
        ExitSuccess -> pure $ CowSupported CowClonefile
        ExitFailure _ -> pure $ CowNotSupported "clonefile not supported on this filesystem"

    tryLinuxReflink :: FilePath -> FilePath -> IO CowCapability
    tryLinuxReflink src dst = do
      -- cp --reflink=always fails if reflink is not supported
      exitCode <- runProcess $ setStderr nullStream $ proc "cp" ["--reflink=always", src, dst]
      case exitCode of
        ExitSuccess -> pure $ CowSupported CowReflink
        ExitFailure _ -> pure $ CowNotSupported "reflink not supported on this filesystem"

-- | Copy a directory, using copy-on-write if the capability is available.
copyDirectory :: CowCapability -> FilePath -> FilePath -> IO (Either Text ())
copyDirectory capability src dst = case capability of
  CowSupported method -> copyDirectoryCoW method src dst
  CowNotSupported _ -> copyDirectoryRegular src dst

-- | Copy a directory using copy-on-write.
copyDirectoryCoW :: CowMethod -> FilePath -> FilePath -> IO (Either Text ())
copyDirectoryCoW method src dst = do
  result <- try $ runCopy method
  case result of
    Left (_ :: SomeException) ->
      -- Fall back to regular copy on failure
      copyDirectoryRegular src dst
    Right () ->
      pure $ Right ()
  where
    runCopy :: CowMethod -> IO ()
    runCopy = \case
      CowClonefile ->
        -- macOS: use cp -cR for recursive copy-on-write
        runProcess_ $ proc "cp" ["-cR", src, dst]
      CowReflink ->
        -- Linux: use cp --reflink=auto -R (auto falls back gracefully)
        runProcess_ $ proc "cp" ["--reflink=auto", "-R", src, dst]

-- | Copy a directory using regular (non-CoW) copy.
copyDirectoryRegular :: FilePath -> FilePath -> IO (Either Text ())
copyDirectoryRegular src dst = do
  result <- try $ runProcess_ $ proc "cp" ["-R", src, dst]
  case result of
    Left (ex :: SomeException) ->
      pure $ Left $ T.pack $ show ex
    Right () ->
      pure $ Right ()