fix: zip handling & tests

This commit is contained in:
Gregor Kleen 2020-09-09 13:44:01 +02:00
parent e80f7d7a89
commit 350ee79af3
19 changed files with 359 additions and 101 deletions

View File

@ -10,4 +10,5 @@ log-settings:
auth-dummy-login: true auth-dummy-login: true
server-session-acid-fallback: true server-session-acid-fallback: true
job-cron-interval: null
job-workers: 1 job-workers: 1

View File

@ -55,7 +55,7 @@ data PersonalisedSheetFileUnresolved a
= PSFUnresolvedDirectory a = PSFUnresolvedDirectory a
| PSFUnresolvedCollatable Text a | PSFUnresolvedCollatable Text a
| PSFUnresolved a | PSFUnresolved a
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Functor, Foldable, Traversable, Generic, Typeable)
makePrisms ''PersonalisedSheetFileUnresolved makePrisms ''PersonalisedSheetFileUnresolved

View File

@ -2,7 +2,7 @@
module Handler.Utils.DateTime module Handler.Utils.DateTime
( utcToLocalTime, utcToZonedTime ( utcToLocalTime, utcToZonedTime
, localTimeToUTC, TZ.LocalToUTCResult(..) , localTimeToUTC, TZ.LocalToUTCResult(..), localTimeToUTCSimple
, toTimeOfDay , toTimeOfDay
, toMidnight, beforeMidnight, toMidday, toMorning , toMidnight, beforeMidnight, toMidday, toMorning
, formatDiffDays , formatDiffDays
@ -47,6 +47,9 @@ utcToZonedTime = ZonedTime <$> TZ.utcToLocalTimeTZ appTZ <*> TZ.timeZoneForUTCTi
localTimeToUTC :: LocalTime -> LocalToUTCResult localTimeToUTC :: LocalTime -> LocalToUTCResult
localTimeToUTC = TZ.localTimeToUTCFull appTZ localTimeToUTC = TZ.localTimeToUTCFull appTZ
localTimeToUTCSimple :: LocalTime -> UTCTime
localTimeToUTCSimple = TZ.localTimeToUTCTZ appTZ
-- | Local midnight of given day -- | Local midnight of given day
toMidnight :: Day -> UTCTime toMidnight :: Day -> UTCTime
toMidnight = toTimeOfDay 0 0 0 toMidnight = toTimeOfDay 0 0 0

View File

@ -3,6 +3,7 @@ module Handler.Utils.Files
, sourceFiles, sourceFiles' , sourceFiles, sourceFiles'
, SourceFilesException(..) , SourceFilesException(..)
, sourceFileDB , sourceFileDB
, acceptFile
) where ) where
import Import import Import
@ -16,6 +17,8 @@ import qualified Network.Minio as Minio
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import System.FilePath (normalise)
data SourceFilesException data SourceFilesException
= SourceFilesMismatchedHashes = SourceFilesMismatchedHashes
@ -85,3 +88,11 @@ sourceFiles' = C.map sourceFile'
sourceFile' :: forall file. HasFileReference file => file -> DBFile sourceFile' :: forall file. HasFileReference file => file -> DBFile
sourceFile' = sourceFile . view (_FileReference . _1) sourceFile' = sourceFile . view (_FileReference . _1)
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{..}

View File

@ -13,12 +13,14 @@ module Handler.Utils.Zip
import Import import Import
import Handler.Utils.Files (acceptFile)
import Handler.Utils.DateTime (localTimeToUTCSimple, utcToLocalTime)
import Codec.Archive.Zip.Conduit.Types import Codec.Archive.Zip.Conduit.Types
import Codec.Archive.Zip.Conduit.UnZip import Codec.Archive.Zip.Conduit.UnZip
import Codec.Archive.Zip.Conduit.Zip import Codec.Archive.Zip.Conduit.Zip
import System.FilePath import System.FilePath
import Data.Time.LocalTime (localTimeToUTC, utcToLocalTime)
import Data.List (dropWhileEnd) import Data.List (dropWhileEnd)
@ -34,6 +36,8 @@ import Data.Encoding.CP437
import qualified Data.Char as Char import qualified Data.Char as Char
import Control.Monad.Trans.Cont import Control.Monad.Trans.Cont
import Control.Monad.Trans.State.Strict (evalStateT)
import qualified Control.Monad.State.Class as State
typeZip :: ContentType typeZip :: ContentType
@ -50,47 +54,88 @@ instance Default ZipInfo where
} }
consumeZip :: forall b m m'. data ConsumeZipException
( MonadThrow b = ConsumeZipUnZipException SomeException
, MonadThrow m | ConsumeZipUnexpectedContent
, MonadBase b m deriving (Show, Generic, Typeable)
, PrimMonad b deriving anyclass (Exception)
consumeZip :: forall m m'.
( MonadThrow m
, PrimMonad m
, MonadUnliftIO m , MonadUnliftIO m
, MonadResource m , MonadResource m
, MonadIO m' , MonadIO m'
, MonadThrow m'
) )
=> ConduitT () ByteString m () -> ConduitT () (File m') m ZipInfo => ConduitT () ByteString m () -> ConduitT () (File m') m ZipInfo
consumeZip inpBS = do consumeZip inpBS = do
inps <- liftIO newBroadcastTMChanIO inpChunk <- liftIO newEmptyTMVarIO
let feedSingle inp = atomically $ do zipAsync <- lift . allocateLinkedAsync $
guardM $ isEmptyTMChan inps runConduit $ (inpBS .| unZipStream) `fuseUpstream` C.mapM_ (atomically . putTMVar inpChunk)
writeTMChan inps inp
zipAsync <- lift . allocateLinkedAsync . runConduit $ do flip evalStateT Nothing . evalContT . callCC $ \finishConsume -> forever $ do
zipInfo <- (inpBS .| transPipe liftBase unZipStream) `fuseUpstream` C.mapM_ feedSingle inpChunk' <- atomically $
atomically $ closeTMChan inps Right <$> takeTMVar inpChunk
return zipInfo <|> 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
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 zipEntryName' <- decodeZipEntryName zipEntryName
let let
fileTitle = dropWhile isPathSeparator . dropTrailingPathSeparator . normalise $ makeValid zipEntryName' fileTitle = "." <//> zipEntryName'
fileModified = localTimeToUTC utc zipEntryTime & normalise
& makeValid
& dropWhile isPathSeparator
& dropWhileEnd isPathSeparator
& normalise
& makeValid
fileModified = localTimeToUTCSimple zipEntryTime
isDirectory = hasTrailingPathSeparator zipEntryName' isDirectory = hasTrailingPathSeparator zipEntryName'
fileContent fileContent <- if
| isDirectory = Nothing | isDirectory -> return Nothing
| otherwise = Just . evalContT . callCC $ \finishContent -> forever $ do | otherwise -> do
nextVal <- atomically $ (preview _Right =<<) <$> readTMChan fileChan fileChan <- liftIO newTMChanIO
maybe (finishContent ()) (lift . yield) nextVal State.put $ Just fileChan
lift $ yield File{..} 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. produceZip :: forall m.
( MonadThrow m ( MonadThrow m
, PrimMonad m , PrimMonad m
@ -100,22 +145,48 @@ produceZip :: forall m.
produceZip info = C.map toZipData .| void (zipStream zipOptions) produceZip info = C.map toZipData .| void (zipStream zipOptions)
where where
zipOptions = ZipOptions zipOptions = ZipOptions
{ zipOpt64 = True { zipOpt64 = False
, zipOptCompressLevel = -1 -- This is passed through all the way to the C zlib, where it means "default level" , zipOptCompressLevel = defaultCompression
, zipOptInfo = info , 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 :: File m -> (ZipEntry, ZipData m)
toZipData f@File{..} toZipData f@File{..}
= (toZipEntry f, ) $ maybe mempty ZipDataSource fileContent = (toZipEntry f, maybe mempty ZipDataSource fileContent)
toZipEntry :: File m -> ZipEntry toZipEntry :: File m -> ZipEntry
toZipEntry File{..} = ZipEntry{..} toZipEntry File{..} = ZipEntry{..}
where where
isDir = isNothing fileContent isDir = is _Nothing fileContent
zipEntryName = encodeZipEntryName . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator isDir . normalise $ makeValid fileTitle zipEntryName = "." <//> fileTitle
zipEntryTime = utcToLocalTime utc fileModified & normalise
& makeValid
& dropWhile isPathSeparator
& dropWhileEnd isPathSeparator
& bool id addTrailingPathSeparator isDir
& normalise
& makeValid
& encodeZipEntryName
zipEntryTime = utcToLocalTime fileModified
zipEntrySize = Nothing zipEntrySize = Nothing
zipEntryExternalAttributes = Nothing zipEntryExternalAttributes = Nothing
@ -123,7 +194,7 @@ modifyFileTitle :: Monad m => (FilePath -> FilePath) -> ConduitT (File m') (File
modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle } modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo -- 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 :: (MonadLogger m, MonadResource m, MonadThrow m, PrimMonad m, MonadUnliftIO m, MonadResource m', MonadThrow m') => FileInfo -> ConduitT () (File m') m ()
receiveFiles fInfo receiveFiles fInfo
| ((==) `on` simpleContentType) mimeType typeZip = do | ((==) `on` simpleContentType) mimeType typeZip = do
$logInfoS "sourceFiles" "Unpacking ZIP" $logInfoS "sourceFiles" "Unpacking ZIP"
@ -134,13 +205,6 @@ receiveFiles fInfo
where where
mimeType = mimeLookup $ fileName fInfo 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 decodeZipEntryName :: MonadThrow m => Either Text ByteString -> m FilePath
-- ^ Extract the filename from a 'ZipEntry' doing decoding along the way. -- ^ Extract the filename from a 'ZipEntry' doing decoding along the way.

View File

@ -116,8 +116,9 @@ manageCrontab foundation@UniWorX{..} unmask = do
jState <- atomically $ readTMVar appJobState jState <- atomically $ readTMVar appJobState
liftIO . unsafeHandler foundation . void $ do liftIO . unsafeHandler foundation . void $ do
atomically . assertM_ (not . Map.null . jobWorkers) $ readTMVar appJobState atomically . assertM_ (not . Map.null . jobWorkers) $ readTMVar appJobState
runReaderT ?? foundation $ when (has (_appJobCronInterval . _Just) foundation) $
writeJobCtlBlock JobCtlDetermineCrontab runReaderT ?? foundation $
writeJobCtlBlock JobCtlDetermineCrontab
void $ evalRWST (forever execCrontab) jState HashMap.empty void $ evalRWST (forever execCrontab) jState HashMap.empty
let awaitTermination = guardM $ let awaitTermination = guardM $
@ -479,7 +480,7 @@ jLocked jId act = flip evalStateT False $ do
threshold <- getsYesod $ view _appJobStaleThreshold threshold <- getsYesod $ view _appJobStaleThreshold
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
heldLocks <- asks jobHeldLocks heldLocks <- asks jobHeldLocks
isHeld <- (jId `Set.member`) <$> atomically (readTVar heldLocks) isHeld <- (jId `Set.member`) <$> readTVarIO heldLocks
hadStale <- maybeT (return False) $ do hadStale <- maybeT (return False) $ do
lockTime <- MaybeT $ return queuedJobLockTime lockTime <- MaybeT $ return queuedJobLockTime
lockInstance <- MaybeT $ return queuedJobLockInstance lockInstance <- MaybeT $ return queuedJobLockInstance

View File

@ -40,14 +40,15 @@ determineCrontab = execWriterT $ do
} }
Nothing -> return () Nothing -> return ()
tell $ HashMap.singleton whenIsJust appJobCronInterval $ \interval ->
JobCtlDetermineCrontab tell $ HashMap.singleton
Cron JobCtlDetermineCrontab
{ cronInitial = CronAsap Cron
, cronRepeat = CronRepeatScheduled CronAsap { cronInitial = CronAsap
, cronRateLimit = appJobCronInterval , cronRepeat = CronRepeatScheduled CronAsap
, cronNotAfter = Right CronNotScheduled , cronRateLimit = interval
} , cronNotAfter = Right CronNotScheduled
}
oldestInvitationMUTC <- lift $ preview (_head . _entityVal . _invitationExpiresAt . _Just) <$> selectList [InvitationExpiresAt !=. Nothing] [Asc InvitationExpiresAt, LimitTo 1] oldestInvitationMUTC <- lift $ preview (_head . _entityVal . _invitationExpiresAt . _Just) <$> selectList [InvitationExpiresAt !=. Nothing] [Asc InvitationExpiresAt, LimitTo 1]
whenIsJust oldestInvitationMUTC $ \oldestInvUTC -> tell $ HashMap.singleton whenIsJust oldestInvitationMUTC $ \oldestInvUTC -> tell $ HashMap.singleton
@ -139,7 +140,7 @@ determineCrontab = execWriterT $ do
} }
let let
getNextIntervals within interval = do getNextIntervals within interval cInterval = do
now <- liftIO getPOSIXTime now <- liftIO getPOSIXTime
return $ do return $ do
let let
@ -147,7 +148,7 @@ determineCrontab = execWriterT $ do
(currEpoch, epochNow) = now `divMod'` epochInterval (currEpoch, epochNow) = now `divMod'` epochInterval
currInterval = epochNow `div'` interval currInterval = epochNow `div'` interval
numIntervals = floor $ epochInterval / interval numIntervals = floor $ epochInterval / interval
n = ceiling $ 4 * appJobCronInterval / interval n = ceiling $ 4 * cInterval / interval
i <- [ negate (ceiling $ n % 2) .. ceiling $ n % 2 ] i <- [ negate (ceiling $ n % 2) .. ceiling $ n % 2 ]
let let
((+ currEpoch) -> nextEpoch, nextInterval) = (currInterval + i) `divMod` numIntervals ((+ currEpoch) -> nextEpoch, nextInterval) = (currInterval + i) `divMod` numIntervals
@ -158,8 +159,9 @@ determineCrontab = execWriterT $ do
if if
| is _Just appLdapConf | is _Just appLdapConf
, Just syncWithin <- appSynchroniseLdapUsersWithin , Just syncWithin <- appSynchroniseLdapUsersWithin
, Just cInterval <- appJobCronInterval
-> do -> do
nextIntervals <- getNextIntervals syncWithin appSynchroniseLdapUsersInterval nextIntervals <- getNextIntervals syncWithin appSynchroniseLdapUsersInterval cInterval
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do
tell $ HashMap.singleton tell $ HashMap.singleton
@ -177,8 +179,8 @@ determineCrontab = execWriterT $ do
| otherwise | otherwise
-> return () -> return ()
whenIsJust appPruneUnreferencedFilesWithin $ \within -> do whenIsJust ((,) <$> appPruneUnreferencedFilesWithin <*> appJobCronInterval) $ \(within, cInterval) -> do
nextIntervals <- getNextIntervals within appPruneUnreferencedFilesInterval nextIntervals <- getNextIntervals within appPruneUnreferencedFilesInterval cInterval
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do
tell $ HashMap.singleton tell $ HashMap.singleton
(JobCtlQueue JobPruneUnreferencedFiles (JobCtlQueue JobPruneUnreferencedFiles

View File

@ -97,7 +97,7 @@ dispatchJobPruneUnreferencedFiles numIterations epoch iteration = JobHandlerAtom
| i <- [1 .. toInteger numIterations] | i <- [1 .. toInteger numIterations]
] ]
intervalsDgsts' = map (over both $ toDigest <=< assertM' (> 0)) $ zip (0 : init intervals) intervals intervalsDgsts' = zipWith (curry . over both $ toDigest <=< assertM' (> 0)) (0 : init intervals) intervals
toDigest :: Integer -> Maybe FileContentChunkReference toDigest :: Integer -> Maybe FileContentChunkReference
toDigest = fmap (review _Wrapped) . digestFromByteString . pad . ByteString.pack . reverse . unfoldr step toDigest = fmap (review _Wrapped) . digestFromByteString . pad . ByteString.pack . reverse . unfoldr step
@ -270,9 +270,9 @@ dispatchJobRechunkFiles = JobHandlerAtomic . hoist lift $ do
E.where_ $ fileContentEntry' E.^. FileContentEntryHash E.==. fileContentEntry E.^. FileContentEntryHash E.where_ $ fileContentEntry' E.^. FileContentEntryHash E.==. fileContentEntry E.^. FileContentEntryHash
return $ E.sum_ (E.length_ $ fileContentChunk E.^. FileContentChunkContent:: E.SqlExpr (E.Value Word64)) return $ E.sum_ (E.length_ $ fileContentChunk E.^. FileContentChunkContent:: E.SqlExpr (E.Value Word64))
return $ ( fileContentEntry E.^. FileContentEntryHash return ( fileContentEntry E.^. FileContentEntryHash
, size , size
) )
rechunkFile :: FileContentReference -> Word64 -> DB (Sum Natural, Sum Word64) rechunkFile :: FileContentReference -> Word64 -> DB (Sum Natural, Sum Word64)
rechunkFile fRef sz = do rechunkFile fRef sz = do

View File

@ -923,7 +923,7 @@ customMigrations = Map.fromListWith (>>)
) )
, ( AppliedMigrationKey [migrationVersion|39.0.0|] [version|40.0.0|] , ( AppliedMigrationKey [migrationVersion|39.0.0|] [version|40.0.0|]
, whenM (tableExists "study_features") $ , whenM (tableExists "study_features")
[executeQQ| [executeQQ|
ALTER TABLE study_features RENAME updated TO last_observed; ALTER TABLE study_features RENAME updated TO last_observed;
ALTER TABLE study_features ADD COLUMN first_observed timestamp with time zone; ALTER TABLE study_features ADD COLUMN first_observed timestamp with time zone;

View File

@ -1,10 +1,11 @@
module Model.Types.File module Model.Types.File
( FileContentChunkReference(..), FileContentReference(..) ( FileContentChunkReference(..), FileContentReference(..)
, File(..), _fileTitle, _fileContent, _fileModified , File(..), _fileTitle, _fileContent, _fileModified
, PureFile, toPureFile, fromPureFile, pureFileToFileReference, _pureFileContent
, transFile , transFile
, minioFileReference , minioFileReference
, FileReference(..), _fileReferenceTitle, _fileReferenceContent, _fileReferenceModified , FileReference(..), _fileReferenceTitle, _fileReferenceContent, _fileReferenceModified
, HasFileReference(..), IsFileReference(..), FileReferenceResidual(..) , HasFileReference(..), IsFileReference(..), FileReferenceResidual(FileReferenceResidual, FileReferenceResidualEither, unFileReferenceResidualEither, FileReferenceResidualEntity, fileReferenceResidualEntityKey, fileReferenceResidualEntityResidual, unPureFileResidual)
) where ) where
import Import.NoModel import Import.NoModel
@ -17,9 +18,14 @@ import qualified Data.ByteString.Base64.URL as Base64
import qualified Data.ByteArray as ByteArray import qualified Data.ByteArray as ByteArray
import qualified Network.Minio as Minio (Object) import qualified Network.Minio as Minio (Object)
import qualified Crypto.Hash as Crypto (digestFromByteString) import qualified Crypto.Hash as Crypto (digestFromByteString)
import qualified Crypto.Hash.Conduit as Crypto (sinkHash)
import Utils.Lens.TH import Utils.Lens.TH
import qualified Data.Conduit.Combinators as C
import Text.Show
newtype FileContentChunkReference = FileContentChunkReference (Digest SHA3_512) newtype FileContentChunkReference = FileContentChunkReference (Digest SHA3_512)
@ -57,6 +63,64 @@ data File m = File
makeLenses_ ''File makeLenses_ ''File
type PureFile = File Identity
_pureFileContent :: forall bs.
( IsSequence bs
, Element bs ~ Word8
)
=> Lens' PureFile (Maybe bs)
_pureFileContent = lens getPureFileContent setPureFileContent
where
getPureFileContent = fmap (repack . runIdentity . runConduit . (.| C.fold)) . fileContent
setPureFileContent f bs = f { fileContent = yield . repack <$> bs }
toPureFile :: Monad m => File m -> m PureFile
toPureFile File{..} = do
c <- for fileContent $ runConduit . (.| C.fold)
return File
{ fileContent = fmap yield c
, ..
}
fromPureFile :: Monad m => PureFile -> File m
fromPureFile = transFile generalize
pureFileToFileReference :: PureFile -> FileReference
pureFileToFileReference File{..} = FileReference
{ fileReferenceTitle = fileTitle
, fileReferenceContent = review _Wrapped . runIdentity . runConduit . (.| Crypto.sinkHash) <$> fileContent
, fileReferenceModified = fileModified
}
instance Eq PureFile where
a == b = all (\f -> f a b)
[ (==) `on` fileTitle
, (==) `on` fileModified
, (==) `on` (view _pureFileContent :: PureFile -> Maybe ByteString)
]
instance Ord PureFile where
compare = mconcat
[ comparing fileTitle
, comparing (view _pureFileContent :: PureFile -> Maybe ByteString)
, comparing fileModified
]
instance Show PureFile where
showsPrec _ f@File{..}
= showString "File{"
. showString "fileTitle = "
. shows fileTitle
. showString ", "
. showString "fileContent = "
. (case f ^. _pureFileContent of
Nothing -> showString "Nothing"
Just c -> showString "Just $ yield " . showsPrec 11 (c :: ByteString)
)
. showString ", "
. showString "fileModified = "
. shows fileModified
. showString "}"
transFile :: Monad m => (forall a. m a -> n a) -> (File m -> File n) transFile :: Monad m => (forall a. m a -> n a) -> (File m -> File n)
transFile l File{..} = File{ fileContent = transPipe l <$> fileContent, .. } transFile l File{..} = File{ fileContent = transPipe l <$> fileContent, .. }
@ -78,6 +142,24 @@ instance HasFileReference FileReference where
data FileReferenceResidual FileReference = FileReferenceResidual data FileReferenceResidual FileReference = FileReferenceResidual
_FileReference = iso (, FileReferenceResidual) $ view _1 _FileReference = iso (, FileReferenceResidual) $ view _1
instance HasFileReference PureFile where
newtype FileReferenceResidual PureFile = PureFileResidual { unPureFileResidual :: Maybe ByteString }
deriving (Eq, Ord, Read, Show, Generic, Typeable)
_FileReference = iso toFileReference fromFileReference
where
toFileReference File{..} = (FileReference{..}, PureFileResidual{..})
where
fileReferenceTitle = fileTitle
(fileReferenceContent, unPureFileResidual) = ((,) <$> preview (_Just . _1) <*> preview (_Just . _2)) $
over _1 (review _Wrapped) . runIdentity . runConduit . (.| getZipConduit ((,) <$> ZipConduit Crypto.sinkHash <*> ZipConduit C.fold)) <$> fileContent
fileReferenceModified = fileModified
fromFileReference (FileReference{..}, PureFileResidual{..}) = File
{ fileTitle = fileReferenceTitle
, fileContent = yield <$> unPureFileResidual
, fileModified = fileReferenceModified
}
instance (HasFileReference a, HasFileReference b) => HasFileReference (Either a b) where instance (HasFileReference a, HasFileReference b) => HasFileReference (Either a b) where
newtype FileReferenceResidual (Either a b) = FileReferenceResidualEither { unFileReferenceResidualEither :: Either (FileReferenceResidual a) (FileReferenceResidual b) } newtype FileReferenceResidual (Either a b) = FileReferenceResidualEither { unFileReferenceResidualEither :: Either (FileReferenceResidual a) (FileReferenceResidual b) }
_FileReference = iso doSplit doJoin _FileReference = iso doSplit doJoin

View File

@ -115,7 +115,7 @@ data AppSettings = AppSettings
, appMailSupport :: Address , appMailSupport :: Address
, appJobWorkers :: Natural , appJobWorkers :: Natural
, appJobFlushInterval :: Maybe NominalDiffTime , appJobFlushInterval :: Maybe NominalDiffTime
, appJobCronInterval :: NominalDiffTime , appJobCronInterval :: Maybe NominalDiffTime
, appJobStaleThreshold :: NominalDiffTime , appJobStaleThreshold :: NominalDiffTime
, appNotificationRateLimit :: NominalDiffTime , appNotificationRateLimit :: NominalDiffTime
, appNotificationCollateDelay :: NominalDiffTime , appNotificationCollateDelay :: NominalDiffTime
@ -452,7 +452,7 @@ instance FromJSON AppSettings where
appJobWorkers <- o .: "job-workers" appJobWorkers <- o .: "job-workers"
appJobFlushInterval <- o .:? "job-flush-interval" appJobFlushInterval <- o .:? "job-flush-interval"
appJobCronInterval <- o .: "job-cron-interval" appJobCronInterval <- o .:? "job-cron-interval"
appJobStaleThreshold <- o .: "job-stale-threshold" appJobStaleThreshold <- o .: "job-stale-threshold"
appNotificationRateLimit <- o .: "notification-rate-limit" appNotificationRateLimit <- o .: "notification-rate-limit"
appNotificationCollateDelay <- o .: "notification-collate-delay" appNotificationCollateDelay <- o .: "notification-collate-delay"

View File

@ -50,7 +50,7 @@ sinkFileDB doReplace fileContentContent = do
existsChunk <- lift $ exists [FileContentChunkHash ==. fileContentChunkHash] existsChunk <- lift $ exists [FileContentChunkHash ==. fileContentChunkHash]
let setContentBased = updateWhere [FileContentChunkHash ==. fileContentChunkHash] [FileContentChunkContentBased =. fileContentChunkContentBased] let setContentBased = updateWhere [FileContentChunkHash ==. fileContentChunkHash] [FileContentChunkContentBased =. fileContentChunkContentBased]
if | existsChunk -> lift setContentBased if | existsChunk -> lift setContentBased
| otherwise -> lift . handleIfSql isUniqueConstraintViolation (const $ setContentBased) $ | otherwise -> lift . handleIfSql isUniqueConstraintViolation (const setContentBased) $
insert_ FileContentChunk{..} insert_ FileContentChunk{..}
return $ FileContentChunkKey fileContentChunkHash return $ FileContentChunkKey fileContentChunkHash
where fileContentChunkHash = _Wrapped # Crypto.hash fileContentChunkContent where fileContentChunkHash = _Wrapped # Crypto.hash fileContentChunkContent

View File

@ -53,6 +53,9 @@ extra-deps:
- gearhash - gearhash
- fastcdc - fastcdc
- git: git@gitlab2.rz.ifi.lmu.de:uni2work/zip-stream.git
commit: 094c70935f1d92016144cd4dfc0a99b831c56f25
- generic-lens-1.2.0.0@sha256:b19e7970c93743a46bc3702331512a96d163de4356472f2d51a2945887aefe8c,6524 # manual downgrade; won't compile with >=2.0.0.0 - generic-lens-1.2.0.0@sha256:b19e7970c93743a46bc3702331512a96d163de4356472f2d51a2945887aefe8c,6524 # manual downgrade; won't compile with >=2.0.0.0
- acid-state-0.16.0.1@sha256:d43f6ee0b23338758156c500290c4405d769abefeb98e9bc112780dae09ece6f,6207 - acid-state-0.16.0.1@sha256:d43f6ee0b23338758156c500290c4405d769abefeb98e9bc112780dae09ece6f,6207
@ -71,6 +74,7 @@ extra-deps:
- tz-0.1.3.4@sha256:bd311e202b8bdd15bcd6a4ca182e69794949d3b3b9f4aa835e9ccff011284979,5086 - tz-0.1.3.4@sha256:bd311e202b8bdd15bcd6a4ca182e69794949d3b3b9f4aa835e9ccff011284979,5086
- unidecode-0.1.0.4@sha256:99581ee1ea334a4596a09ae3642e007808457c66893b587e965b31f15cbf8c4d,1144 - unidecode-0.1.0.4@sha256:99581ee1ea334a4596a09ae3642e007808457c66893b587e965b31f15cbf8c4d,1144
- wai-middleware-prometheus-1.0.0@sha256:1625792914fb2139f005685be8ce519111451cfb854816e430fbf54af46238b4,1314 - wai-middleware-prometheus-1.0.0@sha256:1625792914fb2139f005685be8ce519111451cfb854816e430fbf54af46238b4,1314
- primitive-0.7.1.0@sha256:6a237bb338bcc43193077ff8e8c0f0ce2de14c652231496a15672e8b563a07e2,2604
resolver: nightly-2020-08-08 resolver: nightly-2020-08-08
compiler: ghc-8.10.2 compiler: ghc-8.10.2

View File

@ -222,6 +222,17 @@ packages:
subdir: fastcdc subdir: fastcdc
git: https://github.com/gkleen/FastCDC.git git: https://github.com/gkleen/FastCDC.git
commit: 7326e2931454282df9081105dad812845db5c530 commit: 7326e2931454282df9081105dad812845db5c530
- completed:
name: zip-stream
version: 0.2.0.1
git: git@gitlab2.rz.ifi.lmu.de:uni2work/zip-stream.git
pantry-tree:
size: 812
sha256: e191cb4e12ae1f056acdcf0393ff4dcb566b3ab82305aa14d6d4e35f49a64c96
commit: 094c70935f1d92016144cd4dfc0a99b831c56f25
original:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/zip-stream.git
commit: 094c70935f1d92016144cd4dfc0a99b831c56f25
- completed: - completed:
hackage: generic-lens-1.2.0.0@sha256:b19e7970c93743a46bc3702331512a96d163de4356472f2d51a2945887aefe8c,6524 hackage: generic-lens-1.2.0.0@sha256:b19e7970c93743a46bc3702331512a96d163de4356472f2d51a2945887aefe8c,6524
pantry-tree: pantry-tree:
@ -341,6 +352,13 @@ packages:
sha256: 6d64803c639ed4c7204ea6fab0536b97d3ee16cdecb9b4a883cd8e56d3c61402 sha256: 6d64803c639ed4c7204ea6fab0536b97d3ee16cdecb9b4a883cd8e56d3c61402
original: original:
hackage: wai-middleware-prometheus-1.0.0@sha256:1625792914fb2139f005685be8ce519111451cfb854816e430fbf54af46238b4,1314 hackage: wai-middleware-prometheus-1.0.0@sha256:1625792914fb2139f005685be8ce519111451cfb854816e430fbf54af46238b4,1314
- completed:
hackage: primitive-0.7.1.0@sha256:6a237bb338bcc43193077ff8e8c0f0ce2de14c652231496a15672e8b563a07e2,2604
pantry-tree:
size: 1376
sha256: 924e88629b493abb6b2f3c3029cef076554a2b627091e3bb6887ec03487a707d
original:
hackage: primitive-0.7.1.0@sha256:6a237bb338bcc43193077ff8e8c0f0ce2de14c652231496a15672e8b563a07e2,2604
snapshots: snapshots:
- completed: - completed:
size: 524392 size: 524392

View File

@ -16,8 +16,6 @@ import qualified Data.Conduit.Combinators as C
import Control.Lens.Extras import Control.Lens.Extras
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import qualified Crypto.Hash as Crypto (hash)
import System.FilePath (dropDrive) import System.FilePath (dropDrive)
import Data.Time.Clock (diffUTCTime) import Data.Time.Clock (diffUTCTime)
@ -25,6 +23,8 @@ import Data.Char (chr)
import Database.Persist.Sql (transactionUndo) import Database.Persist.Sql (transactionUndo)
import Data.Bitraversable
instance Arbitrary (FileReferenceResidual PersonalisedSheetFile) where instance Arbitrary (FileReferenceResidual PersonalisedSheetFile) where
arbitrary = PersonalisedSheetFileResidual arbitrary = PersonalisedSheetFileResidual
@ -59,7 +59,7 @@ spec = withApp . describe "Personalised sheet file zip encoding" $ do
lift (insertUnique user) >>= maybe userLoop return lift (insertUnique user) >>= maybe userLoop return
in userLoop in userLoop
let res = res' { personalisedSheetFileResidualSheet = shid, personalisedSheetFileResidualUser = uid } let res = res' { personalisedSheetFileResidualSheet = shid, personalisedSheetFileResidualUser = uid }
fRef <- lift (sinkFile f :: DB FileReference) fRef <- lift (sinkFile (transFile generalize f) :: DB FileReference)
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
void . lift . insert $ CourseParticipant cid (personalisedSheetFileResidualUser res) now Nothing CourseParticipantActive void . lift . insert $ CourseParticipant cid (personalisedSheetFileResidualUser res) now Nothing CourseParticipantActive
void . lift . insert $ _FileReference # (fRef, res) void . lift . insert $ _FileReference # (fRef, res)
@ -68,30 +68,38 @@ spec = withApp . describe "Personalised sheet file zip encoding" $ do
anonMode <- liftIO $ generate arbitrary anonMode <- liftIO $ generate arbitrary
let let
fpL :: Lens' (Either PersonalisedSheetFile File) FilePath fpL :: forall m. Lens' (Either PersonalisedSheetFile (File m)) FilePath
fpL = lens (either personalisedSheetFileTitle fileTitle) $ \f' path -> case f' of fpL = lens (either personalisedSheetFileTitle fileTitle) $ \f' path -> case f' of
Left pf -> Left pf { personalisedSheetFileTitle = path } Left pf -> Left pf { personalisedSheetFileTitle = path }
Right f -> Right f { fileTitle = path } Right f -> Right f { fileTitle = path }
isDirectory = either (is _Nothing . personalisedSheetFileContent) (is _Nothing . fileContent) isDirectory = either (is _Nothing . personalisedSheetFileContent) (is _Nothing . fileContent)
loadFile :: Either (PersonalisedSheetFileUnresolved (Either PersonalisedSheetFile DBFile)) (Either PersonalisedSheetFile DBFile, FileReferenceResidual PersonalisedSheetFile)
-> DB (Either (PersonalisedSheetFileUnresolved (Either PersonalisedSheetFile PureFile)) (Either PersonalisedSheetFile PureFile, FileReferenceResidual PersonalisedSheetFile))
loadFile = bitraverse loadUnresolved loadResolved
where
loadUnresolved = traverse $ traverse toPureFile
loadResolved (f, fRes) = (, fRes) <$> traverse toPureFile f
recoveredFiles <- runConduit $ recoveredFiles <- runConduit $
sourcePersonalisedSheetFiles cid (Just shid) Nothing anonMode sourcePersonalisedSheetFiles cid (Just shid) Nothing anonMode
.| resolvePersonalisedSheetFiles fpL isDirectory cid shid .| resolvePersonalisedSheetFiles fpL isDirectory cid shid
.| C.mapM loadFile
.| C.foldMap pure .| C.foldMap pure
let let
checkFile :: Either (PersonalisedSheetFileUnresolved (Either PersonalisedSheetFile File)) (Either PersonalisedSheetFile File, FileReferenceResidual PersonalisedSheetFile) checkFile :: Either (PersonalisedSheetFileUnresolved (Either PersonalisedSheetFile PureFile)) (Either PersonalisedSheetFile PureFile, FileReferenceResidual PersonalisedSheetFile)
-> (File, FileReferenceResidual PersonalisedSheetFile) -> (PureFile, FileReferenceResidual PersonalisedSheetFile)
-> Bool -> Bool
checkFile (Left _) _ checkFile (Left _) _
= False = False
checkFile (Right (recFile, recResidual)) (file, residual) checkFile (Right (recFile, recResidual)) (file, residual)
= recResidual == residual = recResidual == residual
&& case recFile of && case recFile of
Right f -> file == f Right f -> f == file
Left pf -> dropDrive (fileTitle file) == dropDrive (personalisedSheetFileTitle pf) Left pf -> dropDrive (fileTitle file) == dropDrive (personalisedSheetFileTitle pf)
&& abs (fileModified file `diffUTCTime` personalisedSheetFileModified pf) < 1e-6 -- Precision is a PostgreSQL limitation && abs (fileModified file `diffUTCTime` personalisedSheetFileModified pf) < 1e-6 -- Precision is a PostgreSQL limitation
&& fmap Crypto.hash (fileContent file) == personalisedSheetFileContent pf && fileReferenceContent (pureFileToFileReference file) == personalisedSheetFileContent pf
errors = go [] sheetFiles recoveredFiles errors = go [] sheetFiles recoveredFiles
where go acc xs [] = reverse acc ++ map Left xs where go acc xs [] = reverse acc ++ map Left xs

View File

@ -15,6 +15,8 @@ import Text.Shakespeare.I18N (renderMessage)
import Utils.Lens (_ratingValues, _ratingPoints) import Utils.Lens (_ratingValues, _ratingPoints)
import qualified Data.Conduit.Combinators as C
spec :: Spec spec :: Spec
spec = describe "Rating file parsing/pretty-printing" $ do spec = describe "Rating file parsing/pretty-printing" $ do
@ -33,7 +35,7 @@ spec = describe "Rating file parsing/pretty-printing" $ do
mr' = MsgRenderer $ renderMessage (error "foundation inspected" :: site) [] mr' = MsgRenderer $ renderMessage (error "foundation inspected" :: site) []
parseRating' :: LBS.ByteString -> Maybe Rating' parseRating' :: LBS.ByteString -> Maybe Rating'
parseRating' = either (\(_ :: SomeException) -> Nothing) (Just . fst) . parseRating . flip (File "bewertung.txt") time . Just . LBS.toStrict parseRating' = either (\(_ :: SomeException) -> Nothing) (Just . fst) . parseRating . flip (File "bewertung.txt") time . Just . C.sourceLazy
time = UTCTime systemEpochDay 0 time = UTCTime systemEpochDay 0
mRating rating = rating { ratingValues = mRating' rating $ ratingValues rating } mRating rating = rating { ratingValues = mRating' rating $ ratingValues rating }

View File

@ -1,11 +1,14 @@
{-# OPTIONS_GHC -Wno-error=deprecations #-}
module Handler.Utils.ZipSpec where module Handler.Utils.ZipSpec where
import TestImport import TestImport
import Utils ((<//>))
import Handler.Utils.Zip import Handler.Utils.Zip
import Data.Conduit import Data.Conduit
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.Combinators as C
import Data.List (dropWhileEnd) import Data.List (dropWhileEnd)
@ -14,16 +17,63 @@ import ModelSpec ()
import System.FilePath import System.FilePath
import Data.Time import Data.Time
import Data.Universe
data ZipConsumptionStrategy
= ZipConsumeInterleaved
| ZipConsumeBuffered
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
data ZipProductionStrategy
= ZipProduceInterleaved
| ZipProduceBuffered
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
spec :: Spec spec :: Spec
spec = describe "Zip file handling" $ do spec = describe "Zip file handling" $ do
it "has compatible encoding/decoding to/from zip files" . property $ do describe "has compatible encoding to, and decoding from zip files" . forM_ universeF $ \strategy ->
zipFiles <- listOf $ scale (`div` 2) arbitrary modifyMaxSuccess (bool id (37 *) $ strategy == (ZipProduceInterleaved, ZipConsumeInterleaved)) . it (show strategy) . property $ do
return . property $ do zipFiles <- listOf arbitrary :: Gen [PureFile]
zipFiles' <- runConduit $ Conduit.sourceList zipFiles .| produceZip def .| void consumeZip .| Conduit.consume return . property $ do
forM_ (zipFiles `zip` zipFiles') $ \(file, file') -> do
let acceptableFilenameChanges let zipProduceBuffered
= makeValid . dropWhile isPathSeparator . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator (isNothing $ fileContent file) . normalise . makeValid = evaluate . force <=< runConduitRes $ zipProduceInterleaved .| C.sinkLazy
zipProduceInterleaved
= C.yieldMany zipFiles .| C.map fromPureFile .| produceZip def
zipConsumeBuffered zipProd
= mapM toPureFile <=< runConduitRes $ void (consumeZip zipProd) .| C.foldMap pure
zipConsumeInterleaved zipProd
= void (consumeZip zipProd) .| C.mapM toPureFile .| C.foldMap pure
zipFiles' <- case strategy of
(ZipProduceBuffered, ZipConsumeInterleaved) ->
runConduitRes . zipConsumeInterleaved . C.sourceLazy =<< zipProduceBuffered
(ZipProduceBuffered, ZipConsumeBuffered) ->
zipConsumeBuffered . C.sourceLazy =<< zipProduceBuffered
(ZipProduceInterleaved, ZipConsumeInterleaved) ->
runConduitRes $ zipConsumeInterleaved zipProduceInterleaved
(ZipProduceInterleaved, ZipConsumeBuffered) ->
zipConsumeBuffered zipProduceInterleaved
let acceptableFilenameChanges file
= "." <//> fileTitle file
& normalise
& makeValid
& dropWhile isPathSeparator
& dropWhileEnd isPathSeparator
& bool id addTrailingPathSeparator (isNothing $ fileContent file)
& normalise
& makeValid
acceptableTimeDifference t1 t2 = abs (diffUTCTime t1 t2) <= 2 acceptableTimeDifference t1 t2 = abs (diffUTCTime t1 t2) <= 2
(shouldBe `on` acceptableFilenameChanges) (fileTitle file') (fileTitle file)
(fileModified file', fileModified file) `shouldSatisfy` uncurry acceptableTimeDifference forM_ (zipFiles `zip` zipFiles') $ \(file, file') -> do
(fileContent file') `shouldBe` (fileContent file) when (((/=) `on` acceptableFilenameChanges) file' file) $ do
traceM $ show (fileTitle file, fileTitle file')
traceM $ show (acceptableFilenameChanges file, acceptableFilenameChanges file')
traceM $ show (isNothing $ fileContent file, isNothing $ fileContent file')
(shouldBe `on` acceptableFilenameChanges) file' file
(fileModified file', fileModified file) `shouldSatisfy` uncurry acceptableTimeDifference
(view _pureFileContent file' :: Maybe ByteString) `shouldBe` (view _pureFileContent file)

View File

@ -34,6 +34,10 @@ import Control.Monad.Catch.Pure (Catch, runCatch)
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import qualified Data.Conduit.Combinators as C
import Data.Ratio ((%))
instance Arbitrary EmailAddress where instance Arbitrary EmailAddress where
arbitrary = do arbitrary = do
@ -137,12 +141,18 @@ instance Arbitrary User where
return User{..} return User{..}
shrink = genericShrink shrink = genericShrink
instance Arbitrary File where scaleRatio :: Rational -> Int -> Int
scaleRatio r = ceiling . (* r) . fromIntegral
instance Monad m => Arbitrary (File m) where
arbitrary = do arbitrary = do
fileTitle <- scale (`div` 2) $ (joinPath <$> arbitrary) `suchThat` (any $ not . isPathSeparator) fileTitle <- scale (scaleRatio $ 1 % 8) $ (joinPath <$> arbitrary) `suchThat` (any $ not . isPathSeparator)
date <- addDays <$> arbitrary <*> pure (fromGregorian 2043 7 2) date <- addDays <$> arbitrary <*> pure (fromGregorian 2043 7 2)
fileModified <- (addUTCTime <$> arbitrary <*> pure (UTCTime date 0)) `suchThat` inZipRange fileModified <- (addUTCTime <$> arbitrary <*> pure (UTCTime date 0)) `suchThat` inZipRange
fileContent <- arbitrary fileContent <- oneof
[ pure Nothing
, Just . C.sourceLazy <$> scale (scaleRatio $ 7 % 8) arbitrary
]
return File{..} return File{..}
where where
inZipRange :: UTCTime -> Bool inZipRange :: UTCTime -> Bool
@ -152,7 +162,6 @@ instance Arbitrary File where
= True = True
| otherwise | otherwise
= False = False
shrink = genericShrink
instance Arbitrary School where instance Arbitrary School where
arbitrary = do arbitrary = do
@ -177,8 +186,8 @@ spec = do
parallel $ do parallel $ do
lawsCheckHspec (Proxy @User) lawsCheckHspec (Proxy @User)
[ eqLaws, jsonLaws ] [ eqLaws, jsonLaws ]
lawsCheckHspec (Proxy @File) lawsCheckHspec (Proxy @PureFile)
[ eqLaws ] [ eqLaws, ordLaws ]
lawsCheckHspec (Proxy @School) lawsCheckHspec (Proxy @School)
[ eqLaws ] [ eqLaws ]
lawsCheckHspec (Proxy @Term) lawsCheckHspec (Proxy @Term)

View File

@ -22,6 +22,7 @@ import Yesod.Auth as X
import Yesod.Test as X import Yesod.Test as X
import Yesod.Core.Unsafe (fakeHandlerGetLogger) import Yesod.Core.Unsafe (fakeHandlerGetLogger)
import Test.QuickCheck as X import Test.QuickCheck as X
import Test.Hspec.QuickCheck as X hiding (prop)
import Test.QuickCheck.Gen as X import Test.QuickCheck.Gen as X
import Data.Default as X import Data.Default as X
import Test.QuickCheck.Instances as X () import Test.QuickCheck.Instances as X ()
@ -65,6 +66,8 @@ import Handler.Utils (runAppLoggingT)
import Web.PathPieces (toPathPiece) import Web.PathPieces (toPathPiece)
import Utils.Parameters (GlobalPostParam(PostLoginDummy)) import Utils.Parameters (GlobalPostParam(PostLoginDummy))
import Control.Monad.Morph as X (generalize)
runDB :: SqlPersistM a -> YesodExample UniWorX a runDB :: SqlPersistM a -> YesodExample UniWorX a
runDB query = do runDB query = do