163 lines
5.6 KiB
Haskell
163 lines
5.6 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
|
|
|
|
import Codec.Archive.Zip.Conduit.Types
|
|
import Codec.Archive.Zip.Conduit.UnZip
|
|
import Codec.Archive.Zip.Conduit.Zip
|
|
|
|
import System.FilePath
|
|
import Data.Time.LocalTime (localTimeToUTC, utcToLocalTime)
|
|
|
|
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
|
|
|
|
|
|
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
|
|
}
|
|
|
|
|
|
consumeZip :: forall b m m'.
|
|
( MonadThrow b
|
|
, MonadThrow m
|
|
, MonadBase b m
|
|
, PrimMonad b
|
|
, MonadUnliftIO m
|
|
, MonadResource m
|
|
, MonadIO m'
|
|
)
|
|
=> ConduitT () ByteString m () -> ConduitT () (File m') m ZipInfo
|
|
consumeZip inpBS = do
|
|
inps <- liftIO newBroadcastTMChanIO
|
|
let feedSingle inp = atomically $ do
|
|
guardM $ isEmptyTMChan inps
|
|
writeTMChan inps inp
|
|
zipAsync <- lift . allocateLinkedAsync . runConduit $ do
|
|
zipInfo <- (inpBS .| transPipe liftBase unZipStream) `fuseUpstream` C.mapM_ feedSingle
|
|
atomically $ closeTMChan inps
|
|
return zipInfo
|
|
|
|
evalContT . callCC $ \finish -> forever $ do
|
|
(fileChan, fileDef) <- atomically $ do
|
|
fileChan <- dupTMChan inps
|
|
fileDef <- readTMChan fileChan
|
|
return (fileChan, fileDef)
|
|
case fileDef of
|
|
Nothing -> finish =<< waitAsync zipAsync
|
|
Just (Right _) -> return ()
|
|
Just (Left ZipEntry{..}) -> do
|
|
zipEntryName' <- decodeZipEntryName zipEntryName
|
|
let
|
|
fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise $ makeValid zipEntryName'
|
|
fileModified = localTimeToUTC utc zipEntryTime
|
|
isDirectory = hasTrailingPathSeparator zipEntryName'
|
|
fileContent
|
|
| isDirectory = Nothing
|
|
| otherwise = Just . evalContT . callCC $ \finishContent -> forever $ do
|
|
nextVal <- atomically $ (preview _Right =<<) <$> readTMChan fileChan
|
|
maybe (finishContent ()) (lift . yield) nextVal
|
|
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 = True
|
|
, zipOptCompressLevel = -1 -- This is passed through all the way to the C zlib, where it means "default level"
|
|
, zipOptInfo = info
|
|
}
|
|
|
|
toZipData :: File m -> (ZipEntry, ZipData m)
|
|
toZipData f@File{..}
|
|
= (toZipEntry f, ) $ maybe mempty ZipDataSource fileContent
|
|
|
|
toZipEntry :: File m -> ZipEntry
|
|
toZipEntry File{..} = ZipEntry{..}
|
|
where
|
|
isDir = isNothing fileContent
|
|
|
|
zipEntryName = encodeZipEntryName . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator isDir . normalise $ makeValid fileTitle
|
|
zipEntryTime = utcToLocalTime utc 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, MonadBase IO m, MonadUnliftIO m, MonadResource 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
|
|
|
|
acceptFile :: (MonadResource m, MonadResource m') => FileInfo -> m (File m')
|
|
acceptFile fInfo = do
|
|
let fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise . unpack $ fileName fInfo
|
|
fileContent = Just $ fileSource fInfo
|
|
fileModified <- liftIO getCurrentTime
|
|
return File{..}
|
|
|
|
|
|
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 -> either throwM return $ 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
|