fradrive/src/Handler/Utils/Zip.hs
Gregor Kleen 64c45c515e Tutorials
2019-04-29 00:20:34 +02:00

111 lines
3.8 KiB
Haskell

{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- This concerns zipEntrySize in produceZip
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Zip
( ZipError(..)
, ZipInfo(..)
, produceZip
, consumeZip
, modifyFileTitle
, sourceFiles, acceptFile
) where
import Import
import Codec.Archive.Zip.Conduit.Types
import Codec.Archive.Zip.Conduit.UnZip
import Codec.Archive.Zip.Conduit.Zip
-- import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.ByteString.Lazy as Lazy.ByteString
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import System.FilePath
import Data.Time.LocalTime (localTimeToUTC, utcToLocalTime)
import Data.List (dropWhileEnd)
import Network.Mime
instance Default ZipInfo where
def = ZipInfo
{ zipComment = mempty
}
consumeZip :: ( MonadBase b m
, PrimMonad b
, MonadThrow m
) => ConduitM ByteString File m ZipInfo
consumeZip = unZipStream `fuseUpstream` consumeZip'
where
consumeZip' :: ( MonadThrow m
) => Conduit (Either ZipEntry ByteString) m File
consumeZip' = do
input <- await
case input of
Nothing -> return ()
Just (Right _) -> throw $ userError "Data chunk in unexpected place when parsing ZIP"
Just (Left ZipEntry{..}) -> do
contentChunks <- toConsumer accContents
let
fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise $ makeValid zipEntryName
fileModified = localTimeToUTC utc zipEntryTime
fileContent
| hasTrailingPathSeparator zipEntryName = Nothing
| otherwise = Just $ mconcat contentChunks
yield File{..}
consumeZip'
accContents :: Monad m => Sink (Either a b) m [b]
accContents = do
input <- await
case input of
Just (Right x) -> (x :) <$> accContents
Just (Left x) -> [] <$ leftover (Left x)
_ -> return []
produceZip :: ( MonadBase b m
, PrimMonad b
, MonadThrow m
) => ZipInfo
-> Conduit File m ByteString
produceZip info = mapC toZipData =$= void (zipStream zipOptions)
where
zipOptions = 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 => File -> (ZipEntry, ZipData m)
toZipData f@File{..} = ((toZipEntry f){ zipEntrySize = fromIntegral . ByteString.length <$> fileContent }, maybe mempty (ZipDataByteString . Lazy.ByteString.fromStrict) fileContent)
toZipEntry :: File -> ZipEntry
toZipEntry File{..} = ZipEntry
{ zipEntryName = bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator isDir . normalise . makeValid $ fileTitle
, zipEntryTime = utcToLocalTime utc fileModified
}
where
isDir = isNothing fileContent
modifyFileTitle :: Monad m => (FilePath -> FilePath) -> Conduit File m File
modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo
sourceFiles :: (MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File
sourceFiles fInfo
| mimeType == "application/zip" = fileSource fInfo =$= void consumeZip
| otherwise = yieldM $ acceptFile fInfo
where
mimeType = defaultMimeLookup (fileName fInfo)
acceptFile :: (MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> m File
acceptFile fInfo = do
let fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise . unpack $ fileName fInfo
fileModified <- liftIO getCurrentTime
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
return File{..}