fradrive/src/Handler/Utils/Zip.hs
2021-03-17 21:15:00 +01:00

227 lines
8.0 KiB
Haskell

{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- This concerns zipEntrySize in produceZip
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Zip
( typeZip, extensionZip
, ZipError(..)
, ZipInfo(..)
, produceZip
, consumeZip
, modifyFileTitle
, receiveFiles, acceptFile
) where
import Import.NoFoundation
import Handler.Utils.Files (acceptFile)
import Handler.Utils.DateTime (localTimeToUTCSimple, utcToLocalTime)
import Codec.Archive.Zip.Conduit.Types
import Codec.Archive.Zip.Conduit.UnZip
import Codec.Archive.Zip.Conduit.Zip
import System.FilePath
import Data.List (dropWhileEnd)
import qualified Data.Map as Map
import qualified Data.Conduit.List as C
import Data.Encoding ( decodeStrictByteStringExplicit
, encodeStrictByteStringExplicit
)
import Data.Encoding.CP437
import qualified Data.Char as Char
import Control.Monad.Trans.Cont
import Control.Monad.Trans.State.Strict (evalStateT)
import qualified Control.Monad.State.Class as State
typeZip :: ContentType
typeZip = "application/zip"
extensionZip :: Extension
extensionZip = fromMaybe "zip" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeZip ]
instance Default ZipInfo where
def = ZipInfo
{ zipComment = mempty
}
data ConsumeZipException
= ConsumeZipUnZipException SomeException
| ConsumeZipUnexpectedContent
deriving (Show, Generic, Typeable)
deriving anyclass (Exception)
consumeZip :: forall m m'.
( MonadThrow m
, PrimMonad m
, MonadUnliftIO m
, MonadResource m
, MonadIO m'
, MonadThrow m'
)
=> ConduitT () ByteString m () -> ConduitT () (File m') m ZipInfo
consumeZip inpBS = do
inpChunk <- liftIO newEmptyTMVarIO
zipAsync <- lift . allocateLinkedAsync $
runConduit $ (inpBS .| unZipStream) `fuseUpstream` C.mapM_ (atomically . putTMVar inpChunk)
flip evalStateT Nothing . evalContT . callCC $ \finishConsume -> forever $ do
inpChunk' <- atomically $
Right <$> takeTMVar inpChunk
<|> Left <$> waitCatchSTM zipAsync
fileSink <- State.get
case (fileSink, inpChunk') of
(mFSink , Left (Left unzipExc) ) -> do
for_ mFSink $ \fSink' -> atomically $ do
writeTMChan fSink' $ Left unzipExc
closeTMChan fSink'
throwM unzipExc
(mFSink , Left (Right zInfo) ) -> do
for_ mFSink $ atomically . closeTMChan
finishConsume zInfo
(Just fSink, Right (Right bs) ) ->
atomically . writeTMChan fSink $ Right bs
(Nothing , Right (Right _) ) ->
throwM ConsumeZipUnexpectedContent
(mFSink , Right (Left ZipEntry{..})) -> do
for_ mFSink $ atomically . closeTMChan
State.put Nothing
zipEntryName' <- decodeZipEntryName zipEntryName
let
fileTitle = "." <//> zipEntryName'
& normalise
& makeValid
& dropWhile isPathSeparator
& dropWhileEnd isPathSeparator
& normalise
& makeValid
fileModified = localTimeToUTCSimple zipEntryTime
isDirectory = hasTrailingPathSeparator zipEntryName'
fileContent <- if
| isDirectory -> return Nothing
| otherwise -> do
fileChan <- liftIO newTMChanIO
State.put $ Just fileChan
return . Just . evalContT . callCC $ \finishFileContent -> forever $ do
nextVal <- atomically $ asum
[ readTMChan fileChan
, do
inpChunk'' <- Right <$> takeTMVar inpChunk
<|> Left <$> waitCatchSTM zipAsync
case inpChunk'' of
Left (Left unzipExc) -> return . Just $ Left unzipExc
Left (Right _ ) -> return Nothing
Right (Left zInfo ) -> Nothing <$ putTMVar inpChunk (Left zInfo)
Right (Right bs ) -> return . Just $ Right bs
]
case nextVal of
Nothing -> finishFileContent ()
Just (Right bs) -> lift $ yield bs
Just (Left exc) -> throwM $ ConsumeZipUnZipException exc
lift . lift $ yield File{..}
produceZip :: forall m.
( MonadThrow m
, PrimMonad m
)
=> ZipInfo
-> ConduitT (File m) ByteString m ()
produceZip info = C.map toZipData .| void (zipStream zipOptions)
where
zipOptions = ZipOptions
{ zipOpt64 = False
, zipOptCompressLevel = defaultCompression
, zipOptInfo = info
}
-- toZipData :: forall v. File m -> ConduitT v (ZipEntry, ZipData m) m ()
-- toZipData f
-- | Just fc <- fileContent f = do
-- outpChunk <- newEmptyTMVarIO
-- outpAsync <- lift . allocateLinkedAsync $
-- runConduit $ fc .| C.mapM_ (atomically . putTMVar outpChunk)
-- yield ( toZipEntry f
-- , ZipDataSource . evalContT . callCC $ \finishContent -> forever $ do
-- nextVal <- atomically $
-- Right <$> takeTMVar outpChunk
-- <|> Left <$> waitCatchSTM outpAsync
-- case nextVal of
-- Right chunk -> lift $ yield chunk
-- Left (Right () ) -> finishContent ()
-- Left (Left exc) -> throwM exc
-- )
-- | otherwise = yield (toZipEntry f, mempty)
toZipData :: File m -> (ZipEntry, ZipData m)
toZipData f@File{..}
= (toZipEntry f, maybe mempty ZipDataSource fileContent)
toZipEntry :: File m -> ZipEntry
toZipEntry File{..} = ZipEntry{..}
where
isDir = is _Nothing fileContent
zipEntryName = "." <//> fileTitle
& normalise
& makeValid
& dropWhile isPathSeparator
& dropWhileEnd isPathSeparator
& bool id addTrailingPathSeparator isDir
& normalise
& makeValid
& encodeZipEntryName
zipEntryTime = utcToLocalTime fileModified
zipEntrySize = Nothing
zipEntryExternalAttributes = Nothing
modifyFileTitle :: Monad m => (FilePath -> FilePath) -> ConduitT (File m') (File m') m ()
modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo
receiveFiles :: (MonadLogger m, MonadResource m, MonadThrow m, PrimMonad m, MonadUnliftIO m, MonadResource m', MonadThrow m') => FileInfo -> ConduitT () (File m') m ()
receiveFiles fInfo
| ((==) `on` simpleContentType) mimeType typeZip = do
$logInfoS "sourceFiles" "Unpacking ZIP"
void . consumeZip $ fileSource fInfo
| otherwise = do
$logDebugS "sourceFiles" [st|Not unpacking file of type #{decodeUtf8 mimeType}|]
yieldM $ acceptFile fInfo
where
mimeType = mimeLookup $ fileName fInfo
decodeZipEntryName :: MonadThrow m => Either Text ByteString -> m FilePath
-- ^ Extract the filename from a 'ZipEntry' doing decoding along the way.
--
-- Throws 'Data.Encoding.Exception.DecodingException's.
decodeZipEntryName = \case
Left t -> return $ unpack t
Right cp437 -> throwLeft $ decodeStrictByteStringExplicit CP437 cp437
encodeZipEntryName :: FilePath -> Either Text ByteString
-- ^ Encode a filename for use in a 'ZipEntry', encodes as
-- 'Data.Encoding.UTF8.UTF8' iff the given path contains non-ascii characters.
--
-- Does not do any normalisation (in particular this function does not ensure
-- that the 'FilePath' does not start with a slash).
encodeZipEntryName path = fromMaybe (Left $ pack path) $ do
guard $ all Char.isAscii path
either (const mzero) (return . Right) $ encodeStrictByteStringExplicit CP437 path