fradrive/src/Handler/Zip.hs
2017-10-05 14:10:32 +02:00

103 lines
3.6 KiB
Haskell

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- This concerns Zip.zipEntrySize in produceZip
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Zip
( Zip.ZipError(..)
, Zip.ZipInfo(..)
, produceZip
, consumeZip
) where
import Import
import qualified Data.Conduit.List as Conduit (map)
import qualified Codec.Archive.Zip.Conduit.Types as Zip
import qualified Codec.Archive.Zip.Conduit.UnZip as Zip
import qualified Codec.Archive.Zip.Conduit.Zip as Zip
import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.ByteString.Lazy as Lazy.ByteString
import Data.ByteString (ByteString)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import System.FilePath
import Data.Time
import GHC.Generics (Generic)
import Data.Typeable (Typeable)
data ZipEntry = ZipEntry
{ zipEntryName :: FilePath
, zipEntryTime :: UTCTime
, zipEntryContents :: Maybe Lazy.ByteString -- ^ 'Nothing' means this is a directory
} deriving (Read, Show, Generic, Typeable)
instance Default Zip.ZipInfo where
def = Zip.ZipInfo
{ zipComment = mempty
}
consumeZip :: (ZipEntry -> YesodDB UniWorX a) -- ^ Insert entries into database
-> Sink ByteString (YesodDB UniWorX) (Zip.ZipInfo, [a])
consumeZip insertEntry = Zip.unZipStream `fuseBoth` consumeZip'
where
-- consumeZip' :: Sink (Either Zip.ZipEntry ByteString) (YesodDB UniWorX) [a]
consumeZip' = do
input <- await
case input of
Nothing -> return []
Just (Right _) -> throw $ userError "Data chunk in unexpected place when parsing ZIP"
Just (Left e) -> do
zipEntryName' <- either throw return . Text.decodeUtf8' $ Zip.zipEntryName e
contentChunks <- accContents
let
zipEntryName = normalise . makeValid $ Text.unpack zipEntryName'
zipEntryTime = localTimeToUTC utc $ Zip.zipEntryTime e
zipEntryContents
| hasTrailingPathSeparator zipEntryName = Nothing
| otherwise = Just $ Lazy.ByteString.fromChunks contentChunks
(:) <$> (lift $ insertEntry ZipEntry{..}) <*> consumeZip'
accContents :: Monad m => Sink (Either a b) m [b]
accContents = do
input <- await
case input of
Just (Right x) -> (x :) <$> accContents
_ -> return []
produceZip :: ( MonadBase b m
, PrimMonad b
, MonadThrow m
) => Zip.ZipInfo
-> Conduit ZipEntry m ByteString
produceZip info = Conduit.map toZipData =$= void (Zip.zipStream zipOptions)
where
zipOptions = Zip.ZipOptions
{ zipOpt64 = True
, zipOptCompressLevel = -1 -- This is passed through all the way to the C zlib, where it means "default level"
, zipOptInfo = info
}
toZipData :: Monad m => ZipEntry -> (Zip.ZipEntry, Zip.ZipData m)
toZipData (e@ZipEntry{ zipEntryContents = Nothing })
= ((toZipEntry True e){ Zip.zipEntrySize = Nothing }, mempty)
toZipData (e@ZipEntry{ zipEntryContents = Just b})
= ((toZipEntry False e){ Zip.zipEntrySize = Just . fromIntegral $ Lazy.ByteString.length b }, Zip.ZipDataByteString b)
toZipEntry :: Bool -- ^ Is directory?
-> ZipEntry -> Zip.ZipEntry
toZipEntry isDir ZipEntry{..} = Zip.ZipEntry
{ zipEntryName = Text.encodeUtf8 . Text.pack . bool dropTrailingPathSeparator addTrailingPathSeparator isDir . normalise $ makeValid zipEntryName
, zipEntryTime = utcToLocalTime utc zipEntryTime
}