163 lines
5.5 KiB
Haskell
163 lines
5.5 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
|
|
, 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 qualified Data.ByteString as ByteString
|
|
|
|
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
|
|
|
|
|
|
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.
|
|
( MonadThrow b
|
|
, MonadThrow m
|
|
, MonadBase b m
|
|
, PrimMonad b
|
|
)
|
|
=> ConduitT ByteString File m ZipInfo
|
|
consumeZip = transPipe liftBase unZipStream `fuseUpstream` consumeZip'
|
|
where
|
|
consumeZip' :: ConduitT (Either ZipEntry ByteString) File m ()
|
|
consumeZip' = do
|
|
input <- await
|
|
case input of
|
|
Nothing -> return ()
|
|
Just (Right _) -> throwM $ userError "Data chunk in unexpected place when parsing ZIP"
|
|
Just (Left ZipEntry{..}) -> do
|
|
contentChunks <- toConsumer accContents
|
|
zipEntryName' <- decodeZipEntryName zipEntryName
|
|
let
|
|
fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise $ makeValid zipEntryName'
|
|
fileModified = localTimeToUTC utc zipEntryTime
|
|
fileContent
|
|
| hasTrailingPathSeparator zipEntryName' = Nothing
|
|
| otherwise = Just $ mconcat contentChunks
|
|
yield File{..}
|
|
consumeZip'
|
|
accContents :: ConduitT (Either a b') Void m [b']
|
|
accContents = do
|
|
input <- await
|
|
case input of
|
|
Just (Right x) -> (x :) <$> accContents
|
|
Just (Left x) -> [] <$ leftover (Left x)
|
|
_ -> return []
|
|
|
|
produceZip :: forall b m.
|
|
( MonadThrow b
|
|
, MonadThrow m
|
|
, MonadBase b m
|
|
, PrimMonad b
|
|
)
|
|
=> ZipInfo
|
|
-> ConduitT File ByteString m ()
|
|
produceZip info = C.map toZipData .| transPipe liftBase (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 -> (ZipEntry, ZipData b)
|
|
toZipData f@File{..} =
|
|
let zData = maybe mempty (ZipDataByteString . Lazy.ByteString.fromStrict) fileContent
|
|
zEntry = (toZipEntry f){ zipEntrySize = fromIntegral . ByteString.length <$> fileContent }
|
|
in (zEntry, zData)
|
|
|
|
toZipEntry :: File -> 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 File m ()
|
|
modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
|
|
|
|
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo
|
|
sourceFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadBase IO m) => FileInfo -> ConduitT () File m ()
|
|
sourceFiles fInfo
|
|
| ((==) `on` simpleContentType) mimeType typeZip = do
|
|
$logInfoS "sourceFiles" "Unpacking ZIP"
|
|
fileSource fInfo .| void consumeZip
|
|
| otherwise = do
|
|
$logDebugS "sourceFiles" [st|Not unpacking file of type #{decodeUtf8 mimeType}|]
|
|
yieldM $ acceptFile fInfo
|
|
where
|
|
mimeType = mimeLookup $ fileName fInfo
|
|
|
|
acceptFile :: MonadResource m => FileInfo -> m File
|
|
acceptFile fInfo = do
|
|
let fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise . unpack $ fileName fInfo
|
|
fileModified <- liftIO getCurrentTime
|
|
fileContent <- fmap Just . runConduit $ fileSource fInfo .| foldC
|
|
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
|