{-| Module : Codec.Archive.Tar Maintainer : Brandon Chinn Stability : experimental Portability : portable The Haskell @tar@ library has a lot of bugs, including: * Does not preserve executability (https://github.com/haskell/tar/issues/25) * Does not preserve symbolic links (https://github.com/haskell/tar/issues/34) * Does not allow symbolic links to `..` in a nested directory (https://github.com/haskell/tar/issues/32) Because of these bugs, we will be using the @tar@ Unix command, which should be commonly available on most systems. This module should be deprecated when the Haskell @tar@ library is updated. Note that this module contains the same security considerations as the @tar@ Unix command. Follow the same security guidelines you would with the @tar@ Unix command when using this module. -} {-# LANGUAGE LambdaCase #-} module Codec.Archive.Tar where import Control.DeepSeq (force) import Control.Exception (evaluate) import qualified Data.ByteString.Lazy as BS import GHC.IO.Handle (hClose) import Prelude hiding (read) import System.Directory (createDirectoryIfMissing, removeFile) import System.Exit (ExitCode(..), exitWith) import System.PosixCompat.Temp (mkstemp) import System.Process (readProcessWithExitCode) -- | Runs the UNIX @tar@ command with the given arguments. tar :: [String] -> IO () tar args = readProcessWithExitCode "tar" args "" >>= \case (ExitSuccess, _, _) -> return () (ExitFailure n, _, _) -> exitWith $ ExitFailure n create :: FilePath -> FilePath -> [FilePath] -> IO () create archive base paths = BS.writeFile archive . write =<< pack base paths extract :: FilePath -> FilePath -> IO () extract dir archive = unpack dir . read =<< BS.readFile archive newtype TarArchive = TarArchive { unTar :: BS.ByteString } read :: BS.ByteString -> TarArchive read = TarArchive write :: TarArchive -> BS.ByteString write = unTar pack :: FilePath -> [FilePath] -> IO TarArchive pack base paths = do (archive, h) <- mkstemp "tar-pack" hClose h tar $ ["-cf", archive, "-C", base] ++ paths contents <- evaluate . force =<< BS.readFile archive removeFile archive return $ TarArchive contents unpack :: FilePath -> TarArchive -> IO () unpack dir (TarArchive contents) = do (archive, h) <- mkstemp "tar-unpack" hClose h BS.writeFile archive contents createDirectoryIfMissing True dir tar ["-xf", archive, "-C", dir] removeFile archive