Merge branch 'zip-stream'
This commit is contained in:
commit
3cebf133bf
25
models
25
models
@ -33,7 +33,7 @@ Course
|
||||
description Html Maybe
|
||||
linkexternal Text Maybe
|
||||
schoolId SchoolId
|
||||
termId TermId -- TermId ist jetzt Text als Typ
|
||||
termId TermId -- @data TermId = TermId TermIdentifier@ stored as text
|
||||
capacity Int Maybe
|
||||
created UTCTime
|
||||
changed UTCTime
|
||||
@ -55,10 +55,6 @@ Sheet
|
||||
sheetType SheetType
|
||||
maxPoints Double Maybe
|
||||
requiredPoints Double Maybe
|
||||
exerciseId FileId Maybe
|
||||
hintId FileId Maybe
|
||||
solutionId FileId Maybe
|
||||
markingId FileId Maybe
|
||||
markingText Text Maybe
|
||||
activeFrom UTCTime
|
||||
activeTo UTCTime
|
||||
@ -68,16 +64,18 @@ Sheet
|
||||
changed UTCTime
|
||||
createdBy UserId
|
||||
changedBy UserId
|
||||
SheetFile
|
||||
sheetId SheetId
|
||||
fileId FileId
|
||||
type SheetFileType
|
||||
UniqueSheetFile fileId sheetId type
|
||||
File
|
||||
title Text
|
||||
content ByteString
|
||||
created UTCTime
|
||||
changed UTCTime
|
||||
createdBy UserId
|
||||
changedBy UserId
|
||||
title FilePath
|
||||
content ByteString Maybe -- Nothing iff this is a directory
|
||||
modified UTCTime
|
||||
deriving Show Eq Ord
|
||||
Submission
|
||||
sheetId SheetId
|
||||
updateId FileId Maybe
|
||||
ratingBy UserId Maybe
|
||||
ratingPoints Double Maybe
|
||||
ratingComment Text Maybe
|
||||
@ -89,7 +87,8 @@ Submission
|
||||
SubmissionFile
|
||||
submissionId SubmissionId
|
||||
fileId FileId
|
||||
UniqueSubmissionFile fileId submissionId
|
||||
isUpdate Bool
|
||||
UniqueSubmissionFile fileId submissionId isUpdate
|
||||
SubmissionUser
|
||||
userId UserId
|
||||
submissionId SubmissionId
|
||||
|
||||
@ -54,6 +54,8 @@ dependencies:
|
||||
- colonnade >=1.1.1
|
||||
- yesod-colonnade >=1.1.0
|
||||
- blaze-markup
|
||||
- zip-stream
|
||||
- filepath
|
||||
|
||||
# The library contains all of our application code. The executable
|
||||
# defined below is just a thin wrapper.
|
||||
@ -100,6 +102,8 @@ tests:
|
||||
- hspec >=2.0.0
|
||||
- QuickCheck
|
||||
- yesod-test
|
||||
- conduit-extra
|
||||
- quickcheck-instances
|
||||
|
||||
# Define flags used by "yesod devel" to make compilation faster
|
||||
flags:
|
||||
|
||||
97
src/Handler/Utils/Zip.hs
Normal file
97
src/Handler/Utils/Zip.hs
Normal file
@ -0,0 +1,97 @@
|
||||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
|
||||
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- This concerns zipEntrySize in produceZip
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Handler.Utils.Zip
|
||||
( ZipError(..)
|
||||
, ZipInfo(..)
|
||||
, produceZip
|
||||
, consumeZip
|
||||
) where
|
||||
|
||||
import Import
|
||||
|
||||
import qualified Data.Conduit.List as Conduit (map)
|
||||
|
||||
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 qualified Data.Text as Text
|
||||
import qualified Data.Text.Encoding as Text
|
||||
|
||||
import System.FilePath
|
||||
import Data.Time
|
||||
|
||||
import Data.List (dropWhileEnd)
|
||||
|
||||
|
||||
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 e) -> do
|
||||
zipEntryName' <- fmap Text.unpack . either throw return . Text.decodeUtf8' $ zipEntryName e
|
||||
contentChunks <- toConsumer accContents
|
||||
let
|
||||
fileTitle = normalise $ makeValid zipEntryName'
|
||||
fileModified = localTimeToUTC utc $ zipEntryTime e
|
||||
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 = Conduit.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 :: 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 = Text.encodeUtf8 . Text.pack . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator isDir . normalise . makeValid $ fileTitle
|
||||
, zipEntryTime = utcToLocalTime utc fileModified
|
||||
}
|
||||
where
|
||||
isDir = isNothing fileContent
|
||||
@ -39,6 +39,10 @@ data ExamStatus = Attended | NoShow | Voided
|
||||
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
||||
derivePersistField "ExamStatus"
|
||||
|
||||
data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
|
||||
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
||||
derivePersistField "SheetFileType"
|
||||
|
||||
|
||||
data Season = Summer | Winter
|
||||
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic, Typeable)
|
||||
|
||||
@ -9,4 +9,5 @@ packages:
|
||||
extra-deps:
|
||||
- colonnade-1.1.1
|
||||
- yesod-colonnade-1.1.0
|
||||
- zip-stream-0.1.0.1
|
||||
resolver: lts-9.3
|
||||
|
||||
47
test/Handler/Utils/ZipSpec.hs
Normal file
47
test/Handler/Utils/ZipSpec.hs
Normal file
@ -0,0 +1,47 @@
|
||||
{-# LANGUAGE NoImplicitPrelude #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Handler.Utils.ZipSpec where
|
||||
|
||||
import TestImport
|
||||
|
||||
import Handler.Utils.Zip
|
||||
|
||||
import System.FilePath
|
||||
|
||||
import Data.Conduit
|
||||
import qualified Data.Conduit.List as Conduit
|
||||
|
||||
import Data.List (dropWhileEnd)
|
||||
import Data.Time
|
||||
|
||||
instance Arbitrary File where
|
||||
arbitrary = do
|
||||
fileTitle <- joinPath <$> arbitrary
|
||||
date <- addDays <$> arbitrary <*> pure (fromGregorian 2043 7 2)
|
||||
fileModified <- addUTCTime <$> arbitrary <*> pure (UTCTime date 0)
|
||||
fileContent <- arbitrary
|
||||
return File{..}
|
||||
|
||||
spec :: Spec
|
||||
spec = describe "Zip file handling" $ do
|
||||
it "has compatible encoding/decoding to/from zip files" . property $
|
||||
\zipFiles -> do
|
||||
zipFiles' <- runConduit $ Conduit.sourceList zipFiles =$= produceZip def =$= void consumeZip =$= Conduit.consume
|
||||
forM_ (zipFiles `zip` zipFiles') $ \(file, file') -> do
|
||||
let acceptableFilenameChanges
|
||||
= makeValid . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator (isNothing $ fileContent file) . normalise . makeValid
|
||||
acceptableTimeDifference t1 t2 = abs (diffUTCTime t1 t2) <= 2
|
||||
(shouldBe `on` acceptableFilenameChanges) (fileTitle file') (fileTitle file)
|
||||
when (inZipRange $ fileModified file) $
|
||||
(fileModified file', fileModified file) `shouldSatisfy` uncurry acceptableTimeDifference
|
||||
(fileContent file') `shouldBe` (fileContent file)
|
||||
|
||||
inZipRange :: UTCTime -> Bool
|
||||
inZipRange time
|
||||
| time > UTCTime (fromGregorian 1980 1 1) 0
|
||||
, time < UTCTime (fromGregorian 2107 1 1) 0
|
||||
= True
|
||||
| otherwise
|
||||
= False
|
||||
@ -22,6 +22,8 @@ import Yesod.Test as X
|
||||
import Yesod.Core.Unsafe (fakeHandlerGetLogger)
|
||||
import Test.QuickCheck as X
|
||||
import Test.QuickCheck.Gen as X
|
||||
import Data.Default as X
|
||||
import Test.QuickCheck.Instances as X
|
||||
|
||||
runDB :: SqlPersistM a -> YesodExample UniWorX a
|
||||
runDB query = do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user