packages feed

tar-conduit 0.1.1 → 0.2.0

raw patch · 9 files changed

+1212/−119 lines, 9 filesdep +conduitdep +containersdep +criteriondep ~basedep ~conduit-combinatorsnew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: conduit, containers, criterion, deepseq, directory, filepath, hspec, tar-conduit, time, unix, unix-compat, weigh

Dependency ranges changed: base, conduit-combinators

API changes (from Hackage documentation)

- Data.Conduit.Tar: BadChecksum :: !Offset -> TarException
- Data.Conduit.Tar: BadTrailer :: !Offset -> TarException
- Data.Conduit.Tar: ChunkException :: TarException -> TarChunk
- Data.Conduit.Tar: ChunkHeader :: Header -> TarChunk
- Data.Conduit.Tar: ChunkPayload :: !Offset -> !ByteString -> TarChunk
- Data.Conduit.Tar: FTBlockSpecial :: FileType
- Data.Conduit.Tar: FTCharacterSpecial :: FileType
- Data.Conduit.Tar: FTDirectory :: FileType
- Data.Conduit.Tar: FTFifo :: FileType
- Data.Conduit.Tar: FTHardLink :: FileType
- Data.Conduit.Tar: FTNormal :: FileType
- Data.Conduit.Tar: FTOther :: !Word8 -> FileType
- Data.Conduit.Tar: FTSymbolicLink :: FileType
- Data.Conduit.Tar: Header :: !Offset -> !Offset -> !ShortByteString -> !CMode -> !Int -> !Int -> !Size -> !Int64 -> !Word8 -> !ShortByteString -> !ShortByteString -> !Int -> !Int -> !ShortByteString -> Header
- Data.Conduit.Tar: IncompleteHeader :: !Offset -> TarException
- Data.Conduit.Tar: IncompletePayload :: !Offset -> !Size -> TarException
- Data.Conduit.Tar: InvalidHeader :: !Offset -> TarException
- Data.Conduit.Tar: NoMoreHeaders :: TarException
- Data.Conduit.Tar: ShortTrailer :: !Offset -> TarException
- Data.Conduit.Tar: UnexpectedPayload :: !Offset -> TarException
- Data.Conduit.Tar: [headerDeviceMajor] :: Header -> !Int
- Data.Conduit.Tar: [headerDeviceMinor] :: Header -> !Int
- Data.Conduit.Tar: [headerFileMode] :: Header -> !CMode
- Data.Conduit.Tar: [headerFileNamePrefix] :: Header -> !ShortByteString
- Data.Conduit.Tar: [headerFileNameSuffix] :: Header -> !ShortByteString
- Data.Conduit.Tar: [headerGroupId] :: Header -> !Int
- Data.Conduit.Tar: [headerGroupName] :: Header -> !ShortByteString
- Data.Conduit.Tar: [headerLinkIndicator] :: Header -> !Word8
- Data.Conduit.Tar: [headerOffset] :: Header -> !Offset
- Data.Conduit.Tar: [headerOwnerId] :: Header -> !Int
- Data.Conduit.Tar: [headerOwnerName] :: Header -> !ShortByteString
- Data.Conduit.Tar: [headerPayloadOffset] :: Header -> !Offset
- Data.Conduit.Tar: [headerPayloadSize] :: Header -> !Size
- Data.Conduit.Tar: [headerTime] :: Header -> !Int64
- Data.Conduit.Tar: data FileType
- Data.Conduit.Tar: data Header
- Data.Conduit.Tar: data TarChunk
- Data.Conduit.Tar: data TarException
- Data.Conduit.Tar: instance GHC.Classes.Eq Data.Conduit.Tar.FileType
- Data.Conduit.Tar: instance GHC.Exception.Exception Data.Conduit.Tar.TarException
- Data.Conduit.Tar: instance GHC.Show.Show Data.Conduit.Tar.FileType
- Data.Conduit.Tar: instance GHC.Show.Show Data.Conduit.Tar.Header
- Data.Conduit.Tar: instance GHC.Show.Show Data.Conduit.Tar.TarChunk
- Data.Conduit.Tar: instance GHC.Show.Show Data.Conduit.Tar.TarException
- Data.Conduit.Tar: type Offset = Int
- Data.Conduit.Tar: type Size = Int
+ Data.Conduit.Tar: createTarball :: FilePath -> [FilePath] -> IO ()
+ Data.Conduit.Tar: extractTarball :: FilePath -> Maybe FilePath -> IO ()
+ Data.Conduit.Tar: filePathConduit :: MonadResource m => ConduitM FilePath (Either FileInfo ByteString) m ()
+ Data.Conduit.Tar: restoreFile :: (MonadResource m) => FileInfo -> ConduitM ByteString (IO ()) m ()
+ Data.Conduit.Tar: restoreFileInto :: MonadResource m => FilePath -> FileInfo -> ConduitM ByteString (IO ()) m ()
+ Data.Conduit.Tar: tar :: MonadResource m => ConduitM (Either FileInfo ByteString) ByteString m FileOffset
+ Data.Conduit.Tar: tarEntries :: MonadResource m => ConduitM (Either Header ByteString) ByteString m FileOffset
+ Data.Conduit.Tar: tarFilePath :: MonadResource m => ConduitM FilePath ByteString m FileOffset
+ Data.Conduit.Tar: untarWithFinalizers :: (MonadThrow m, MonadIO m) => (FileInfo -> ConduitM ByteString (IO ()) m ()) -> ConduitM ByteString c m ()
+ Data.Conduit.Tar: writeTarball :: Handle -> [FilePath] -> IO ()
+ Data.Conduit.Tar.Types: BadChecksum :: !FileOffset -> TarException
+ Data.Conduit.Tar.Types: BadTrailer :: !FileOffset -> TarException
+ Data.Conduit.Tar.Types: ChunkException :: TarException -> TarChunk
+ Data.Conduit.Tar.Types: ChunkHeader :: Header -> TarChunk
+ Data.Conduit.Tar.Types: ChunkPayload :: !FileOffset -> !ByteString -> TarChunk
+ Data.Conduit.Tar.Types: FTBlockSpecial :: FileType
+ Data.Conduit.Tar.Types: FTCharacterSpecial :: FileType
+ Data.Conduit.Tar.Types: FTDirectory :: FileType
+ Data.Conduit.Tar.Types: FTFifo :: FileType
+ Data.Conduit.Tar.Types: FTHardLink :: FileType
+ Data.Conduit.Tar.Types: FTNormal :: FileType
+ Data.Conduit.Tar.Types: FTOther :: !Word8 -> FileType
+ Data.Conduit.Tar.Types: FTSymbolicLink :: !ByteString -> FileType
+ Data.Conduit.Tar.Types: FileInfo :: !ByteString -> !UserID -> !ByteString -> !GroupID -> !ByteString -> !FileMode -> !FileOffset -> !FileType -> !EpochTime -> FileInfo
+ Data.Conduit.Tar.Types: FileNameTooLong :: !FileInfo -> TarCreateException
+ Data.Conduit.Tar.Types: FileTypeError :: !FileOffset -> !Char -> !String -> TarException
+ Data.Conduit.Tar.Types: Header :: !FileOffset -> !FileOffset -> !ShortByteString -> !CMode -> !UserID -> !GroupID -> !FileOffset -> !EpochTime -> !Word8 -> !ShortByteString -> !ShortByteString -> !ShortByteString -> !ShortByteString -> !DeviceID -> !DeviceID -> !ShortByteString -> Header
+ Data.Conduit.Tar.Types: IncompleteHeader :: !FileOffset -> TarException
+ Data.Conduit.Tar.Types: IncompletePayload :: !FileOffset -> !ByteCount -> TarException
+ Data.Conduit.Tar.Types: InvalidHeader :: !FileOffset -> TarException
+ Data.Conduit.Tar.Types: NoMoreHeaders :: TarException
+ Data.Conduit.Tar.Types: ShortTrailer :: !FileOffset -> TarException
+ Data.Conduit.Tar.Types: TarCreationError :: !String -> TarCreateException
+ Data.Conduit.Tar.Types: UnexpectedPayload :: !FileOffset -> TarException
+ Data.Conduit.Tar.Types: [fileGroupId] :: FileInfo -> !GroupID
+ Data.Conduit.Tar.Types: [fileGroupName] :: FileInfo -> !ByteString
+ Data.Conduit.Tar.Types: [fileModTime] :: FileInfo -> !EpochTime
+ Data.Conduit.Tar.Types: [fileMode] :: FileInfo -> !FileMode
+ Data.Conduit.Tar.Types: [filePath] :: FileInfo -> !ByteString
+ Data.Conduit.Tar.Types: [fileSize] :: FileInfo -> !FileOffset
+ Data.Conduit.Tar.Types: [fileType] :: FileInfo -> !FileType
+ Data.Conduit.Tar.Types: [fileUserId] :: FileInfo -> !UserID
+ Data.Conduit.Tar.Types: [fileUserName] :: FileInfo -> !ByteString
+ Data.Conduit.Tar.Types: [headerDeviceMajor] :: Header -> !DeviceID
+ Data.Conduit.Tar.Types: [headerDeviceMinor] :: Header -> !DeviceID
+ Data.Conduit.Tar.Types: [headerFileMode] :: Header -> !CMode
+ Data.Conduit.Tar.Types: [headerFileNamePrefix] :: Header -> !ShortByteString
+ Data.Conduit.Tar.Types: [headerFileNameSuffix] :: Header -> !ShortByteString
+ Data.Conduit.Tar.Types: [headerGroupId] :: Header -> !GroupID
+ Data.Conduit.Tar.Types: [headerGroupName] :: Header -> !ShortByteString
+ Data.Conduit.Tar.Types: [headerLinkIndicator] :: Header -> !Word8
+ Data.Conduit.Tar.Types: [headerLinkName] :: Header -> !ShortByteString
+ Data.Conduit.Tar.Types: [headerMagicVersion] :: Header -> !ShortByteString
+ Data.Conduit.Tar.Types: [headerOffset] :: Header -> !FileOffset
+ Data.Conduit.Tar.Types: [headerOwnerId] :: Header -> !UserID
+ Data.Conduit.Tar.Types: [headerOwnerName] :: Header -> !ShortByteString
+ Data.Conduit.Tar.Types: [headerPayloadOffset] :: Header -> !FileOffset
+ Data.Conduit.Tar.Types: [headerPayloadSize] :: Header -> !FileOffset
+ Data.Conduit.Tar.Types: [headerTime] :: Header -> !EpochTime
+ Data.Conduit.Tar.Types: data FileInfo
+ Data.Conduit.Tar.Types: data FileType
+ Data.Conduit.Tar.Types: data Header
+ Data.Conduit.Tar.Types: data TarChunk
+ Data.Conduit.Tar.Types: data TarCreateException
+ Data.Conduit.Tar.Types: data TarException
+ Data.Conduit.Tar.Types: instance GHC.Classes.Eq Data.Conduit.Tar.Types.FileInfo
+ Data.Conduit.Tar.Types: instance GHC.Classes.Eq Data.Conduit.Tar.Types.FileType
+ Data.Conduit.Tar.Types: instance GHC.Exception.Exception Data.Conduit.Tar.Types.TarCreateException
+ Data.Conduit.Tar.Types: instance GHC.Exception.Exception Data.Conduit.Tar.Types.TarException
+ Data.Conduit.Tar.Types: instance GHC.Show.Show Data.Conduit.Tar.Types.FileInfo
+ Data.Conduit.Tar.Types: instance GHC.Show.Show Data.Conduit.Tar.Types.FileType
+ Data.Conduit.Tar.Types: instance GHC.Show.Show Data.Conduit.Tar.Types.Header
+ Data.Conduit.Tar.Types: instance GHC.Show.Show Data.Conduit.Tar.Types.TarChunk
+ Data.Conduit.Tar.Types: instance GHC.Show.Show Data.Conduit.Tar.Types.TarCreateException
+ Data.Conduit.Tar.Types: instance GHC.Show.Show Data.Conduit.Tar.Types.TarException
+ Data.Conduit.Tar.Types: type ByteCount = CSize
+ Data.Conduit.Tar.Types: type DeviceID = CDev
+ Data.Conduit.Tar.Types: type EpochTime = CTime
+ Data.Conduit.Tar.Types: type FileOffset = COff
+ Data.Conduit.Tar.Types: type GroupID = CGid
+ Data.Conduit.Tar.Types: type UserID = CUid
- Data.Conduit.Tar: untar :: Monad m => ConduitM ByteString TarChunk m ()
+ Data.Conduit.Tar: untar :: MonadThrow m => (FileInfo -> ConduitM ByteString o m ()) -> ConduitM ByteString o m ()

Files

ChangeLog.md view
@@ -1,3 +1,12 @@+## 0.2.0 - 2018-01-23++ * Implemented tarball creation+ * Introduced `FileInfo` datatype that makes it easier to work with archives by automatically+   handling tar specific blocks and applying them to `FileInfo`.+ * Full support for `ustar` format.+ * Support for some GNU tar features: long file name, discardes.+ * Helper tar/untar functions for dealing with directly with the filesystem.+ ## 0.1.1 - 2017-05-31   * Allow checksums to have leading spaces ([PR 8](https://github.com/snoyberg/tar-conduit/pull/8))
src/Data/Conduit/Tar.hs view
@@ -1,76 +1,75 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE BangPatterns       #-}+{-# LANGUAGE CPP                #-}+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE RecordWildCards    #-} {-| This module is about stream-processing tar archives. It is currently not very well tested. See the documentation of 'withEntries' for an usage sample. -} module Data.Conduit.Tar     ( -- * Basic functions-      untar+      tar+    , tarEntries+    , untar+    , untarWithFinalizers+    , restoreFile+    , restoreFileInto     , withEntry     , withEntries       -- * Helper functions     , headerFileType     , headerFilePath+      -- ** Creation+    , tarFilePath+    , filePathConduit+      -- * Directly on files+    , createTarball+    , writeTarball+    , extractTarball       -- * Types-    , Header (..)-    , TarChunk (..)-    , TarException (..)-    , Offset-    , Size-    , FileType (..)+    , module Data.Conduit.Tar.Types     ) where -import Conduit-import Control.Exception (Exception, assert)-import Control.Monad (unless)-import Data.ByteString (ByteString)-import Data.Typeable (Typeable)-import qualified Data.ByteString        as S-import qualified Data.ByteString.Char8  as S8-import qualified Data.ByteString.Unsafe as BU-import System.Posix.Types (CMode)-import Data.Word (Word8)-import Data.Int (Int64)-import Data.ByteString.Short (ShortByteString, toShort, fromShort)-import Data.Monoid ((<>))+import           Conduit                  as C+import           Control.Exception        (assert)+import           Control.Monad            (unless, void)+import           Data.ByteString          (ByteString)+import qualified Data.ByteString          as S+import           Data.ByteString.Builder+import qualified Data.ByteString.Char8    as S8+import qualified Data.ByteString.Lazy     as SL+import           Data.ByteString.Short    (ShortByteString, fromShort, toShort)+import qualified Data.ByteString.Short    as SS+import qualified Data.ByteString.Unsafe   as BU+import           Data.Foldable            (foldr')+import           Data.Monoid              ((<>), mempty)+import           Foreign.C.Types          (CTime (..))+import           System.Directory         (createDirectoryIfMissing,+                                           getCurrentDirectory)+import           System.FilePath+import           System.IO  #if !MIN_VERSION_base(4,8,0)-import Control.Applicative ((<*))+import           Control.Applicative      ((<*)) #endif -data Header = Header-    { headerOffset         :: !Offset-    , headerPayloadOffset  :: !Offset-    , headerFileNameSuffix :: !ShortByteString-    , headerFileMode       :: !CMode-    , headerOwnerId        :: !Int-    , headerGroupId        :: !Int-    , headerPayloadSize    :: !Size-    , headerTime           :: !Int64-    , headerLinkIndicator  :: !Word8-    , headerOwnerName      :: !ShortByteString-    , headerGroupName      :: !ShortByteString-    , headerDeviceMajor    :: !Int-    , headerDeviceMinor    :: !Int-    , headerFileNamePrefix :: !ShortByteString-    }-    deriving Show+import           Data.Conduit.Tar.Types+#ifdef WINDOWS+import           Data.Conduit.Tar.Windows+#else+import           Data.Conduit.Tar.Unix+#endif ++headerFilePathBS :: Header -> S.ByteString+headerFilePathBS Header {..} =+    if SS.length headerFileNamePrefix > 0+        then S.concat+                 [fromShort headerFileNamePrefix, pathSeparatorS, fromShort headerFileNameSuffix]+        else fromShort headerFileNameSuffix+ headerFilePath :: Header -> FilePath-headerFilePath h = S8.unpack $ fromShort-                 $ headerFileNamePrefix h <> headerFileNameSuffix h+headerFilePath = S8.unpack . headerFilePathBS -data FileType-    = FTNormal-    | FTHardLink-    | FTSymbolicLink-    | FTCharacterSpecial-    | FTBlockSpecial-    | FTDirectory-    | FTFifo-    | FTOther !Word8-    deriving (Show, Eq)  headerFileType :: Header -> FileType headerFileType h =@@ -78,39 +77,14 @@         0  -> FTNormal         48 -> FTNormal         49 -> FTHardLink-        50 -> FTSymbolicLink+        50 -> FTSymbolicLink (fromShort (headerLinkName h))         51 -> FTCharacterSpecial         52 -> FTBlockSpecial         53 -> FTDirectory         54 -> FTFifo         x  -> FTOther x -type Offset = Int-type Size = Int--data TarChunk-    = ChunkHeader Header-    | ChunkPayload !Offset !ByteString-    | ChunkException TarException-    deriving Show---- | This the the exception type that is used in this module.------ More constructors are susceptible to be added without bumping the major--- version of this module.-data TarException-    = NoMoreHeaders-    | UnexpectedPayload !Offset-    | IncompleteHeader  !Offset-    | IncompletePayload !Offset !Size-    | ShortTrailer      !Offset-    | BadTrailer        !Offset-    | InvalidHeader     !Offset-    | BadChecksum       !Offset-    deriving (Show, Typeable)-instance Exception TarException--parseHeader :: Offset -> ByteString -> Either TarException Header+parseHeader :: FileOffset -> ByteString -> Either TarException Header parseHeader offset bs = assert (S.length bs == 512) $ do     let checksumBytes = S.take 8 $ S.drop 148 bs         expectedChecksum = parseOctal checksumBytes@@ -124,8 +98,10 @@         , headerOwnerId        = getOctal 108 8         , headerGroupId        = getOctal 116 8         , headerPayloadSize    = getOctal 124 12-        , headerTime           = getOctal 136 12+        , headerTime           = CTime $ getOctal 136 12         , headerLinkIndicator  = BU.unsafeIndex bs 156+        , headerLinkName       = getShort 157 100+        , headerMagicVersion   = toShort $ S.take 8 $ S.drop 257 bs         , headerOwnerName      = getShort 265 32         , headerGroupName      = getShort 297 32         , headerDeviceMajor    = getOctal 329 8@@ -150,8 +126,8 @@     zero = 48     seven = 55 -untar :: Monad m => ConduitM ByteString TarChunk m ()-untar =+untarChunks :: Monad m => ConduitM ByteString TarChunk m ()+untarChunks =     loop 0   where     loop !offset = assert (offset `mod` 512 == 0) $ do@@ -179,9 +155,9 @@                         yield $ ChunkHeader h                         offset' <- payloads (offset + 512) $ headerPayloadSize h                         let expectedOffset = offset + 512 + headerPayloadSize h +-                                (case (512 - (headerPayloadSize h `mod` 512)) of+                                (case 512 - (headerPayloadSize h `mod` 512) of                                     512 -> 0-                                    x -> x)+                                    x   -> x)                         assert (offset' == expectedOffset) (loop offset')             _ -> do                 leftover bs@@ -191,23 +167,24 @@         let padding =                 case offset `mod` 512 of                     0 -> 0-                    x -> 512 - x+                    x -> 512 - fromIntegral x         takeCE padding .| sinkNull-        return $! offset + padding+        return $! offset + fromIntegral padding     payloads !offset !size = do         mbs <- await         case mbs of             Nothing -> do-                yield $ ChunkException $ IncompletePayload offset size+                yield $ ChunkException $ IncompletePayload offset $ fromIntegral size                 return offset             Just bs -> do-                let (x, y) = S.splitAt size bs+                let (x, y) = S.splitAt (fromIntegral (min size (fromIntegral (maxBound :: Int)))) bs                 yield $ ChunkPayload offset x-                let size' = size - S.length x-                    offset' = offset + S.length x+                let size' = size - fromIntegral (S.length x)+                    offset' = offset + fromIntegral (S.length x)                 unless (S.null y) (leftover y)                 payloads offset' size' + -- | Process a single tar entry. See 'withEntries' for more details. withEntry :: MonadThrow m           => (Header -> ConduitM ByteString o m r)@@ -216,20 +193,24 @@     mc <- await     case mc of         Nothing -> throwM NoMoreHeaders-        Just (ChunkHeader h) -> payloads .| (inner h <* sinkNull)+        Just (ChunkHeader h) -> payloadsConduit .| (inner h <* sinkNull)         Just x@(ChunkPayload offset _bs) -> do             leftover x             throwM $ UnexpectedPayload offset         Just (ChunkException e) -> throwM e-  where-    payloads = do-        mx <- await-        case mx of-            Just (ChunkPayload _ bs) -> yield bs >> payloads-            Just x@ChunkHeader{} -> leftover x-            Just (ChunkException e) -> throwM e-            Nothing -> return () ++payloadsConduit :: MonadThrow m+               => ConduitM TarChunk ByteString m ()+payloadsConduit = do+    mx <- await+    case mx of+        Just (ChunkPayload _ bs) -> yield bs >> payloadsConduit+        Just x@ChunkHeader {}    -> leftover x+        Just (ChunkException e)  -> throwM e+        Nothing                  -> return ()++ {-| This function handles each entry of the tar archive according to the behaviour of the function passed as first argument. @@ -237,17 +218,17 @@  > import qualified Crypto.Hash.Conduit as CH > import qualified Data.Conduit.Tar    as CT-> +> > import Conduit > import Crypto.Hash (Digest, SHA256) > import Control.Monad (when) > import Data.Conduit.Zlib (ungzip) > import Data.ByteString (ByteString)-> +> > filedigests :: FilePath -> IO () > filedigests fp = runConduitRes (  sourceFileBS fp          -- read the raw file >                                .| ungzip                   -- gunzip->                                .| CT.untar                 -- decode the tar archive+>                                .| CT.untarChunks           -- decode the tar archive >                                .| CT.withEntries hashentry -- process each file >                                .| printC                   -- print the results >                                )@@ -270,3 +251,492 @@             => (Header -> ConduitM ByteString o m ())             -> ConduitM TarChunk o m () withEntries = peekForever . withEntry+++-- | Extract a tarball, similarly to `withEntries`, but instead of dealing directly with tar format,+-- this conduit allows you to work directly on file abstractions `FileInfo`. For now support is+-- minimal:+--+-- * Old v7 tar format.+-- * ustar: POSIX 1003.1-1988 format+-- * and only portions of GNU format:+--   * 'L' type - long file names, but only up to 4096 chars to prevent DoS attack+--   * other types are simply discarded+--+-- /Note/ - Here is a really good reference for specifics of different tar formats:+-- <https://github.com/libarchive/libarchive/wiki/ManPageTar5>+withFileInfo :: MonadThrow m+             => (FileInfo -> ConduitM ByteString o m ())+             -> ConduitM TarChunk o m ()+withFileInfo inner = go+  where+    go = do+        mc <- await+        case mc of+            Nothing -> return ()+            Just (ChunkHeader h)+                | headerLinkIndicator h >= 55 -> do+                    if (headerMagicVersion h == gnuTarMagicVersion)+                        then handleGnuTarHeader h .| go+                        else go+            Just (ChunkHeader h) -> do+                payloadsConduit .| (inner (fileInfoFromHeader h) <* sinkNull)+                go+            Just x@(ChunkPayload offset _bs) -> do+                leftover x+                throwM $ UnexpectedPayload offset+            Just (ChunkException e) -> throwM e++-- | Take care of custom GNU tar format.+handleGnuTarHeader :: MonadThrow m+                   => Header+                   -> ConduitM TarChunk TarChunk m ()+handleGnuTarHeader h = do+    case headerLinkIndicator h of+        76 -> do+            let pSize = headerPayloadSize h+            -- guard against names that are too long in order to prevent a DoS attack on unbounded+            -- file names+            unless (0 < pSize && pSize <= 4096) $+                throwM $+                FileTypeError (headerPayloadOffset h) 'L' $ "Filepath is too long: " ++ show pSize+            longFileNameBuilder <- payloadsConduit .| sinkBuilder+            let longFileName = SL.toStrict . SL.init . toLazyByteString $ longFileNameBuilder+            mcNext <- await+            case mcNext of+                Just (ChunkHeader nh) -> do+                    unless (S.isPrefixOf (fromShort (headerFileNameSuffix nh)) longFileName) $+                        throwM $+                        FileTypeError (headerPayloadOffset nh) 'L' $+                        "Long filename doesn't match the original."+                    yield+                        (ChunkHeader $+                         nh+                         { headerFileNameSuffix = toShort longFileName+                         , headerFileNamePrefix = SS.empty+                         })+                Just c@(ChunkPayload offset _) -> do+                    leftover c+                    throwM $ InvalidHeader offset+                Just (ChunkException exc) -> throwM exc+                Nothing -> throwM NoMoreHeaders+        83 -> do+            payloadsConduit .| sinkNull -- discard sparse files payload+            -- TODO : Implement restoring of sparse files+        _ -> return ()++++-- | Just like `withFileInfo`, but works directly on the stream of bytes.+untar :: MonadThrow m+      => (FileInfo -> ConduitM ByteString o m ())+      -> ConduitM ByteString o m ()+untar inner = untarChunks .| withFileInfo inner+++-- | Just like `untar`, except that each `FileInfo` handling function can produce a finalizing+-- action, all of which will be executed after the whole tarball has been processed in the opposite+-- order. Very useful with `restoreFile` and `restoreFileInto`, since they restore direcory+-- modification timestamps only after files have been fully written to disk.+untarWithFinalizers ::+       (MonadThrow m, MonadIO m)+    => (FileInfo -> ConduitM ByteString (IO ()) m ())+    -> ConduitM ByteString c m ()+untarWithFinalizers inner = do+    finilizers <- untar inner .| foldlC (>>) (return ())+    liftIO finilizers+++--------------------------------------------------------------------------------+-- Create a tar file -----------------------------------------------------------+--------------------------------------------------------------------------------++gnuTarMagicVersion :: ShortByteString+gnuTarMagicVersion = toShort (S8.pack "ustar  \NUL")++ustarMagicVersion :: ShortByteString+ustarMagicVersion = toShort (S8.pack "ustar\NUL00")++blockSize :: FileOffset+blockSize = 512++terminatorBlock :: ByteString+terminatorBlock = S.replicate (fromIntegral (2 * blockSize)) 0++defHeader :: FileOffset -> Header+defHeader offset = Header+          { headerOffset = offset+          , headerPayloadOffset = offset + 512+          , headerFileNameSuffix = SS.empty+          , headerFileMode = 0o644+          , headerOwnerId = 0+          , headerGroupId = 0+          , headerPayloadSize = 0+          , headerTime = 0+          , headerLinkIndicator = 0+          , headerLinkName = SS.empty+          , headerMagicVersion = ustarMagicVersion+          , headerOwnerName = "root"+          , headerGroupName = "root"+          , headerDeviceMajor = 0+          , headerDeviceMinor = 0+          , headerFileNamePrefix = SS.empty+          }+++headerFromFileInfo :: MonadThrow m =>+                      FileOffset -- ^ Starting offset within the tarball. Must+                      -- be multiple of 512, otherwise error.+                   -> FileInfo -- ^ File info.+                   -> m (Either TarCreateException Header)+headerFromFileInfo offset fi = do+    unless (offset `mod` 512 == 0) $+        throwM $ TarCreationError $ "Offset must always be a multiple of 512"+    let (prefix, suffix) = splitPathAt 100 $ filePath fi+    if (SS.length prefix > 155)+        then return $ Left $ FileNameTooLong fi+        else do+            (payloadSize, linkName, linkIndicator) <-+                case fileType fi of+                    FTNormal -> return (fileSize fi, SS.empty, 48)+                    FTSymbolicLink ln -> return (0, toShort ln, 50)+                    FTDirectory -> return (0, SS.empty, 53)+                    fty -> throwM $ TarCreationError $ "Unsupported file type: " ++ show fty+            return $+                Right+                    Header+                    { headerOffset = offset+                    , headerPayloadOffset = offset + 512+                    , headerFileNameSuffix = suffix+                    , headerFileMode = fileMode fi+                    , headerOwnerId = fileUserId fi+                    , headerGroupId = fileGroupId fi+                    , headerPayloadSize = payloadSize+                    , headerTime = fileModTime fi+                    , headerLinkIndicator = linkIndicator+                    , headerLinkName = linkName+                    , headerMagicVersion = ustarMagicVersion+                    , headerOwnerName = toShort $ fileUserName fi+                    , headerGroupName = toShort $ fileGroupName fi+                    , headerDeviceMajor = 0+                    , headerDeviceMinor = 0+                    , headerFileNamePrefix = prefix+                    }+++-- | Split a file path at the @n@ mark from the end, while still keeping the+-- split as a valid path, i.e split at a path separator only.+splitPathAt :: Int -> ByteString -> (ShortByteString, ShortByteString)+splitPathAt n fp+    | S.length fp <= n = (SS.empty, toShort fp)+    | otherwise =+        let sfp = S8.splitWith isPathSeparator fp+            sepWith p (tlen, prefix', suffix') =+                case S.length p + 1 + tlen of+                    tlen'+                        | tlen' <= n -> (tlen', prefix', p : suffix')+                    tlen' -> (tlen', p : prefix', suffix')+            (_, prefix, suffix) = foldr' sepWith (0, [], []) sfp+            toShortPath = toShort . S8.intercalate pathSeparatorS+        in (toShortPath prefix, toShortPath suffix)++packHeader :: MonadThrow m => Header -> m S.ByteString+packHeader header = do+    (left, right) <- packHeaderNoChecksum header+    let sumsl :: SL.ByteString -> Int+        sumsl = SL.foldl' (\ !acc !v -> acc + fromIntegral v) 0+    encChecksum <- encodeOctal 7 $ sumsl left + 32 * 8 + sumsl right+    return $+        SL.toStrict $+        toLazyByteString $ lazyByteString left <> encChecksum <> word8 0 <> lazyByteString right+++packHeaderNoChecksum :: MonadThrow m => Header -> m (SL.ByteString, SL.ByteString)+packHeaderNoChecksum Header {..} = do+    let CTime headerTime' = headerTime+    hNameSuffix <- encodeShort 100 headerFileNameSuffix+    hFileMode <- encodeOctal 7 headerFileMode+    hOwnerId <- encodeOctal 7 headerOwnerId+    hGroupId <- encodeOctal 7 headerGroupId+    hPayloadSize <- encodeOctal 11 headerPayloadSize+    hTime <- encodeOctal 11 headerTime'+    hLinkName <- encodeShort 100 headerLinkName+    hMagicVersion <- encodeShort 8 headerMagicVersion+    hOwnerName <- encodeShort 32 headerOwnerName+    hGroupName <- encodeShort 32 headerGroupName+    hDevMajor <- encodeDevice headerDeviceMajor+    hDevMinor <- encodeDevice headerDeviceMinor+    hNamePrefix <- encodeShort 155 headerFileNamePrefix+    return+        ( toLazyByteString $+          hNameSuffix <>+          hFileMode <> word8 0 <>+          hOwnerId <> word8 0 <>+          hGroupId <> word8 0 <>+          hPayloadSize <> word8 0 <>+          hTime <> word8 0+        , toLazyByteString $+          word8 headerLinkIndicator <>+          hLinkName <>+          hMagicVersion <>+          hOwnerName <>+          hGroupName <>+          hDevMajor <> word8 0 <>+          hDevMinor <> word8 0 <>+          hNamePrefix <>+          byteString (S.replicate 12 0)+        )+  where+    encodeDevice 0     = return $ byteString $ S.replicate 7 0+    encodeDevice devid = encodeOctal 7 devid+++-- | Encode a `ShortByteString` with an exact length, NUL terminating if it is+-- shorter, but throwing `TarCreationError` if it is longer.+encodeShort :: MonadThrow m => Int -> ShortByteString -> m Builder+encodeShort !len !sbs+    | lenShort <= len = return $ shortByteString sbs <> byteString (S.replicate (len - lenShort) 0)+    | otherwise =+        throwM $+        TarCreationError $ "Can't fit '" ++ S8.unpack (fromShort sbs) ++ "' into the tar header"+  where+    lenShort = SS.length sbs+++-- | Encode a number in 8base padded with zeros. Throws `TarCreationError` when overflows.+encodeOctal :: (Show a, Integral a, MonadThrow m) => Int -> a -> m Builder+encodeOctal !len !val = go 0 val mempty+  where+    go !n !cur !acc+      | cur == 0 =+        if n < len+          then return $ byteString (S.replicate (len - n) 48) <> acc+          else return acc+      | n < len =+        let !(q, r) = cur `quotRem` 8+        in go (n + 1) q (word8 (fromIntegral r + 48) <> acc)+      | otherwise =+        throwM $+        TarCreationError $+        "<encodeOctal>: Tar value overflow (for maxLen " ++ show len ++ "): " ++ show val++++-- | Produce a ByteString chunk with NUL characters of the size needed to get up+-- to the next 512 byte mark in respect to the supplied offset and return that+-- offset incremented to that mark.+yieldNulPadding :: Monad m => FileOffset -> ConduitM i ByteString m FileOffset+yieldNulPadding n = do+    let pad = blockSize - (n `mod` blockSize)+    if pad /= blockSize+        then yield (S.replicate (fromIntegral pad) 0) >> return (n + pad)+        else return n+++++-- | Handle tar payload, while validating its size and padding it to the full+-- block at the end.+tarPayload :: MonadThrow m =>+              FileOffset -- ^ Received payload size+           -> Header -- ^ Header for the file that we are currently recieving the payload for+           -> (FileOffset -> ConduitM (Either a ByteString) ByteString m FileOffset)+           -- ^ Continuation for after all payload has been received+           -> ConduitM (Either a ByteString) ByteString m FileOffset+tarPayload size header cont+    | size == headerPayloadSize header = cont (headerOffset header + blockSize)+    | otherwise = go size+  where+    go prevSize = do+        eContent <- await+        case eContent of+            Just h@(Left _) -> do+                leftover h+                throwM $ TarCreationError "Not enough payload."+            Just (Right content) -> do+                let nextSize = prevSize + fromIntegral (S.length content)+                unless (nextSize <= headerPayloadSize header) $+                    throwM $ TarCreationError "Too much payload"+                yield content+                if nextSize == headerPayloadSize header+                    then do+                        paddedSize <- yieldNulPadding nextSize+                        cont (headerPayloadOffset header + paddedSize)+                    else go nextSize+            Nothing -> throwM $ TarCreationError "Stream finished abruptly. Not enough payload."++++tarHeader :: MonadThrow m =>+             FileOffset -> ConduitM (Either Header ByteString) ByteString m FileOffset+tarHeader offset = do+    eContent <- await+    case eContent of+        Just c@(Right _) -> do+            leftover c+            throwM $ TarCreationError "Received payload without a corresponding Header."+        Just (Left header) -> do+            packHeader header >>= yield+            tarPayload 0 header tarHeader+        Nothing -> do+            yield terminatorBlock+            return $ offset + fromIntegral (S.length terminatorBlock)++++tarFileInfo :: MonadThrow m =>+               FileOffset -> ConduitM (Either FileInfo ByteString) ByteString m FileOffset+tarFileInfo offset = do+    eContent <- await+    case eContent of+        Just (Right _) ->+            throwM $ TarCreationError "Received payload without a corresponding FileInfo."+        Just (Left fi) -> do+            eHeader <- headerFromFileInfo offset fi+            case eHeader of+                Left (FileNameTooLong _) -> do+                    let fPath = filePath fi+                        fPathLen = fromIntegral (S.length fPath + 1)+                        pad =+                            case fPathLen `mod` blockSize of+                                0 -> 0+                                x -> blockSize - x+                    eHeader' <-+                        headerFromFileInfo+                            (offset + blockSize + fPathLen + pad)+                            (fi {filePath = S.take 100 fPath})+                    header <- either throwM return eHeader'+                    pHeader <- packHeader header+                    pFileNameHeader <-+                        packHeader $+                        (defHeader offset)+                        { headerFileNameSuffix = "././@LongLink"+                        , headerPayloadSize = fPathLen+                        , headerLinkIndicator = 76 -- 'L'+                        , headerMagicVersion = gnuTarMagicVersion+                        }+                    yield pFileNameHeader+                    yield fPath+                    yield $ S.replicate (fromIntegral pad + 1) 0+                    yield pHeader+                    tarPayload 0 header tarFileInfo+                Left exc -> throwM exc+                Right header -> do+                    packHeader header >>= yield+                    tarPayload 0 header tarFileInfo+        Nothing -> return offset++++-- | Create a tar archive by suppying a stream of `Left` `FileInfo`s. Whenever a+-- file type is `FTNormal`, it must be immediately followed by its content as+-- `Right` `ByteString`. The produced `ByteString` is in the raw tar format and+-- is properly terminated at the end, therefore it should no be modified+-- afterwards. Returned is the total size of the bytestring as a `FileOffset`.+tar :: MonadResource m =>+       ConduitM (Either FileInfo ByteString) ByteString m FileOffset+tar = do+    offset <- tarFileInfo 0+    yield terminatorBlock+    return $ offset + fromIntegral (S.length terminatorBlock)+++-- | Just like `tar`, except gives you the ability to work at a lower `Header`+-- level, versus more user friendly `FileInfo`. A deeper understanding of tar+-- format is necessary in order to work directly with `Header`s.+tarEntries :: MonadResource m =>+            ConduitM (Either Header ByteString) ByteString m FileOffset+tarEntries = do+    offset <- tarHeader 0+    yield terminatorBlock+    return $ offset + fromIntegral (S.length terminatorBlock)++++-- | Turn a stream of file paths into a stream of `FileInfo` and file+-- content. All paths will be decended into recursively.+filePathConduit :: MonadResource m =>+                   ConduitM FilePath (Either FileInfo ByteString) m ()+filePathConduit = do+    mfp <- await+    case mfp of+        Just fp -> do+            fi <- liftIO $ getFileInfo $ S8.pack fp+            case fileType fi of+                FTNormal -> do+                    yield (Left fi)+                    sourceFile (S8.unpack (filePath fi)) .| mapC Right+                FTSymbolicLink _ -> yield (Left fi)+                FTDirectory -> do+                    yield (Left fi)+                    sourceDirectory (S8.unpack (filePath fi)) .| filePathConduit+                fty -> do+                    leftover fp+                    throwM $ TarCreationError $ "Unsupported file type: " ++ show fty+            filePathConduit+        Nothing -> return ()+++-- | Recursively tar all of the files and directories. There will be no+-- conversion between relative and absolute paths, so just like with GNU @tar@+-- cli tool, it may be necessary to `setCurrentDirectory` in order to get the+-- paths relative. Using `filePathConduit` directly, while modifying the+-- `filePath`, would be another approach to handling the file paths.+tarFilePath :: MonadResource m => ConduitM FilePath ByteString m FileOffset+tarFilePath = filePathConduit .| tar+++-- | Uses `tarFilePath` to create a tarball, that will recursively include the+-- supplied list of all the files and directories+createTarball :: FilePath -- ^ File name for the tarball+              -> [FilePath] -- ^ List of files and directories to include in the tarball+              -> IO ()+createTarball tarfp dirs = do+    runConduitRes $ yieldMany dirs .| void tarFilePath .| sinkFile tarfp+++writeTarball :: Handle -- ^ Handle where created tarball will be written to+             -> [FilePath] -- ^ List of files and directories to include in the tarball+             -> IO ()+writeTarball tarHandle dirs = do+    runConduitRes $ yieldMany dirs .| void tarFilePath .| sinkHandle tarHandle+++pathSeparatorS :: ByteString+pathSeparatorS = S8.singleton pathSeparator+++fileInfoFromHeader :: Header -> FileInfo+fileInfoFromHeader header@(Header {..}) =+    FileInfo+    { filePath = headerFilePathBS header+    , fileUserId = headerOwnerId+    , fileUserName = fromShort headerOwnerName+    , fileGroupId = headerGroupId+    , fileGroupName = fromShort headerGroupName+    , fileMode = headerFileMode+    , fileSize = headerPayloadSize+    , fileType = headerFileType header+    , fileModTime = headerTime+    }+++-- | Extract a tarball.+extractTarball :: FilePath -- ^ Filename for the tarball+               -> Maybe FilePath -- ^ Folder where tarball should be extract+                                 -- to. Default is the current path+               -> IO ()+extractTarball tarfp mcd = do+    cd <- maybe getCurrentDirectory return mcd+    createDirectoryIfMissing True cd+    runConduitRes $ sourceFileBS tarfp .| untarWithFinalizers (restoreFileInto cd)+++-- | Restore all files into a folder. Absolute file paths will be turned into+-- relative to the supplied folder.+restoreFileInto :: MonadResource m =>+                   FilePath -> FileInfo -> ConduitM ByteString (IO ()) m ()+restoreFileInto cd fi =+    restoreFile fi {filePath = S8.pack (cd </> makeRelative "/" (S8.unpack (filePath fi)))}++
+ src/Data/Conduit/Tar/Types.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE CPP #-}+-- | Module contains all the types necessary for tarball processing.+module Data.Conduit.Tar.Types+    ( Header(..)+    , TarChunk(..)+    , TarException(..)+    , TarCreateException(..)+    , FileType(..)+    , FileInfo(..)+    , FileOffset+    , ByteCount+    , UserID+    , GroupID+    , DeviceID+    , EpochTime+    ) where++import           Control.Exception        (Exception)+import           Data.ByteString          (ByteString)+import           Data.ByteString.Short    (ShortByteString)+import           Data.Typeable+import           Data.Word++#if WINDOWS+import           System.PosixCompat.Types+#else+import           System.Posix.Types+#endif++data FileType+    = FTNormal+    | FTHardLink+    | FTSymbolicLink !ByteString+    | FTCharacterSpecial+    | FTBlockSpecial+    | FTDirectory+    | FTFifo+    | FTOther !Word8+    deriving (Show, Eq)+++data FileInfo = FileInfo+    { filePath      :: !ByteString -- ^ File path.+    , fileUserId    :: !UserID  -- ^ Unix user id.+    , fileUserName  :: !ByteString  -- ^ Unix user name.+    , fileGroupId   :: !GroupID -- ^ Unix group id.+    , fileGroupName :: !ByteString  -- ^ Unix group name.+    , fileMode      :: !FileMode -- ^ Unix file permissions+    , fileSize      :: !FileOffset -- ^ File size+    , fileType      :: !FileType  -- ^ File type. `FTNormal`, `FTSymbolicLink`+                                  -- and `FTDirectory` are the only ones+                                  -- supported for now+    , fileModTime   :: !EpochTime -- ^ File modification timestamp+    } deriving (Show, Eq)+++data Header = Header+    { headerOffset         :: !FileOffset+    , headerPayloadOffset  :: !FileOffset+    , headerFileNameSuffix :: !ShortByteString+    , headerFileMode       :: !CMode+    , headerOwnerId        :: !UserID+    , headerGroupId        :: !GroupID+    , headerPayloadSize    :: !FileOffset+    , headerTime           :: !EpochTime+    , headerLinkIndicator  :: !Word8+    , headerLinkName       :: !ShortByteString+    , headerMagicVersion   :: !ShortByteString+    , headerOwnerName      :: !ShortByteString+    , headerGroupName      :: !ShortByteString+    , headerDeviceMajor    :: !DeviceID+    , headerDeviceMinor    :: !DeviceID+    , headerFileNamePrefix :: !ShortByteString+    }+    deriving Show++++data TarChunk+    = ChunkHeader Header+    | ChunkPayload !FileOffset !ByteString+    | ChunkException TarException+    deriving Show++-- | This the the exception type that is used in this module.+--+-- More constructors are susceptible to be added without bumping the major+-- version of this module.+data TarException+    = NoMoreHeaders+    | UnexpectedPayload !FileOffset+    | IncompleteHeader  !FileOffset+    | IncompletePayload !FileOffset !ByteCount+    | ShortTrailer      !FileOffset+    | BadTrailer        !FileOffset+    | InvalidHeader     !FileOffset+    | BadChecksum       !FileOffset+    | FileTypeError     !FileOffset !Char !String+    deriving (Show, Typeable)+instance Exception TarException+++data TarCreateException+    = FileNameTooLong   !FileInfo+    | TarCreationError  !String+    deriving (Show, Typeable)+instance Exception TarCreateException++
+ src/Data/Conduit/Tar/Unix.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE RecordWildCards     #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Data.Conduit.Tar.Unix+    ( getFileInfo+    , restoreFile+    ) where++import           Conduit+import           Control.Exception+import           Control.Monad                 (when)+import           Data.Bits+import qualified Data.ByteString.Char8         as S8+import           Data.Conduit.Tar.Types        (FileInfo (..), FileType (..))+import qualified System.Directory              as Dir+import qualified System.Posix.Files.ByteString as Posix+import qualified System.Posix.User             as Posix++getFileInfo :: S8.ByteString -> IO FileInfo+getFileInfo fp = do+    fs <- Posix.getSymbolicLinkStatus fp+    let uid = Posix.fileOwner fs+        gid = Posix.fileGroup fs+    -- Allow for username/group retrieval failure, especially useful for non-tty environment.+    -- Workaround for: https://ghc.haskell.org/trac/ghc/ticket/1487+    -- Moreover, names are non-critical as they are not used during unarchival process+    euEntry :: Either IOException Posix.UserEntry <- try $ Posix.getUserEntryForID uid+    egEntry :: Either IOException Posix.GroupEntry <- try $ Posix.getGroupEntryForID gid+    (fType, fSize) <-+        case () of+            () | Posix.isRegularFile fs     -> return (FTNormal, Posix.fileSize fs)+               | Posix.isSymbolicLink fs    -> do+                     ln <- Posix.readSymbolicLink fp+                     return (FTSymbolicLink ln, 0)+               | Posix.isCharacterDevice fs -> return (FTCharacterSpecial, 0)+               | Posix.isBlockDevice fs     -> return (FTBlockSpecial, 0)+               | Posix.isDirectory fs       -> return (FTDirectory, 0)+               | Posix.isNamedPipe fs       -> return (FTFifo, 0)+               | otherwise                  -> error $ "Unsupported file type: " ++ S8.unpack fp+    return FileInfo+        { filePath      = fp+        , fileUserId    = uid+        , fileUserName  = either (const "") (S8.pack . Posix.userName) euEntry+        , fileGroupId   = gid+        , fileGroupName = either (const "") (S8.pack . Posix.groupName) egEntry+        , fileMode      = Posix.fileMode fs .&. 0o7777+        , fileSize      = fSize+        , fileType      = fType+        , fileModTime   = Posix.modificationTime fs+        }++-- | Restore files onto the file system. Produces actions that will set the modification time on the+-- directories, which can be executed after the pipeline has finished and all files have been+-- written to disk.+restoreFile :: (MonadResource m) =>+               FileInfo -> ConduitM S8.ByteString (IO ()) m ()+restoreFile FileInfo {..} = do+    let filePath' = S8.unpack filePath+    case fileType of+        FTDirectory -> do+            liftIO $ Dir.createDirectoryIfMissing False filePath'+            yield $+                (Dir.doesDirectoryExist filePath' >>=+                 (`when` Posix.setFileTimes filePath fileModTime fileModTime))+        FTSymbolicLink link ->+            liftIO $ do+                exist <- Posix.fileExist filePath+                when exist $ Dir.removeFile filePath'+                Posix.createSymbolicLink link filePath+        FTNormal -> sinkFile filePath'+        ty -> error $ "Unsupported tar entry type: " ++ show ty+    liftIO $ do+        Posix.setFileTimes filePath fileModTime fileModTime+        Posix.setSymbolicLinkOwnerAndGroup filePath fileUserId fileGroupId+        Posix.setFileMode filePath fileMode
+ src/Data/Conduit/Tar/Windows.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+module Data.Conduit.Tar.Windows+    ( getFileInfo+    , restoreFile+    ) where++import           Conduit+import           Control.Monad            (when)+import           Data.Bits+import qualified Data.ByteString.Char8    as S8+import           Data.Conduit.Tar.Types   (FileInfo (..), FileType (..))+import           Data.Time.Clock.POSIX+import           Foreign.C.Types          (CTime (..))+import qualified System.Directory         as Dir+import qualified System.PosixCompat.Files as Posix++getFileInfo :: S8.ByteString -> IO FileInfo+getFileInfo fp = do+    let fp' = S8.unpack fp+    fs <- Posix.getSymbolicLinkStatus fp'+    let uid = Posix.fileOwner fs+        gid = Posix.fileGroup fs+    (fType, fSize) <-+        case () of+            () | Posix.isRegularFile fs     -> return (FTNormal, Posix.fileSize fs)+               | Posix.isDirectory fs       -> return (FTDirectory, 0)+               | otherwise                  -> error $ "Unsupported file type: " ++ fp'+    return FileInfo+        { filePath      = fp+        , fileUserId    = uid+        , fileUserName  = ""+        , fileGroupId   = gid+        , fileGroupName = ""+        , fileMode      = Posix.fileMode fs .&. 0o7777+        , fileSize      = fSize+        , fileType      = fType+        , fileModTime   = Posix.modificationTime fs+        }++-- | Restore files onto the file system. Produces actions that will set the modification time on the+-- directories, which can be executed after the pipeline has finished and all files have been+-- written to disk.+restoreFile :: (MonadResource m) =>+               FileInfo -> ConduitM S8.ByteString (IO ()) m ()+restoreFile FileInfo {..} = do+    let filePath' = S8.unpack filePath+        CTime modTimeEpoch = fileModTime+        modTime = posixSecondsToUTCTime (fromIntegral modTimeEpoch)+    case fileType of+        FTDirectory -> do+            liftIO $ Dir.createDirectoryIfMissing False filePath'+            yield $+                (Dir.doesDirectoryExist filePath' >>=+                 (`when` Dir.setModificationTime filePath' modTime))+        FTNormal -> sinkFile filePath'+        ty -> error $ "Unsupported tar entry type: " ++ show ty+    liftIO $ do+        Dir.setModificationTime filePath' modTime+        Posix.setSymbolicLinkOwnerAndGroup filePath' fileUserId fileGroupId+        Posix.setFileMode filePath' fileMode+
tar-conduit.cabal view
@@ -1,12 +1,12 @@ name:                tar-conduit-version:             0.1.1-synopsis:            Parse tar files using conduit for streaming-description:         Please see README.md+version:             0.2.0+synopsis:            Extract and create tar files using conduit for streaming+description:         Please see README.md. This is just filler to avoid warnings. homepage:            https://github.com/snoyberg/tar-conduit#readme license:             MIT license-file:        LICENSE author:              Michael Snoyman-maintainer:          michael@snoyman.com, bartavelle@gmail.com+maintainer:          michael@snoyman.com, bartavelle@gmail.com, alexey@kuleshevi.ch category:            Data Conduit build-type:          Simple extra-source-files:  README.md ChangeLog.md@@ -14,23 +14,73 @@  library   hs-source-dirs:      src-  exposed-modules:     Data.Conduit.Tar+  exposed-modules:     Data.Conduit.Tar, Data.Conduit.Tar.Types   build-depends:       base >= 4.7 && < 5                      , bytestring                      , conduit-combinators >= 1.0.8.1+                     , filepath   default-language:    Haskell2010+  ghc-options:         -Wall+  if os(windows)+      other-modules: Data.Conduit.Tar.Windows+      build-depends: directory+                   , time+                   , unix-compat+      cpp-options:   -DWINDOWS+  else+      other-modules: Data.Conduit.Tar.Unix+      build-depends: directory+                   , unix --- test-suite tar-conduit-test---   type:                exitcode-stdio-1.0---   hs-source-dirs:      test---   main-is:             Spec.hs---   build-depends:       base---                      , hspec---                      , tar-conduit---                      , conduit-extra---                      , conduit-combinators---   ghc-options:         -threaded -rtsopts -with-rtsopts=-N---   default-language:    Haskell2010+test-suite tests+  type:                exitcode-stdio-1.0+  hs-source-dirs:      tests+  main-is:             Spec.hs+  build-depends:       base+                     , bytestring+                     , directory+                     , filepath+                     , hspec+                     , tar-conduit+                     , conduit-combinators+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++test-suite space+  default-language:    Haskell2010+  type: exitcode-stdio-1.0+  ghc-options: -O2+  main-is: Space.hs+  hs-source-dirs: tests+  build-depends: base+               , weigh+               , deepseq+               , containers+               , bytestring+               , directory+               , filepath+               , hspec+               , tar-conduit+               , conduit-combinators+               , conduit++benchmark time+  default-language:    Haskell2010+  type: exitcode-stdio-1.0+  ghc-options: -O2+  main-is: Time.hs+  hs-source-dirs: tests+  build-depends: base+               , criterion+               , deepseq+               , containers+               , bytestring+               , directory+               , filepath+               , hspec+               , tar-conduit+               , conduit-combinators+               , conduit  source-repository head   type:     git
+ tests/Space.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- | Measure space usage by the tar algo.++module Main where++import           Conduit+import           Control.DeepSeq+import           Control.Monad+import           Data.ByteString (ByteString)+import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as S8+import qualified Data.Conduit.List as CL+import qualified Data.Conduit.Tar as Tar+import           Data.Conduit.Tar.Types+import           Data.Monoid+import           GHC.Generics+import           System.Posix.Types+import           Weigh++main :: IO ()+main = do+  mainWith+    (do setColumns [Case, Allocated, Max, Live, GCs, Check]+        sequence_+          [ action+            ("tar " ++ show count ++ " files")+            (runConduitRes (CL.sourceList files .| void Tar.tar .| CL.sinkNull))+          | count :: Int <- [1, 10, 100, 1000, 10000]+          , let !files =+                  force+                    (concat+                       (map+                          (\i -> makeFileN (S8.pack (show i) <> ".txt") 10)+                          [1 :: Int .. count]))+          ]+        sequence_+          [ action+            ("tar file of " ++ show bytes ++ " bytes")+            (runConduitRes (CL.sourceList files .| void Tar.tar .| CL.sinkNull))+          | bytes :: Int <- [1, 10, 100, 1000, 10000]+          , let !files = force (makeFileN "file.txt" bytes)+          ]+        sequence_+          [ action+            ("untar " ++ show count ++ " files")+            (runConduitRes+               (CL.sourceList files .| void Tar.tar .|+                void (Tar.untar (const (return ()))) .|+                CL.sinkNull))+          | count :: Int <- [1, 10, 100, 1000, 10000]+          , let !files =+                  force+                    (concat+                       (map+                          (\i -> makeFileN (S8.pack (show i) <> ".txt") 10)+                          [1 :: Int .. count]))+          ]+        sequence_+          [ action+            ("untar file of " ++ show bytes ++ " bytes")+            (runConduitRes+               (CL.sourceList files .| void Tar.tar .|+                void (Tar.untar (const (return ()))) .|+                CL.sinkNull))+          | bytes :: Int <- [1, 10, 100, 1000, 10000]+          , let !files = force (makeFileN "file.txt" bytes)+          ])++----------------------------------------------------------------------+-- Helpers++makeFileN :: ByteString -> Int -> [Either FileInfo ByteString]+makeFileN fname bytes =+  let contents = S8.pack (take bytes (cycle "Hello Dave"))+  in [ Left+         FileInfo+         { filePath = fname+         , fileUserId = 0+         , fileUserName = "test"+         , fileGroupId = 0+         , fileGroupName = "test"+         , fileMode = 0+         , fileSize = fromIntegral (S.length contents)+         , fileType = FTNormal+         , fileModTime = 1234+         }+     , Right contents+     ]++----------------------------------------------------------------------+-- NFData helper instances. If ever these instances become available,+-- these can just be removed.++deriving instance Generic FileInfo+instance NFData FileInfo++deriving instance Generic FileType+instance NFData FileType++deriving instance Generic CUid+instance NFData CUid++deriving instance Generic COff+instance NFData COff++deriving instance Generic CGid+instance NFData CGid++deriving instance Generic CMode+instance NFData CMode
+ tests/Spec.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE FlexibleContexts #-}+module Main where++import Prelude as P+import Conduit+import Control.Monad (void, when, zipWithM_)+import Test.Hspec+import Data.Conduit.Tar+import System.Directory+import Data.ByteString as S+import System.IO+import System.FilePath+import Control.Exception++main :: IO ()+main = do+    let baseTmp = "tar-conduit-tests"+    isStack <- doesDirectoryExist ".stack-work"+    let testPaths =+            ["src", "./tests", "README.md", "ChangeLog.md", "LICENSE"] +++            if isStack+                then [".stack-work", "./sample"]+                else []+    hspec $ do+        describe "tar/untar" $ do+            let tarUntarContent dir =+                    runConduitRes $+                    yield dir .| void tarFilePath .| untar (const (foldC >>= yield)) .| foldC+            it "content" $ do+                c <- collectContent "src"+                tarUntarContent "src" `shouldReturn` c+        describe "tar/untar/tar" $ do+            around (withTempTarFiles baseTmp) $+                it "structure" $ \(fpIn, hIn, outDir, fpOut) -> do+                    writeTarball hIn testPaths+                    hClose hIn+                    extractTarball fpIn (Just outDir)+                    curDir <- getCurrentDirectory+                    finally+                        (setCurrentDirectory outDir >> createTarball fpOut testPaths)+                        (setCurrentDirectory curDir)+                    tb1 <- readTarball fpIn+                    tb2 <- readTarball fpOut+                    P.length tb1 `shouldBe` P.length tb2+                    zipWithM_ shouldBe (fmap fst tb2) (fmap fst tb1)+                    zipWithM_ shouldBe (fmap snd tb2) (fmap snd tb1)++withTempTarFiles :: FilePath -> ((FilePath, Handle, FilePath, FilePath) -> IO c) -> IO c+withTempTarFiles base =+    bracket+        (do tmpDir <- getTemporaryDirectory+            (fp1, h1) <- openBinaryTempFile tmpDir (addExtension base ".tar")+            let outPath = dropExtension fp1 ++ ".out"+            return (fp1, h1, outPath, addExtension outPath ".tar")+        )+        (\(fp, h, dirOut, fpOut) -> do+             hClose h+             removeFile fp+             doesDirectoryExist dirOut >>= (`when` removeDirectoryRecursive dirOut)+             doesFileExist fpOut >>= (`when` removeFile fpOut)+        )+++readTarball+  :: (MonadIO m, MonadThrow m, MonadBaseControl IO m) =>+     FilePath -> m [(FileInfo, Maybe ByteString)]+readTarball fp = runConduitRes $ sourceFileBS fp .| untar grabBoth .| sinkList+  where+    grabBoth fi =+        case fileType fi of+            FTNormal -> do+                content <- foldC+                yield (fi, Just content)+            _ -> yield (fi, Nothing)+++collectContent :: FilePath -> IO (ByteString)+collectContent dir =+    runConduitRes $+    sourceDirectoryDeep False dir .| mapMC (\fp -> runConduit (sourceFileBS fp .| foldC)) .| foldC+
+ tests/Time.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- | Measure time usage by the tar/untar functions.++module Main where++import           Conduit+import           Control.DeepSeq+import           Control.Monad+import           Criterion.Main+import           Data.ByteString (ByteString)+import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as S8+import qualified Data.Conduit.List as CL+import qualified Data.Conduit.Tar as Tar+import           Data.Conduit.Tar.Types+import           Data.Monoid+import           GHC.Generics+import           System.Posix.Types++main :: IO ()+main =+  defaultMain+    (concat+       [ [ bench+           ("tar " ++ show count ++ " files")+           (nfIO+              (runConduitRes+                 (CL.sourceList files .| void Tar.tar .| CL.sinkNull)))+         | count :: Int <- [1, 10, 100, 1000, 10000]+         , let !files =+                 force+                   (concat+                      (map+                         (\i -> makeFileN (S8.pack (show i) <> ".txt") 10)+                         [1 :: Int .. count]))+         ]+       , [ bench+           ("tar file of " ++ show bytes ++ " bytes")+           (nfIO+              (runConduitRes+                 (CL.sourceList files .| void Tar.tar .| CL.sinkNull)))+         | bytes :: Int <- [1, 10, 100, 1000, 10000]+         , let !files = force (makeFileN "file.txt" bytes)+         ]+       , [ bench+           ("untar " ++ show count ++ " files")+           (nfIO+              (runConduitRes+                 (CL.sourceList files .| void Tar.tar .|+                  void (Tar.untar (const (return ()))) .|+                  CL.sinkNull)))+         | count :: Int <- [1, 10, 100, 1000, 10000]+         , let !files =+                 force+                   (concat+                      (map+                         (\i -> makeFileN (S8.pack (show i) <> ".txt") 10)+                         [1 :: Int .. count]))+         ]+       , [ bench+           ("untar file of " ++ show bytes ++ " bytes")+           (nfIO+              (runConduitRes+                 (CL.sourceList files .| void Tar.tar .|+                  void (Tar.untar (const (return ()))) .|+                  CL.sinkNull)))+         | bytes :: Int <- [1, 10, 100, 1000, 10000]+         , let !files = force (makeFileN "file.txt" bytes)+         ]+       ])+++----------------------------------------------------------------------+-- Helpers++makeFileN :: ByteString -> Int -> [Either FileInfo ByteString]+makeFileN fname bytes =+  let contents = S8.pack (take bytes (cycle "Hello Dave"))+  in [ Left+         FileInfo+         { filePath = fname+         , fileUserId = 0+         , fileUserName = "test"+         , fileGroupId = 0+         , fileGroupName = "test"+         , fileMode = 0+         , fileSize = fromIntegral (S.length contents)+         , fileType = FTNormal+         , fileModTime = 1234+         }+     , Right contents+     ]++----------------------------------------------------------------------+-- NFData helper instances. If ever these instances become available,+-- these can just be removed.++deriving instance Generic FileInfo+instance NFData FileInfo++deriving instance Generic FileType+instance NFData FileType++deriving instance Generic CUid+instance NFData CUid++deriving instance Generic COff+instance NFData COff++deriving instance Generic CGid+instance NFData CGid++deriving instance Generic CMode+instance NFData CMode