Merge branch '631-content-dependent-chunking' into master

This commit is contained in:
Gregor Kleen 2020-09-11 12:05:55 +02:00
commit 7d559cc0f1
47 changed files with 1168 additions and 395 deletions

View File

@ -35,7 +35,8 @@ bearer-expiration: 604800
bearer-encoding: HS256 bearer-encoding: HS256
maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728"
session-files-expire: 3600 session-files-expire: 3600
prune-unreferenced-files: 28800 prune-unreferenced-files-within: 57600
prune-unreferenced-files-interval: 3600
keep-unreferenced-files: 86400 keep-unreferenced-files: 86400
health-check-interval: health-check-interval:
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600" matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
@ -158,7 +159,13 @@ upload-cache:
auto-discover-region: "_env:UPLOAD_S3_AUTO_DISCOVER_REGION:true" auto-discover-region: "_env:UPLOAD_S3_AUTO_DISCOVER_REGION:true"
disable-cert-validation: "_env:UPLOAD_S3_DISABLE_CERT_VALIDATION:false" disable-cert-validation: "_env:UPLOAD_S3_DISABLE_CERT_VALIDATION:false"
upload-cache-bucket: "uni2work-uploads" upload-cache-bucket: "uni2work-uploads"
inject-files: 10
inject-files: 307
rechunk-files: 601
file-upload-db-chunksize: 4194304 # 4MiB
file-chunking-target-exponent: 21 # 2MiB
file-chunking-hash-window: 4096
server-sessions: server-sessions:
idle-timeout: 28807 idle-timeout: 28807
@ -229,6 +236,9 @@ token-buckets:
depth: 1572864000 # 1500MiB depth: 1572864000 # 1500MiB
inv-rate: 1.9e-6 # 2MiB/s inv-rate: 1.9e-6 # 2MiB/s
initial-value: 0 initial-value: 0
rechunk-files:
depth: 20971520 # 20MiB
inv-rate: 9.5e-7 # 1MiB/s
initial-value: 0
fallback-personalised-sheet-files-keys-expire: 2419200 fallback-personalised-sheet-files-keys-expire: 2419200

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

@ -1,9 +1,20 @@
FileContent FileContentEntry
hash FileContentReference hash FileContentReference
ix Natural
chunkHash FileContentChunkId
UniqueFileContentEntry hash ix
FileContentChunk
hash FileContentChunkReference
content ByteString content ByteString
unreferencedSince UTCTime Maybe contentBased Bool default=false -- For Migration
Primary hash Primary hash
FileContentChunkUnreferenced
hash FileContentChunkId
since UTCTime
UniqueFileContentChunkUnreferenced hash
SessionFile SessionFile
content FileContentReference Maybe content FileContentReference Maybe
touched UTCTime touched UTCTime
@ -12,3 +23,8 @@ FileLock
content FileContentReference content FileContentReference
instance InstanceId instance InstanceId
time UTCTime time UTCTime
FileChunkLock
hash FileContentChunkReference
instance InstanceId
time UTCTime

View File

@ -151,6 +151,7 @@ dependencies:
- minio-hs - minio-hs
- network-ip - network-ip
- data-textual - data-textual
- fastcdc
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -227,13 +227,15 @@ makeFoundation appSettings'@AppSettings{..} = do
forM_ ldapPool $ registerFailoverMetrics "ldap" forM_ ldapPool $ registerFailoverMetrics "ldap"
-- Perform database migration using our application's logging settings. -- Perform database migration using our application's logging settings.
if flip runReaderT tempFoundation $
| appAutoDbMigrate -> do if
$logDebugS "setup" "Migration" | appAutoDbMigrate -> do
migrateAll `runSqlPool` sqlPool $logDebugS "setup" "Migration"
| otherwise -> whenM (requiresMigration `runSqlPool` sqlPool) $ do migrateAll `runSqlPool` sqlPool
$logErrorS "setup" "Migration required" | otherwise -> whenM (requiresMigration `runSqlPool` sqlPool) $ do
liftIO . exitWith $ ExitFailure 2 $logErrorS "setup" "Migration required"
liftIO . exitWith $ ExitFailure 2
$logDebugS "setup" "Cluster-Config" $logDebugS "setup" "Cluster-Config"
appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool
appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool

View File

@ -18,6 +18,8 @@ import Data.Aeson as Aeson
import Control.Monad.Fail import Control.Monad.Fail
import Language.Haskell.TH.Syntax (Lift(liftTyped))
import Instances.TH.Lift ()
instance HashAlgorithm hash => PersistField (Digest hash) where instance HashAlgorithm hash => PersistField (Digest hash) where
toPersistValue = PersistByteString . convert toPersistValue = PersistByteString . convert
@ -46,3 +48,6 @@ instance HashAlgorithm hash => FromJSON (Digest hash) where
instance Hashable (Digest hash) where instance Hashable (Digest hash) where
hashWithSalt s = (hashWithSalt s :: ByteString -> Int) . convert hashWithSalt s = (hashWithSalt s :: ByteString -> Int) . convert
instance HashAlgorithm hash => Lift (Digest hash) where
liftTyped dgst = [||fromMaybe (error "Lifted digest has wrong length") $ digestFromByteString $$(liftTyped (convert dgst :: ByteString))||]

View File

@ -6,6 +6,7 @@ module Database.Esqueleto.Utils
, justVal, justValList , justVal, justValList
, isJust , isJust
, isInfixOf, hasInfix , isInfixOf, hasInfix
, strConcat, substring
, or, and , or, and
, any, all , any, all
, subSelectAnd, subSelectOr , subSelectAnd, subSelectOr
@ -39,7 +40,8 @@ import qualified Data.Set as Set
import qualified Data.List as List import qualified Data.List as List
import qualified Data.Foldable as F import qualified Data.Foldable as F
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E import qualified Database.Esqueleto.PostgreSQL as E
import qualified Database.Esqueleto.Internal.Internal as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
import qualified Data.Text.Lazy as Lazy (Text) import qualified Data.Text.Lazy as Lazy (Text)
@ -96,6 +98,42 @@ hasInfix :: ( E.SqlString s1
=> E.SqlExpr (E.Value s2) -> E.SqlExpr (E.Value s1) -> E.SqlExpr (E.Value Bool) => E.SqlExpr (E.Value s2) -> E.SqlExpr (E.Value s1) -> E.SqlExpr (E.Value Bool)
hasInfix = flip isInfixOf hasInfix = flip isInfixOf
infixl 6 `strConcat`
strConcat :: E.SqlString s
=> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s)
strConcat = E.unsafeSqlBinOp " || "
substring :: ( E.SqlString str
, Num from, Num for
)
=> E.SqlExpr (E.Value str)
-> E.SqlExpr (E.Value from)
-> E.SqlExpr (E.Value for)
-> E.SqlExpr (E.Value str)
substring (E.ERaw p1 f1) (E.ERaw p2 f2) (E.ERaw p3 f3)
= E.ERaw E.Never $ \info ->
let (strTLB, strVals) = f1 info
(fromiTLB, fromiVals) = f2 info
(foriTLB, foriVals) = f3 info
in ( "SUBSTRING" <> E.parens (E.parensM p1 strTLB <> " FROM " <> E.parensM p2 fromiTLB <> " FOR " <> E.parensM p3 foriTLB)
, strVals <> fromiVals <> foriVals
)
substring a b c = substring (construct a) (construct b) (construct c)
where construct :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a)
construct (E.ERaw p f) = E.ERaw E.Parens $ \info ->
let (b1, vals) = f info
build ("?", [E.PersistList vals']) =
(E.uncommas $ replicate (length vals') "?", vals')
build expr = expr
in build (E.parensM p b1, vals)
construct (E.ECompositeKey f) =
E.ERaw E.Parens $ \info -> (E.uncommas $ f info, mempty)
construct (E.EAliasedValue i _) =
E.ERaw E.Never $ E.aliasedValueIdentToRawSql i
construct (E.EValueReference i i') =
E.ERaw E.Never $ E.valueReferenceToRawSql i i'
and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool)
and = F.foldr (E.&&.) true and = F.foldr (E.&&.) true
or = F.foldr (E.||.) false or = F.foldr (E.||.) false
@ -111,8 +149,11 @@ all :: MonoFoldable f => (Element f -> E.SqlExpr (E.Value Bool)) -> f -> E.SqlEx
all test = and . map test . otoList all test = and . map test . otoList
subSelectAnd, subSelectOr :: E.SqlQuery (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) subSelectAnd, subSelectOr :: E.SqlQuery (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool)
subSelectAnd q = E.subSelectUnsafe $ E.unsafeSqlFunction "bool_and" <$> q subSelectAnd q = parens . E.subSelectUnsafe $ flip (E.unsafeSqlAggregateFunction "bool_and" E.AggModeAll) [] <$> q
subSelectOr q = E.subSelectUnsafe $ E.unsafeSqlFunction "bool_or" <$> q subSelectOr q = parens . E.subSelectUnsafe $ flip (E.unsafeSqlAggregateFunction "bool_or" E.AggModeAll) [] <$> q
parens :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a)
parens = E.unsafeSqlFunction ""
-- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples -- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples

View File

@ -7,7 +7,7 @@ module Foundation.Type
, _SessionStorageMemcachedSql, _SessionStorageAcid , _SessionStorageMemcachedSql, _SessionStorageAcid
, SMTPPool , SMTPPool
, _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport , _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport
, DB, Form, MsgRenderer, MailM , DB, Form, MsgRenderer, MailM, DBFile
) where ) where
import Import.NoFoundation import Import.NoFoundation
@ -81,3 +81,4 @@ type DB = YesodDB UniWorX
type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, WidgetFor UniWorX ()) type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, WidgetFor UniWorX ())
type MsgRenderer = MsgRendererS UniWorX -- see Utils type MsgRenderer = MsgRendererS UniWorX -- see Utils
type MailM a = MailT (HandlerFor UniWorX) a type MailM a = MailT (HandlerFor UniWorX) a
type DBFile = File (YesodDB UniWorX)

View File

@ -80,12 +80,12 @@ testDownload = do
sourceDBChunks :: ConduitT () Int DB () sourceDBChunks :: ConduitT () Int DB ()
sourceDBChunks = forever sourceDBFiles sourceDBChunks = forever sourceDBFiles
.| C.mapM (\x -> x <$ $logDebugS "testDownload.sourceDBChunks" (tshow $ entityKey x)) .| C.mapM (\x -> x <$ $logDebugS "testDownload.sourceDBChunks" (tshow $ entityKey x))
.| C.map ((length $!!) . fileContentContent . entityVal) .| C.map ((length $!!) . fileContentChunkContent . entityVal)
.| takeLimit dlMaxSize .| takeLimit dlMaxSize
where where
sourceDBFiles = E.selectSource . E.from $ \fileContent -> do sourceDBFiles = E.selectSource . E.from $ \fileContentChunk -> do
E.orderBy [E.asc $ E.random_ @Int64] E.orderBy [E.asc $ E.random_ @Int64]
return fileContent return fileContentChunk
takeLimit n | n <= 0 = return () takeLimit n | n <= 0 = return ()
takeLimit n = do takeLimit n = do

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
@ -195,7 +195,7 @@ sourcePersonalisedSheetFiles :: forall m.
-> Maybe SheetId -> Maybe SheetId
-> Maybe (Set UserId) -> Maybe (Set UserId)
-> PersonalisedSheetFilesDownloadAnonymous -> PersonalisedSheetFilesDownloadAnonymous
-> ConduitT () (Either PersonalisedSheetFile File) (SqlPersistT m) () -> ConduitT () (Either PersonalisedSheetFile DBFile) (SqlPersistT m) ()
sourcePersonalisedSheetFiles cId mbsid mbuids anonMode = do sourcePersonalisedSheetFiles cId mbsid mbuids anonMode = do
(mbIdx, cIDKey) <- lift . newPersonalisedFilesKey $ maybe (Left cId) Right mbsid (mbIdx, cIDKey) <- lift . newPersonalisedFilesKey $ maybe (Left cId) Right mbsid
let let
@ -255,9 +255,10 @@ sourcePersonalisedSheetFiles cId mbsid mbuids anonMode = do
, fileModified = courseParticipantRegistration , fileModified = courseParticipantRegistration
} }
yieldM . fmap Right $ do yieldM . fmap Right $ do
fileContent <- lift $ Just . toStrict <$> formatPersonalisedSheetFilesMeta anonMode cPart cID fileContent' <- lift $ formatPersonalisedSheetFilesMeta anonMode cPart cID
let fileTitle = (dirName <//>) . ensureExtension "yaml" . unpack . mr $ MsgPersonalisedSheetFilesMetaFilename cID let fileTitle = (dirName <//>) . ensureExtension "yaml" . unpack . mr $ MsgPersonalisedSheetFilesMetaFilename cID
fileModified = courseParticipantRegistration fileModified = courseParticipantRegistration
fileContent = Just $ C.sourceLazy fileContent'
return File{..} return File{..}
_dirCache %= Set.insert dirName _dirCache %= Set.insert dirName
whenIsJust mbPFile $ \(Entity _ pFile@PersonalisedSheetFile{..}) -> do whenIsJust mbPFile $ \(Entity _ pFile@PersonalisedSheetFile{..}) -> do

View File

@ -11,8 +11,6 @@ import Handler.Utils.Submission
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Text.Encoding as Text
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Data.Conduit.Combinators as Conduit import qualified Data.Conduit.Combinators as Conduit
@ -32,9 +30,8 @@ getSubDownloadR tid ssh csh shn cID (submissionFileTypeIsUpdate -> isUpdate) pat
case isRating of case isRating of
True True
| isUpdate -> runDB $ do | isUpdate -> maybe notFound sendThisFile <=< runDB . runMaybeT $
file <- runMaybeT $ lift . ratingFile cID =<< MaybeT (getRating submissionID) lift . ratingFile cID =<< MaybeT (getRating submissionID)
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
| otherwise -> notFound | otherwise -> notFound
False -> do False -> do
let results = (.| Conduit.map entityVal) . E.selectSource . E.from $ \sf -> do let results = (.| Conduit.map entityVal) . E.selectSource . E.from $ \sf -> do

View File

@ -34,11 +34,13 @@ import Control.Monad.Logger
-- | Simply send a `File`-Value -- | Simply send a `File`-Value
sendThisFile :: File -> Handler TypedContent sendThisFile :: DBFile -> Handler TypedContent
sendThisFile File{..} sendThisFile File{..}
| Just fileContent' <- fileContent = do | Just fileContent' <- fileContent = do
setContentDisposition' . Just $ takeFileName fileTitle setContentDisposition' . Just $ takeFileName fileTitle
return $ TypedContent (simpleContentType (mimeLookup $ pack fileTitle) <> "; charset=utf-8") (toContent fileContent') let cType = simpleContentType (mimeLookup $ pack fileTitle) <> "; charset=utf-8"
respondSourceDB cType $
fileContent' .| Conduit.map toFlushBuilder
| otherwise = sendResponseStatus noContent204 () | otherwise = sendResponseStatus noContent204 ()
-- | Serve a single file, identified through a given DB query -- | Serve a single file, identified through a given DB query
@ -46,7 +48,7 @@ serveOneFile :: forall file. HasFileReference file => ConduitT () file (YesodDB
serveOneFile source = do serveOneFile source = do
results <- runDB . runConduit $ source .| Conduit.take 2 -- We don't need more than two files to make a decision below results <- runDB . runConduit $ source .| Conduit.take 2 -- We don't need more than two files to make a decision below
case results of case results of
[file] -> sendThisFile =<< runDB (sourceFile' file) [file] -> sendThisFile $ sourceFile' file
[] -> notFound [] -> notFound
_other -> do _other -> do
$logErrorS "SFileR" "Multiple matching files found." $logErrorS "SFileR" "Multiple matching files found."
@ -58,7 +60,7 @@ serveOneFile source = do
serveSomeFiles :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent serveSomeFiles :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent
serveSomeFiles archiveName source = serveSomeFiles' archiveName $ source .| C.map Left serveSomeFiles archiveName source = serveSomeFiles' archiveName $ source .| C.map Left
serveSomeFiles' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file File) (YesodDB UniWorX) () -> Handler TypedContent serveSomeFiles' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file DBFile) (YesodDB UniWorX) () -> Handler TypedContent
serveSomeFiles' archiveName source = do serveSomeFiles' archiveName source = do
(source', results) <- runDB $ runPeekN 2 source (source', results) <- runDB $ runPeekN 2 source
@ -66,7 +68,7 @@ serveSomeFiles' archiveName source = do
case results of case results of
[] -> notFound [] -> notFound
[file] -> sendThisFile =<< either (runDB . sourceFile') return file [file] -> sendThisFile $ either sourceFile' id file
_moreFiles -> do _moreFiles -> do
setContentDisposition' $ Just archiveName setContentDisposition' $ Just archiveName
respondSourceDB typeZip $ do respondSourceDB typeZip $ do
@ -79,7 +81,7 @@ serveSomeFiles' archiveName source = do
serveZipArchive :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent serveZipArchive :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent
serveZipArchive archiveName source = serveZipArchive' archiveName $ source .| C.map Left serveZipArchive archiveName source = serveZipArchive' archiveName $ source .| C.map Left
serveZipArchive' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file File) (YesodDB UniWorX) () -> Handler TypedContent serveZipArchive' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file DBFile) (YesodDB UniWorX) () -> Handler TypedContent
serveZipArchive' archiveName source = do serveZipArchive' archiveName source = do
(source', results) <- runDB $ runPeekN 1 source (source', results) <- runDB $ runPeekN 1 source

View File

@ -276,7 +276,7 @@ storeAllocationResult :: AllocationId
-> (AllocationFingerprint, Set (UserId, CourseId), Seq MatchingLogRun) -> (AllocationFingerprint, Set (UserId, CourseId), Seq MatchingLogRun)
-> DB () -> DB ()
storeAllocationResult allocId now (allocFp, allocMatchings, ppMatchingLog -> allocLog) = do storeAllocationResult allocId now (allocFp, allocMatchings, ppMatchingLog -> allocLog) = do
FileReference{..} <- sinkFile $ File "matchings.log" (Just $ encodeUtf8 allocLog) now FileReference{..} <- sinkFile $ File "matchings.log" (Just . yield $ encodeUtf8 allocLog) now
insert_ . AllocationMatching allocId allocFp now $ fromMaybe (error "allocation result stored without fileReferenceContent") fileReferenceContent insert_ . AllocationMatching allocId allocFp now $ fromMaybe (error "allocation result stored without fileReferenceContent") fileReferenceContent
doAllocation allocId now allocMatchings doAllocation allocId now allocMatchings

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

@ -2,17 +2,22 @@ module Handler.Utils.Files
( sourceFile, sourceFile' ( sourceFile, sourceFile'
, sourceFiles, sourceFiles' , sourceFiles, sourceFiles'
, SourceFilesException(..) , SourceFilesException(..)
, sourceFileDB
, acceptFile
) where ) where
import Import import Import
import qualified Data.Conduit.Combinators as C import qualified Data.Conduit.Combinators as C
import qualified Data.Conduit.List as C (unfoldM)
import Handler.Utils.Minio import Handler.Utils.Minio
import qualified Network.Minio as Minio import qualified Network.Minio as Minio
import qualified Data.ByteString.Base64.URL as Base64 import qualified Database.Esqueleto as E
import qualified Data.ByteArray as ByteArray import qualified Database.Esqueleto.Utils as E
import System.FilePath (normalise)
data SourceFilesException data SourceFilesException
@ -22,36 +27,72 @@ data SourceFilesException
deriving anyclass (Exception) deriving anyclass (Exception)
sourceFiles :: ConduitT FileReference File (YesodDB UniWorX) () sourceFileDB :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX)
sourceFiles = C.mapM sourceFile => FileContentReference -> ConduitT () ByteString (SqlPersistT m) ()
sourceFileDB fileReference = do
dbChunksize <- getsYesod $ view _appFileUploadDBChunksize
let retrieveChunk chunkHash = \case
Nothing -> return Nothing
Just start -> do
chunk <- E.selectMaybe . E.from $ \fileContentChunk -> do
E.where_ $ fileContentChunk E.^. FileContentChunkId E.==. E.val chunkHash
return $ E.substring (fileContentChunk E.^. FileContentChunkContent) (E.val start) (E.val dbChunksize)
case chunk of
Nothing -> throwM SourceFilesContentUnavailable
Just (E.Value c) -> return . Just . (c, ) $ if
| olength c >= dbChunksize -> Just $ start + dbChunksize
| otherwise -> Nothing
chunkHashes = E.selectSource . E.from $ \fileContentEntry -> do
E.where_ $ fileContentEntry E.^. FileContentEntryHash E.==. E.val fileReference
E.orderBy [ E.asc $ fileContentEntry E.^. FileContentEntryIx ]
return $ fileContentEntry E.^. FileContentEntryChunkHash
chunkHashes .| C.map E.unValue .| awaitForever (\chunkHash -> C.unfoldM (retrieveChunk chunkHash) $ Just (1 :: Int))
sourceFile :: FileReference -> DB File
sourceFile FileReference{..} = do
mFileContent <- traverse get $ FileContentKey <$> fileReferenceContent
fileContent <- if
| is (_Just . _Nothing) mFileContent
, Just fileContentHash <- fileReferenceContent -- Not a restriction
-> maybeT (throwM SourceFilesContentUnavailable) $ do
let uploadName = decodeUtf8 . Base64.encodeUnpadded $ ByteArray.convert fileContentHash
uploadBucket <- getsYesod $ views appSettings appUploadCacheBucket
fmap Just . hoistMaybe <=< runAppMinio . runMaybeT $ do
objRes <- catchIfMaybeT minioIsDoesNotExist $ Minio.getObject uploadBucket uploadName Minio.defaultGetObjectOptions
lift . runConduit $ Minio.gorObjectStream objRes .| C.fold
| fmap (fmap fileContentHash) mFileContent /= fmap Just fileReferenceContent
-> throwM SourceFilesMismatchedHashes
| Just fileContent' <- fileContentContent <$> join mFileContent
-> return $ Just fileContent'
| otherwise
-> return Nothing
return File sourceFiles :: Monad m => ConduitT FileReference DBFile m ()
{ fileTitle = fileReferenceTitle sourceFiles = C.map sourceFile
, fileContent
, fileModified = fileReferenceModified
}
sourceFiles' :: forall file. HasFileReference file => ConduitT file File (YesodDB UniWorX) () sourceFile :: FileReference -> DBFile
sourceFiles' = C.mapM sourceFile' sourceFile FileReference{..} = File
{ fileTitle = fileReferenceTitle
, fileModified = fileReferenceModified
, fileContent = toFileContent <$> fileReferenceContent
}
where
toFileContent fileReference
| fileReference == $$(liftTyped $ FileContentReference $$(emptyHash))
= return ()
toFileContent fileReference = do
inDB <- lift . E.selectExists . E.from $ \fileContentEntry -> E.where_ $ fileContentEntry E.^. FileContentEntryHash E.==. E.val fileReference
if
| inDB -> sourceFileDB fileReference
| otherwise -> do
chunkVar <- newEmptyTMVarIO
minioAsync <- lift . allocateLinkedAsync $
maybeT (throwM SourceFilesContentUnavailable) $ do
let uploadName = minioFileReference # fileReference
uploadBucket <- getsYesod $ views appSettings appUploadCacheBucket
hoistMaybe <=< runAppMinio . runMaybeT $ do
objRes <- catchIfMaybeT minioIsDoesNotExist $ Minio.getObject uploadBucket uploadName Minio.defaultGetObjectOptions
lift . runConduit $ Minio.gorObjectStream objRes .| C.mapM_ (atomically . putTMVar chunkVar . Just)
atomically $ putTMVar chunkVar Nothing
let go = do
mChunk <- atomically $ readTMVar chunkVar
case mChunk of
Nothing -> waitAsync minioAsync
Just chunk -> yield chunk >> go
in go
sourceFile' :: forall file. HasFileReference file => file -> DB File sourceFiles' :: forall file m. (HasFileReference file, Monad m) => ConduitT file DBFile m ()
sourceFiles' = C.map sourceFile'
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

@ -32,7 +32,7 @@ import Yesod.Form.Bootstrap3
import Handler.Utils.Zip import Handler.Utils.Zip
import qualified Data.Conduit.Combinators as C import qualified Data.Conduit.Combinators as C
import qualified Data.Conduit.List as C (mapMaybe) import qualified Data.Conduit.List as C (mapMaybe, mapMaybeM)
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
@ -831,7 +831,10 @@ pseudonymWordField = checkMMap doCheck id $ ciField & addDatalist (return $ mkOp
uploadContents :: (MonadHandler m, HandlerSite m ~ UniWorX) => ConduitT FileReference ByteString m () uploadContents :: (MonadHandler m, HandlerSite m ~ UniWorX) => ConduitT FileReference ByteString m ()
uploadContents = transPipe (liftHandler . runDB) sourceFiles .| C.mapMaybe fileContent uploadContents = transPipe (liftHandler . runDB) sourceFiles .| C.mapMaybeM fileContent'
where fileContent' f = runMaybeT $ do
File{fileContent = Just fc} <- return f
liftHandler . runDB . runConduit $ fc .| C.fold
data FileFieldUserOption a = FileFieldUserOption data FileFieldUserOption a = FileFieldUserOption
{ fieldOptionForce :: Bool { fieldOptionForce :: Bool
@ -893,11 +896,21 @@ genericFileField mkOpts = Field{..}
, Map.filter (views _3 $ (&&) <$> fieldOptionForce <*> fieldOptionDefault) fieldAdditionalFiles , Map.filter (views _3 $ (&&) <$> fieldOptionForce <*> fieldOptionDefault) fieldAdditionalFiles
] ]
handleUpload :: FileField -> Maybe Text -> ConduitT File FileReference (YesodDB UniWorX) () handleUpload :: FileField -> Maybe Text -> ConduitT (File Handler) FileReference (YesodDB UniWorX) ()
handleUpload FileField{fieldMaxFileSize} mIdent handleUpload FileField{fieldMaxFileSize} mIdent
= C.filter (\File{..} -> maybe (const True) (>) fieldMaxFileSize $ maybe 0 (fromIntegral . olength) fileContent) = C.map (transFile liftHandler)
.| sinkFiles .| C.mapMaybeM (\f@File{..} -> maybeT (return $ Just f) $ do
.| C.mapM mkSessionFile maxSize <- fromIntegral <$> hoistMaybe fieldMaxFileSize
fc <- hoistMaybe fileContent
let peekNE n = do
str <- C.takeE n .| C.fold
leftover str
yield str
(unsealConduitT -> fc', size) <- lift $ fc $$+ peekNE (succ maxSize) .| C.lengthE
return . guardOn (size <= maxSize) $ f { fileContent = Just fc' }
)
.| sinkFiles
.| C.mapM mkSessionFile
where where
mkSessionFile fRef@FileReference{..} = fRef <$ do mkSessionFile fRef@FileReference{..} = fRef <$ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
@ -924,7 +937,7 @@ genericFileField mkOpts = Field{..}
doUnpack doUnpack
| fieldOptionForce fieldUnpackZips = fieldOptionDefault fieldUnpackZips | fieldOptionForce fieldUnpackZips = fieldOptionDefault fieldUnpackZips
| otherwise = unpackZips `elem` vals | otherwise = unpackZips `elem` vals
handleFile :: FileInfo -> ConduitT () File Handler () handleFile :: FileInfo -> ConduitT () (File Handler) Handler ()
handleFile handleFile
| doUnpack = receiveFiles | doUnpack = receiveFiles
| otherwise = yieldM . acceptFile | otherwise = yieldM . acceptFile

View File

@ -12,9 +12,7 @@ import Handler.Utils.Files
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.ByteString.Lazy as LBS import qualified Data.Conduit.Combinators as C
import qualified Data.Conduit.List as C
import qualified Text.Pandoc as P import qualified Text.Pandoc as P
@ -72,12 +70,13 @@ addFileDB :: ( MonadMail m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => FileReference -> m (Maybe MailObjectId) ) => FileReference -> m (Maybe MailObjectId)
addFileDB fRef = runMaybeT $ do addFileDB fRef = runMaybeT $ do
File{fileTitle = pack . takeBaseName -> fileName, fileContent = Just fileContent} <- lift . liftHandler . runDB $ sourceFile fRef File{fileTitle = pack . takeBaseName -> fileName, fileContent = Just fileContent'} <- return $ sourceFile fRef
fileContent <- liftHandler . runDB . runConduit $ fileContent' .| C.sinkLazy
lift . addPart $ do lift . addPart $ do
_partType .= decodeUtf8 (mimeLookup fileName) _partType .= decodeUtf8 (mimeLookup fileName)
_partEncoding .= Base64 _partEncoding .= Base64
_partDisposition .= AttachmentDisposition fileName _partDisposition .= AttachmentDisposition fileName
_partContent .= PartContent (LBS.fromStrict fileContent) _partContent .= PartContent fileContent
setMailObjectIdPseudorandom (fileName, fileContent) :: StateT Part (HandlerFor UniWorX) MailObjectId setMailObjectIdPseudorandom (fileName, fileContent) :: StateT Part (HandlerFor UniWorX) MailObjectId

View File

@ -16,11 +16,9 @@ import Handler.Utils.DateTime (getDateTimeFormatter)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.ByteString.Lazy as Lazy.ByteString
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.Combinators as C
import Handler.Utils.Rating.Format import Handler.Utils.Rating.Format
@ -91,15 +89,16 @@ extensionRating = "txt"
ratingFile :: ( MonadHandler m ratingFile :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, Monad m'
) )
=> CryptoFileNameSubmission -> Rating -> m File => CryptoFileNameSubmission -> Rating -> m (File m')
ratingFile cID rating@Rating{ ratingValues = Rating'{..} } = do ratingFile cID rating@Rating{ ratingValues = Rating'{..} } = do
mr'@(MsgRenderer mr) <- getMsgRenderer mr'@(MsgRenderer mr) <- getMsgRenderer
dtFmt <- getDateTimeFormatter dtFmt <- getDateTimeFormatter
fileModified <- maybe (liftIO getCurrentTime) return ratingTime fileModified <- maybe (liftIO getCurrentTime) return ratingTime
let let
fileTitle = ensureExtension extensionRating . unpack . mr $ MsgRatingFileTitle cID fileTitle = ensureExtension extensionRating . unpack . mr $ MsgRatingFileTitle cID
fileContent = Just . Lazy.ByteString.toStrict $ formatRating mr' dtFmt cID rating fileContent = Just . C.sourceLazy $ formatRating mr' dtFmt cID rating
return File{..} return File{..}
type SubmissionContent = Either FileReference (SubmissionId, Rating') type SubmissionContent = Either FileReference (SubmissionId, Rating')
@ -107,13 +106,12 @@ type SubmissionContent = Either FileReference (SubmissionId, Rating')
extractRatings :: ( MonadHandler m extractRatings :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => ConduitT FileReference SubmissionContent m () ) => ConduitT FileReference SubmissionContent m ()
extractRatings = Conduit.mapM $ \fRef@FileReference{..} -> liftHandler $ do extractRatings = C.mapM $ \fRef@FileReference{..} -> liftHandler $ do
msId <- isRatingFile fileReferenceTitle msId <- isRatingFile fileReferenceTitle
if if
| Just sId <- msId | Just sId <- msId
, isJust fileReferenceContent -> do , isJust fileReferenceContent -> do
f <- runDB $ sourceFile fRef (rating, cID) <- handle (throwM . RatingFileException fileReferenceTitle) . runDB . parseRating $ sourceFile fRef
(rating, cID) <- handle (throwM . RatingFileException fileReferenceTitle) $ parseRating f
sId' <- traverse decrypt cID sId' <- traverse decrypt cID
unless (maybe (const True) (==) sId' sId) $ unless (maybe (const True) (==) sId' sId) $
throwM $ RatingFileException fileReferenceTitle RatingSubmissionIDIncorrect throwM $ RatingFileException fileReferenceTitle RatingSubmissionIDIncorrect

View File

@ -35,6 +35,8 @@ import qualified System.FilePath.Cryptographic as Explicit
import Control.Exception (ErrorCall(..)) import Control.Exception (ErrorCall(..))
import qualified Data.Conduit.Combinators as C
data PrettifyState data PrettifyState
= PrettifyInitial = PrettifyInitial
@ -195,8 +197,9 @@ instance ns ~ CryptoIDNamespace (CI FilePath) SubmissionId => YAML.FromYAML (May
) )
parseRating :: MonadCatch m => File -> m (Rating', Maybe CryptoFileNameSubmission) parseRating :: MonadCatch m => File m -> m (Rating', Maybe CryptoFileNameSubmission)
parseRating f@File{ fileContent = Just (fromStrict -> input), .. } = handle onFailure . handle (throwM . RatingParseException) . handleIf isYAMLUnicodeError (\(ErrorCall msg) -> throwM $ RatingYAMLNotUnicode msg) $ do parseRating f@File{ fileContent = Just input', .. } = handle onFailure . handle (throwM . RatingParseException) . handleIf isYAMLUnicodeError (\(ErrorCall msg) -> throwM $ RatingYAMLNotUnicode msg) $ do
input <- runConduit $ input' .| C.sinkLazy
let evStream = YAML.Event.parseEvents input let evStream = YAML.Event.parseEvents input
delimitDocument = do delimitDocument = do
ev <- maybe (throwM RatingYAMLStreamTerminatedUnexpectedly) return =<< await ev <- maybe (throwM RatingYAMLStreamTerminatedUnexpectedly) return =<< await

View File

@ -16,6 +16,8 @@ import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Conduit.Combinators as C
import Text.Read (readEither) import Text.Read (readEither)
@ -55,9 +57,9 @@ formatRating cID Rating{ ratingValues = Rating'{..}, ..} = let
] ]
in Lazy.Text.encodeUtf8 . (<> "\n") $ displayT doc in Lazy.Text.encodeUtf8 . (<> "\n") $ displayT doc
parseRating :: MonadCatch m => File -> m Rating' parseRating :: MonadCatch m => File m -> m Rating'
parseRating File{ fileContent = Just input, .. } = handle (throwM . RatingParseLegacyException) $ do parseRating File{ fileContent = Just input, .. } = handle (throwM . RatingParseLegacyException) $ do
inputText <- either (throwM . RatingNotUnicode) return $ Text.decodeUtf8' input inputText <- either (throwM . RatingNotUnicode) return . Text.decodeUtf8' =<< runConduit (input .| C.fold)
let let
(headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText (headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText
(reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines' (reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines'

View File

@ -256,7 +256,7 @@ planSubmissions sid restriction = do
maximumsBy f xs = flip Set.filter xs $ \x -> maybe True (((==) `on` f) x . maximumBy (comparing f)) $ fromNullable xs maximumsBy f xs = flip Set.filter xs $ \x -> maybe True (((==) `on` f) x . maximumBy (comparing f)) $ fromNullable xs
submissionFileSource :: SubmissionId -> ConduitT () File (YesodDB UniWorX) () submissionFileSource :: SubmissionId -> ConduitT () DBFile (YesodDB UniWorX) ()
submissionFileSource subId = E.selectSource (E.from $ submissionFileQuery subId) submissionFileSource subId = E.selectSource (E.from $ submissionFileQuery subId)
.| C.map entityVal .| C.map entityVal
.| sourceFiles' .| sourceFiles'
@ -319,7 +319,7 @@ submissionMultiArchive anonymous (Set.toList -> ids) = do
setContentDisposition' $ Just ((addExtension `on` unpack) (mr archiveName) extensionZip) setContentDisposition' $ Just ((addExtension `on` unpack) (mr archiveName) extensionZip)
respondSource typeZip . (<* lift cleanup) . transPipe (runDBRunner dbrunner) $ do respondSource typeZip . (<* lift cleanup) . transPipe (runDBRunner dbrunner) $ do
let let
fileEntitySource' :: (Rating, Entity Submission, Maybe UTCTime, (SheetName,CourseShorthand,SchoolId,TermId,Bool)) -> ConduitT () File (YesodDB UniWorX) () fileEntitySource' :: (Rating, Entity Submission, Maybe UTCTime, (SheetName,CourseShorthand,SchoolId,TermId,Bool)) -> ConduitT () DBFile (YesodDB UniWorX) ()
fileEntitySource' (rating, Entity submissionID Submission{}, subTime, (shn,csh,ssh,tid,sheetAnonymous)) = do fileEntitySource' (rating, Entity submissionID Submission{}, subTime, (shn,csh,ssh,tid,sheetAnonymous)) = do
cID <- encrypt submissionID cID <- encrypt submissionID

View File

@ -13,17 +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 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 System.FilePath
import Data.Time.LocalTime (localTimeToUTC, utcToLocalTime)
import Data.List (dropWhileEnd) import Data.List (dropWhileEnd)
@ -38,6 +35,10 @@ import Data.Encoding ( decodeStrictByteStringExplicit
import Data.Encoding.CP437 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.State.Strict (evalStateT)
import qualified Control.Monad.State.Class as State
typeZip :: ContentType typeZip :: ContentType
typeZip = "application/zip" typeZip = "application/zip"
@ -53,94 +54,157 @@ instance Default ZipInfo where
} }
consumeZip :: forall b m. data ConsumeZipException
( MonadThrow b = ConsumeZipUnZipException SomeException
, MonadThrow m | ConsumeZipUnexpectedContent
, MonadBase b m deriving (Show, Generic, Typeable)
, PrimMonad b deriving anyclass (Exception)
)
=> 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 consumeZip :: forall m m'.
, MonadThrow m ( MonadThrow m
, MonadBase b m , PrimMonad m
, PrimMonad b , 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 => ZipInfo
-> ConduitT File ByteString m () -> ConduitT (File m) ByteString m ()
produceZip info = C.map toZipData .| transPipe liftBase (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 :: File -> (ZipEntry, ZipData b) -- toZipData :: forall v. File m -> ConduitT v (ZipEntry, ZipData m) m ()
toZipData f@File{..} = -- toZipData f
let zData = maybe mempty (ZipDataByteString . Lazy.ByteString.fromStrict) fileContent -- | Just fc <- fileContent f = do
zEntry = (toZipEntry f){ zipEntrySize = fromIntegral . ByteString.length <$> fileContent } -- outpChunk <- newEmptyTMVarIO
in (zEntry, zData) -- 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)
toZipEntry :: File -> ZipEntry toZipData :: File m -> (ZipEntry, ZipData m)
toZipData f@File{..}
= (toZipEntry f, maybe mempty ZipDataSource fileContent)
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
modifyFileTitle :: Monad m => (FilePath -> FilePath) -> ConduitT File File m () modifyFileTitle :: Monad m => (FilePath -> FilePath) -> ConduitT (File m') (File m') m ()
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) => FileInfo -> ConduitT () File 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"
fileSource fInfo .| void consumeZip void . consumeZip $ fileSource fInfo
| otherwise = do | otherwise = do
$logDebugS "sourceFiles" [st|Not unpacking file of type #{decodeUtf8 mimeType}|] $logDebugS "sourceFiles" [st|Not unpacking file of type #{decodeUtf8 mimeType}|]
yieldM $ acceptFile fInfo yieldM $ acceptFile fInfo
where where
mimeType = mimeLookup $ fileName fInfo 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 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

@ -4,6 +4,7 @@ module Import
import Foundation as Import import Foundation as Import
import Import.NoFoundation as Import import Import.NoFoundation as Import
import Model.Migration as Import
import Utils.SystemMessage as Import import Utils.SystemMessage as Import
import Utils.Metrics as Import import Utils.Metrics as Import

View File

@ -4,7 +4,6 @@ module Import.NoFoundation
import Import.NoModel as Import import Import.NoModel as Import
import Model as Import import Model as Import
import Model.Migration as Import
import Model.Rating as Import import Model.Rating as Import
import Model.Submission as Import import Model.Submission as Import
import Model.Tokens as Import import Model.Tokens as Import

View File

@ -128,6 +128,8 @@ import Data.Proxy as Import (Proxy(..))
import Data.List.PointedList as Import (PointedList) import Data.List.PointedList as Import (PointedList)
import Language.Haskell.TH.Syntax as Import (Lift(liftTyped))
import Language.Haskell.TH.Instances as Import () import Language.Haskell.TH.Instances as Import ()
import Data.NonNull.Instances as Import () import Data.NonNull.Instances as Import ()
import Data.Monoid.Instances as Import () import Data.Monoid.Instances as Import ()

View File

@ -5,7 +5,7 @@ module Jobs
, stopJobCtl , stopJobCtl
) where ) where
import Import import Import hiding (StateT)
import Jobs.Types as Types hiding (JobCtl(JobCtlQueue)) import Jobs.Types as Types hiding (JobCtl(JobCtlQueue))
import Jobs.Queue import Jobs.Queue
import Jobs.Crontab import Jobs.Crontab
@ -30,6 +30,7 @@ import qualified Data.Map.Strict as Map
import Data.Map.Strict ((!)) import Data.Map.Strict ((!))
import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST, evalRWST) import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST, evalRWST)
import Control.Monad.Trans.State.Strict (StateT, evalStateT)
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Writer.Class (MonadWriter(..)) import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Trans.Cont (ContT(..), callCC) import Control.Monad.Trans.Cont (ContT(..), callCC)
@ -99,6 +100,7 @@ handleJobs foundation@UniWorX{..}
jobConfirm <- liftIO $ newTVarIO HashMap.empty jobConfirm <- liftIO $ newTVarIO HashMap.empty
jobShutdown <- liftIO newEmptyTMVarIO jobShutdown <- liftIO newEmptyTMVarIO
jobCurrentCrontab <- liftIO $ newTVarIO Nothing jobCurrentCrontab <- liftIO $ newTVarIO Nothing
jobHeldLocks <- liftIO $ newTVarIO Set.empty
atomically $ putTMVar appJobState JobState atomically $ putTMVar appJobState JobState
{ jobContext = JobContext{..} { jobContext = JobContext{..}
, .. , ..
@ -114,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 $
@ -414,13 +417,14 @@ handleJobs' wNum = C.mapM_ $ \jctl -> withJobWorkerState wNum JobWorkerBusy $ do
handleCmd JobCtlTest = return () handleCmd JobCtlTest = return ()
handleCmd JobCtlFlush = void . lift . runDB . runConduit $ selectKeys [] [ Asc QueuedJobCreationTime ] .| C.mapM_ (\j -> lift $ runReaderT (writeJobCtl $ JobCtlPerform j) =<< getYesod) handleCmd JobCtlFlush = void . lift . runDB . runConduit $ selectKeys [] [ Asc QueuedJobCreationTime ] .| C.mapM_ (\j -> lift $ runReaderT (writeJobCtl $ JobCtlPerform j) =<< getYesod)
handleCmd (JobCtlQueue job) = lift $ queueJob' job handleCmd (JobCtlQueue job) = lift $ queueJob' job
handleCmd (JobCtlPerform jId) = lift . handle handleQueueException . jLocked jId $ \j@QueuedJob{..} -> do handleCmd (JobCtlPerform jId) = handle handleQueueException . jLocked jId $ \(Entity _ j@QueuedJob{..}) -> lift $ do
content <- case fromJSON queuedJobContent of content <- case fromJSON queuedJobContent of
Aeson.Success c -> return c Aeson.Success c -> return c
Aeson.Error t -> do Aeson.Error t -> do
$logErrorS logIdent $ "Aeson decoding error: " <> pack t $logErrorS logIdent $ "Aeson decoding error: " <> pack t
throwM $ JInvalid jId j throwM $ JInvalid jId j
$logInfoS logIdent $ tshow content
$logDebugS logIdent . LT.toStrict . decodeUtf8 $ Aeson.encode content $logDebugS logIdent . LT.toStrict . decodeUtf8 $ Aeson.encode content
instanceID' <- getsYesod $ view instanceID instanceID' <- getsYesod $ view instanceID
@ -466,40 +470,45 @@ handleJobs' wNum = C.mapM_ $ \jctl -> withJobWorkerState wNum JobWorkerBusy $ do
. Set.filter (((/=) `on` classifyHealthReport) newReport . snd) . Set.filter (((/=) `on` classifyHealthReport) newReport . snd)
atomically . modifyTVar' hrStorage $ force . updateReports atomically . modifyTVar' hrStorage $ force . updateReports
jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a jLocked :: QueuedJobId -> (Entity QueuedJob -> ReaderT JobContext Handler a) -> ReaderT JobContext Handler a
jLocked jId act = do jLocked jId act = flip evalStateT False $ do
hasLock <- liftIO $ newTVarIO False
let let
lock = runDB . setSerializable $ do lock :: StateT Bool (ReaderT JobContext Handler) (Entity QueuedJob)
qj@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId lock = hoist (hoist $ runDB . setSerializable) $ do
qj@QueuedJob{..} <- lift . lift $ maybe (throwM $ JNonexistant jId) return =<< get jId
instanceID' <- getsYesod $ view instanceID instanceID' <- getsYesod $ view instanceID
threshold <- getsYesod $ view _appJobStaleThreshold threshold <- getsYesod $ view _appJobStaleThreshold
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
heldLocks <- asks jobHeldLocks
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
if if
| lockInstance == instanceID' | lockInstance == instanceID'
, diffUTCTime now lockTime >= threshold , diffUTCTime now lockTime >= threshold
, not isHeld
-> return True -> return True
| otherwise | otherwise
-> throwM $ JLocked jId lockInstance lockTime -> throwM $ JLocked jId lockInstance lockTime
when hadStale . when hadStale .
$logWarnS "Jobs" $ "Ignored stale lock: " <> tshow (Entity jId qj) $logWarnS "Jobs" $ "Ignored stale lock: " <> tshow (Entity jId qj)
val <- updateGet jId [ QueuedJobLockInstance =. Just instanceID' State.put True
, QueuedJobLockTime =. Just now val <- lift . lift $ updateGet jId [ QueuedJobLockInstance =. Just instanceID'
] , QueuedJobLockTime =. Just now
liftIO . atomically $ writeTVar hasLock True ]
return val atomically . modifyTVar' heldLocks $ Set.insert jId
return $ Entity jId val
unlock = whenM (readTVarIO hasLock) $ unlock :: Entity QueuedJob -> StateT Bool (ReaderT JobContext Handler) ()
runDB . setSerializable $ unlock (Entity jId' _) = whenM State.get $ do
update jId [ QueuedJobLockInstance =. Nothing atomically . flip modifyTVar' (Set.delete jId') =<< asks jobHeldLocks
, QueuedJobLockTime =. Nothing lift . lift . runDB . setSerializable $
] update jId' [ QueuedJobLockInstance =. Nothing
, QueuedJobLockTime =. Nothing
]
bracket lock (const unlock) act bracket lock unlock $ lift . act
pruneLastExecs :: Crontab JobCtl -> DB () pruneLastExecs :: Crontab JobCtl -> DB ()

View File

@ -40,22 +40,14 @@ determineCrontab = execWriterT $ do
} }
Nothing -> return () Nothing -> return ()
tell $ HashMap.singleton whenIsJust appJobCronInterval $ \interval ->
JobCtlDetermineCrontab
Cron
{ cronInitial = CronAsap
, cronRepeat = CronRepeatScheduled CronAsap
, cronRateLimit = appJobCronInterval
, cronNotAfter = Right CronNotScheduled
}
whenIsJust appPruneUnreferencedFiles $ \pInterval ->
tell $ HashMap.singleton tell $ HashMap.singleton
(JobCtlQueue JobPruneUnreferencedFiles) JobCtlDetermineCrontab
Cron Cron
{ cronInitial = CronAsap { cronInitial = CronAsap
, cronRepeat = CronRepeatScheduled CronAsap , cronRepeat = CronRepeatScheduled CronAsap
, cronRateLimit = pInterval , cronRateLimit = interval
, cronNotAfter = Right CronNotScheduled , 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]
@ -98,6 +90,15 @@ determineCrontab = execWriterT $ do
, cronRateLimit = iInterval , cronRateLimit = iInterval
, cronNotAfter = Right CronNotScheduled , cronNotAfter = Right CronNotScheduled
} }
whenIsJust appRechunkFiles $ \rInterval ->
tell $ HashMap.singleton
(JobCtlQueue JobRechunkFiles)
Cron
{ cronInitial = CronAsap
, cronRepeat = CronRepeatScheduled CronAsap
, cronRateLimit = rInterval
, cronNotAfter = Right CronNotScheduled
}
tell . flip foldMap universeF $ \kind -> tell . flip foldMap universeF $ \kind ->
case appHealthCheckInterval kind of case appHealthCheckInterval kind of
@ -138,33 +139,31 @@ determineCrontab = execWriterT $ do
, cronNotAfter = Right CronNotScheduled , cronNotAfter = Right CronNotScheduled
} }
let
getNextIntervals within interval cInterval = do
now <- liftIO getPOSIXTime
return $ do
let
epochInterval = within / 2
(currEpoch, epochNow) = now `divMod'` epochInterval
currInterval = epochNow `div'` interval
numIntervals = floor $ epochInterval / interval
n = ceiling $ 4 * cInterval / interval
i <- [ negate (ceiling $ n % 2) .. ceiling $ n % 2 ]
let
((+ currEpoch) -> nextEpoch, nextInterval) = (currInterval + i) `divMod` numIntervals
nextIntervalTime
= posixSecondsToUTCTime $ fromInteger nextEpoch * epochInterval + fromInteger nextInterval * interval
return (nextEpoch, nextInterval, nextIntervalTime, numIntervals)
if if
| is _Just appLdapConf | is _Just appLdapConf
, is _Just appLdapConf
, Just syncWithin <- appSynchroniseLdapUsersWithin , Just syncWithin <- appSynchroniseLdapUsersWithin
, Just cInterval <- appJobCronInterval
-> do -> do
now <- liftIO getPOSIXTime nextIntervals <- getNextIntervals syncWithin appSynchroniseLdapUsersInterval cInterval
let
epochInterval = syncWithin / 2
interval = appSynchroniseLdapUsersInterval
(ldapEpoch, epochNow) = now `divMod'` epochInterval forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do
ldapInterval = epochNow `div'` interval
numIntervals = floor $ epochInterval / interval
nextIntervals = do
let
n = ceiling $ 4 * appJobCronInterval / appSynchroniseLdapUsersInterval
i <- [negate (ceiling $ n % 2) .. ceiling $ n % 2]
let
((+ ldapEpoch) -> nextEpoch, nextInterval) = (ldapInterval + i) `divMod` numIntervals
nextIntervalTime
= posixSecondsToUTCTime $ fromInteger nextEpoch * epochInterval + fromInteger nextInterval * interval
return (nextEpoch, nextInterval, nextIntervalTime)
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime) -> do
$logDebugS "SynchroniseLdap" [st|currentTime: #{tshow ldapEpoch}.#{tshow epochNow}; upcomingSync: #{tshow nextEpoch}.#{tshow (fromInteger nextInterval * interval)}; upcomingData: #{tshow (numIntervals, nextEpoch, nextInterval)}|]
tell $ HashMap.singleton tell $ HashMap.singleton
(JobCtlQueue JobSynchroniseLdap (JobCtlQueue JobSynchroniseLdap
{ jEpoch = fromInteger nextEpoch { jEpoch = fromInteger nextEpoch
@ -180,6 +179,22 @@ determineCrontab = execWriterT $ do
| otherwise | otherwise
-> return () -> return ()
whenIsJust ((,) <$> appPruneUnreferencedFilesWithin <*> appJobCronInterval) $ \(within, cInterval) -> do
nextIntervals <- getNextIntervals within appPruneUnreferencedFilesInterval cInterval
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do
tell $ HashMap.singleton
(JobCtlQueue JobPruneUnreferencedFiles
{ jEpoch = fromInteger nextEpoch
, jNumIterations = fromInteger numIntervals
, jIteration = fromInteger nextInterval
}
)
Cron
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ nextIntervalTime
, cronRepeat = CronRepeatNever
, cronRateLimit = appPruneUnreferencedFilesInterval
, cronNotAfter = Left within
}
let let
sheetJobs (Entity nSheet Sheet{..}) = do sheetJobs (Entity nSheet Sheet{..}) = do

View File

@ -1,27 +1,36 @@
module Jobs.Handler.Files module Jobs.Handler.Files
( dispatchJobPruneSessionFiles ( dispatchJobPruneSessionFiles
, dispatchJobPruneUnreferencedFiles , dispatchJobPruneUnreferencedFiles
, dispatchJobInjectFiles , dispatchJobInjectFiles, dispatchJobRechunkFiles
) where ) where
import Import hiding (matching) import Import hiding (matching, maximumBy, init)
import Database.Persist.Sql (deleteWhereCount) import Database.Persist.Sql (deleteWhereCount)
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.PostgreSQL as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Database.Esqueleto.Internal.Sql as E (unsafeSqlCastAs)
import qualified Data.Conduit.Combinators as C import qualified Data.Conduit.Combinators as C
import qualified Data.Conduit.List as C (mapMaybe) import qualified Data.Conduit.List as C (mapMaybe, unfoldM)
import Handler.Utils.Minio import Handler.Utils.Minio
import qualified Network.Minio as Minio import qualified Network.Minio as Minio
import qualified Crypto.Hash as Crypto import Crypto.Hash (hashDigestSize, digestFromByteString)
import qualified Data.ByteString.Base64.URL as Base64
import Control.Monad.Memo (startEvalMemoT, memo) import Data.List ((!!), unfoldr, maximumBy, init, genericLength)
import qualified Data.ByteString as ByteString
import Data.Bits (Bits(shiftR))
import qualified Data.Map.Strict as Map
import Control.Monad.Random.Lazy
import System.Random.Shuffle (shuffleM)
import System.IO.Unsafe
import Handler.Utils.Files (sourceFileDB)
dispatchJobPruneSessionFiles :: JobHandler UniWorX dispatchJobPruneSessionFiles :: JobHandler UniWorX
@ -44,72 +53,190 @@ fileReferences (E.just -> fHash)
, E.from $ \subFile -> E.where_ $ subFile E.^. SubmissionFileContent E.==. fHash , E.from $ \subFile -> E.where_ $ subFile E.^. SubmissionFileContent E.==. fHash
, E.from $ \sessFile -> E.where_ $ sessFile E.^. SessionFileContent E.==. fHash , E.from $ \sessFile -> E.where_ $ sessFile E.^. SessionFileContent E.==. fHash
, E.from $ \lock -> E.where_ $ E.just (lock E.^. FileLockContent) E.==. fHash , E.from $ \lock -> E.where_ $ E.just (lock E.^. FileLockContent) E.==. fHash
, E.from $ \chunkLock -> E.where_ . E.exists . E.from $ \fileContentEntry ->
E.where_ $ E.just (fileContentEntry E.^. FileContentEntryHash) E.==. fHash
E.&&. chunkLock E.^. FileChunkLockHash E.==. E.subSelectForeign fileContentEntry FileContentEntryChunkHash (E.^. FileContentChunkHash)
] ]
{-# NOINLINE pruneUnreferencedFilesIntervalsCache #-}
pruneUnreferencedFilesIntervalsCache :: TVar (Map Natural [(Maybe FileContentChunkReference, Maybe FileContentChunkReference)])
pruneUnreferencedFilesIntervalsCache = unsafePerformIO $ newTVarIO Map.empty
dispatchJobPruneUnreferencedFiles :: JobHandler UniWorX dispatchJobPruneUnreferencedFiles :: Natural -> Natural -> Natural -> JobHandler UniWorX
dispatchJobPruneUnreferencedFiles = JobHandlerAtomic . hoist lift $ do dispatchJobPruneUnreferencedFiles numIterations epoch iteration = JobHandlerAtomic . hoist lift $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
interval <- fmap (fmap $ max 0) . getsYesod $ view _appPruneUnreferencedFiles interval <- getsYesod $ view _appPruneUnreferencedFilesInterval
keep <- fmap (max 0) . getsYesod $ view _appKeepUnreferencedFiles keep <- fmap (max 0) . getsYesod $ view _appKeepUnreferencedFiles
E.update $ \fileContent -> do let
let isReferenced = E.any E.exists . fileReferences $ fileContent E.^. FileContentHash chunkHashBytes :: forall h.
now' = E.unsafeSqlCastAs "TIMESTAMP WITH TIME ZONE" $ E.val now ( Unwrapped FileContentChunkReference ~ Digest h )
shouldBe = E.bool (E.just . E.maybe now' (E.min now') $ fileContent E.^. FileContentUnreferencedSince) E.nothing isReferenced => Integer
E.set fileContent [ FileContentUnreferencedSince E.=. shouldBe ] chunkHashBytes = fromIntegral (hashDigestSize (error "hashDigestSize inspected argument" :: h))
chunkHashBits = chunkHashBytes * 8
base :: Integer
base = 2 ^ chunkHashBits
intervals :: [Integer]
-- | Exclusive upper bounds
intervals
| numIterations <= 0 = pure base
| otherwise = go protoIntervals ^.. folded . _1
where
go [] = []
go ints
| maximumOf (folded . _1) ints == Just base = ints
| otherwise = go $ lts ++ over _1 succ (over _2 (subtract $ toInteger numIterations) closest) : map (over _1 succ) gts
where
closest = maximumBy (comparing $ view _2) ints
(lts, geqs) = partition (((>) `on` view _1) closest) ints
gts = filter (((<) `on` view _1) closest) geqs
-- | Exclusive upper bounds
protoIntervals :: [(Integer, Integer)]
protoIntervals = [ over _1 (i *) $ base `divMod` toInteger numIterations
| i <- [1 .. toInteger numIterations]
]
intervalsDgsts' = zipWith (curry . over both $ toDigest <=< assertM' (> 0)) (0 : init intervals) intervals
toDigest :: Integer -> Maybe FileContentChunkReference
toDigest = fmap (review _Wrapped) . digestFromByteString . pad . ByteString.pack . reverse . unfoldr step
where step i
| i <= 0 = Nothing
| otherwise = Just (fromIntegral i, i `shiftR` 8)
pad bs
| toInteger (ByteString.length bs) >= chunkHashBytes = bs
| otherwise = pad $ ByteString.cons 0 bs
intervalsDgsts <- atomically $ do
cachedDgsts <- readTVar pruneUnreferencedFilesIntervalsCache
case Map.lookup numIterations cachedDgsts of
Just c -> return c
Nothing -> do
modifyTVar' pruneUnreferencedFilesIntervalsCache $ force . Map.insert numIterations intervalsDgsts'
return intervalsDgsts'
let let
getCandidates = E.selectSource . E.from $ \fileContent -> do permIntervalsDgsts = shuffleM intervalsDgsts `evalRand` mkStdGen (hash epoch)
E.where_ . E.maybe E.false (E.<. E.val (addUTCTime (-keep) now)) $ fileContent E.^. FileContentUnreferencedSince
return ( fileContent E.^. FileContentHash (minBoundDgst, maxBoundDgst) = permIntervalsDgsts !! fromIntegral (toInteger iteration `mod` genericLength permIntervalsDgsts)
, E.length_ $ fileContent E.^. FileContentContent chunkIdFilter :: E.SqlExpr (E.Value FileContentChunkReference) -> E.SqlExpr (E.Value Bool)
chunkIdFilter cRef = E.and $ catMaybes
[ minBoundDgst <&> \b -> cRef E.>=. E.val b
, maxBoundDgst <&> \b -> cRef E.<. E.val b
]
$logDebugS "PruneUnreferencedFiles" . tshow $ (minBoundDgst, maxBoundDgst)
E.insertSelectWithConflict
(UniqueFileContentChunkUnreferenced $ error "insertSelectWithConflict inspected constraint")
(E.from $ \fileContentChunk -> do
E.where_ . E.not_ . E.subSelectOr . E.from $ \fileContentEntry -> do
E.where_ $ fileContentEntry E.^. FileContentEntryChunkHash E.==. fileContentChunk E.^. FileContentChunkId
return . E.any E.exists . fileReferences $ fileContentEntry E.^. FileContentEntryHash
E.where_ . chunkIdFilter $ fileContentChunk E.^. FileContentChunkHash
return $ FileContentChunkUnreferenced E.<# (fileContentChunk E.^. FileContentChunkId) E.<&> E.val now
)
(\current excluded ->
[ FileContentChunkUnreferencedSince E.=. E.min (current E.^. FileContentChunkUnreferencedSince) (excluded E.^. FileContentChunkUnreferencedSince) ]
)
E.delete . E.from $ \fileContentChunkUnreferenced -> do
E.where_ . E.subSelectOr . E.from $ \fileContentEntry -> do
E.where_ $ fileContentEntry E.^. FileContentEntryChunkHash E.==. fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedHash
return . E.any E.exists . fileReferences $ fileContentEntry E.^. FileContentEntryHash
E.where_ . chunkIdFilter $ E.subSelectForeign fileContentChunkUnreferenced FileContentChunkUnreferencedHash (E.^. FileContentChunkHash)
let
getEntryCandidates = E.selectSource . E.from $ \fileContentEntry -> do
let unreferencedSince = E.subSelectMaybe . E.from $ \(fileContentEntry' `E.InnerJoin` fileContentChunkUnreferenced) -> do
E.on $ fileContentEntry' E.^. FileContentEntryChunkHash E.==. fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedHash
E.where_ $ fileContentEntry' E.^. FileContentEntryHash E.==. fileContentEntry E.^. FileContentEntryHash
E.where_ . chunkIdFilter $ E.subSelectForeign fileContentChunkUnreferenced FileContentChunkUnreferencedHash (E.^. FileContentChunkHash)
return . E.max_ $ fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedSince
E.where_ $ E.maybe E.false (E.<. E.val (addUTCTime (-keep) now)) unreferencedSince
E.groupBy $ fileContentEntry E.^. FileContentEntryHash
E.orderBy [ E.asc $ fileContentEntry E.^. FileContentEntryHash ]
return $ fileContentEntry E.^. FileContentEntryHash
deleteEntry :: _ -> DB (Sum Natural)
deleteEntry (E.Value fRef) =
bool 0 1 . (> 0) <$> deleteWhereCount [FileContentEntryHash ==. fRef]
Sum deletedEntries <- runConduit $
getEntryCandidates
.| takeWhileTime (interval / 3)
.| C.mapM deleteEntry
.| C.fold
when (deletedEntries > 0) $
$logInfoS "PruneUnreferencedFiles" [st|Deleted #{tshow deletedEntries} long-unreferenced files|]
let
getChunkCandidates = E.selectSource . E.from $ \fileContentChunkUnreferenced -> do
E.where_ $ fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedSince E.<. E.val (addUTCTime (-keep) now)
E.where_ . E.not_ . E.exists . E.from $ \fileContentEntry ->
E.where_ $ fileContentEntry E.^. FileContentEntryChunkHash E.==. fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedHash
E.where_ . chunkIdFilter $ E.subSelectForeign fileContentChunkUnreferenced FileContentChunkUnreferencedHash (E.^. FileContentChunkHash)
return ( fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedHash
, E.subSelectForeign fileContentChunkUnreferenced FileContentChunkUnreferencedHash $ E.length_ . (E.^. FileContentChunkContent)
) )
Sum deleted <- runConduit $ deleteChunk :: _ -> DB (Sum Natural, Sum Word64)
getCandidates deleteChunk (E.Value cRef, E.Value size) = do
.| maybe (C.map id) (takeWhileTime . (/ 2)) interval deleteWhere [ FileContentChunkUnreferencedHash ==. cRef ]
(, Sum size) . fromIntegral <$> deleteWhereCount [FileContentChunkHash ==. unFileContentChunkKey cRef]
(Sum deletedChunks, Sum deletedChunkSize) <- runConduit $
getChunkCandidates
.| takeWhileTime (interval / 3)
.| persistentTokenBucketTakeC' TokenBucketPruneFiles (view $ _2 . _Value :: _ -> Word64) .| persistentTokenBucketTakeC' TokenBucketPruneFiles (view $ _2 . _Value :: _ -> Word64)
.| C.map (view $ _1 . _Value) .| C.mapM deleteChunk
.| C.mapM (\fRef -> Sum <$> deleteWhereCount [FileContentHash ==. fRef])
.| C.fold .| C.fold
when (deleted > 0) $
$logInfoS "PruneUnreferencedFiles" [st|Deleted #{deleted} long-unreferenced files|] when (deletedChunks > 0) $
$logInfoS "PruneUnreferencedFiles" [st|Deleted #{tshow deletedChunks} chunks (#{tshow deletedChunkSize} bytes)|]
dispatchJobInjectFiles :: JobHandler UniWorX dispatchJobInjectFiles :: JobHandler UniWorX
dispatchJobInjectFiles = JobHandlerException . maybeT (return ()) $ do dispatchJobInjectFiles = JobHandlerException . maybeT (return ()) $ do
uploadBucket <- getsYesod $ view _appUploadCacheBucket uploadBucket <- getsYesod $ view _appUploadCacheBucket
interval <- getsYesod $ view _appInjectFiles interval <- getsYesod $ view _appInjectFiles
now <- liftIO getCurrentTime
let let
extractReference (Minio.ListItemObject oi) extractReference (Minio.ListItemObject oi) = (oi, ) <$> Minio.oiObject oi ^? minioFileReference
| Right bs <- Base64.decodeUnpadded . encodeUtf8 $ Minio.oiObject oi
, Just fRef <- Crypto.digestFromByteString bs
= Just (oi, fRef)
extractReference _ = Nothing extractReference _ = Nothing
injectOrDelete :: (Minio.Object, FileContentReference) injectOrDelete :: (Minio.Object, FileContentReference)
-> Handler (Sum Int64, Sum Int64) -- ^ Injected, Already existed -> Handler (Sum Int64) -- ^ Injected
injectOrDelete (obj, fRef) = maybeT (return mempty) $ do injectOrDelete (obj, fRef) = do
res <- hoist (startEvalMemoT . hoistStateCache (runDB . setSerializable)) $ do fRef' <- runDB . setSerializable $ do
alreadyInjected <- lift . lift $ exists [ FileContentHash ==. fRef ] chunkVar <- newEmptyTMVarIO
if | alreadyInjected -> return (mempty, Sum 1) dbAsync <- allocateLinkedAsync $ do
| otherwise -> do atomically $ isEmptyTMVar chunkVar >>= guard . not
content <- flip memo obj $ \obj' -> hoistMaybeM . runAppMinio . runMaybeT $ do sinkFileDB False $ C.unfoldM (\x -> fmap (, x) <$> atomically (takeTMVar chunkVar)) ()
objRes <- catchIfMaybeT minioIsDoesNotExist $ Minio.getObject uploadBucket obj' Minio.defaultGetObjectOptions
lift . runConduit $ Minio.gorObjectStream objRes .| C.fold
fmap ((, mempty) . Sum) . lift. lift . E.insertSelectCount $ didSend <- maybeT (return False) . hoistMaybeM . runAppMinio . runMaybeT $ do
let isReferenced = E.any E.exists $ fileReferences (E.val fRef) objRes <- catchIfMaybeT minioIsDoesNotExist $ Minio.getObject uploadBucket obj Minio.defaultGetObjectOptions
now' = E.unsafeSqlCastAs "TIMESTAMP WITH TIME ZONE" $ E.val now lift . runConduit $ Minio.gorObjectStream objRes .| C.mapM_ (atomically . putTMVar chunkVar . Just)
in return $ FileContent E.<# E.val fRef E.<&> E.val content E.<&> E.bool (E.just now') E.nothing isReferenced return True
runAppMinio . maybeT (return ()) . catchIfMaybeT minioIsDoesNotExist $ Minio.removeObject uploadBucket obj if
return res | not didSend -> Nothing <$ cancel dbAsync
| otherwise -> do
atomically $ putTMVar chunkVar Nothing
Just <$> waitAsync dbAsync
let matchesFRef = is _Just $ assertM (== fRef) fRef'
if | matchesFRef ->
maybeT (return ()) . runAppMinio . handleIf minioIsDoesNotExist (const $ return ()) $ Minio.removeObject uploadBucket obj
| otherwise ->
$logErrorS "InjectFiles" [st|Minio object #{obj}'s content does not match it's name (content hash: #{tshow fRef'} /= name hash: #{tshow fRef})|]
return . bool mempty (Sum 1) $ is _Just fRef'
(Sum inj, Sum exc) <- Sum inj <-
runConduit $ transPipe runAppMinio (Minio.listObjects uploadBucket Nothing True) runConduit $ transPipe runAppMinio (Minio.listObjects uploadBucket Nothing True)
.| C.mapMaybe extractReference .| C.mapMaybe extractReference
.| maybe (C.map id) (takeWhileTime . (/ 2)) interval .| maybe (C.map id) (takeWhileTime . (/ 2)) interval
@ -118,7 +245,49 @@ dispatchJobInjectFiles = JobHandlerException . maybeT (return ()) $ do
.| transPipe lift (C.mapM injectOrDelete) .| transPipe lift (C.mapM injectOrDelete)
.| C.fold .| C.fold
when (exc > 0) $
$logInfoS "InjectFiles" [st|Deleted #{exc} files from upload cache because they were already injected|]
when (inj > 0) $ when (inj > 0) $
$logInfoS "InjectFiles" [st|Injected #{inj} files from upload cache into database|] $logInfoS "InjectFiles" [st|Injected #{inj} files from upload cache into database|]
data RechunkFileException
= RechunkFileExceptionHashMismatch
{ oldHash, newHash :: FileContentReference }
deriving (Eq, Ord, Show, Generic, Typeable)
deriving anyclass (Exception)
dispatchJobRechunkFiles :: JobHandler UniWorX
dispatchJobRechunkFiles = JobHandlerAtomic . hoist lift $ do
interval <- getsYesod $ view _appRechunkFiles
let
getEntryCandidates = E.selectSource . E.from $ \fileContentEntry -> E.distinctOnOrderBy [E.asc $ fileContentEntry E.^. FileContentEntryHash] $ do
E.where_ . E.exists . E.from $ \(fileContentEntry' `E.InnerJoin` fileContentChunk) -> do
E.on $ fileContentChunk E.^. FileContentChunkId E.==. fileContentEntry' E.^. FileContentEntryChunkHash
E.where_ $ fileContentEntry' E.^. FileContentEntryHash E.==. fileContentEntry E.^. FileContentEntryHash
E.where_ . E.not_ $ fileContentChunk E.^. FileContentChunkContentBased
let size = E.subSelectMaybe . E.from $ \(fileContentEntry' `E.InnerJoin` fileContentChunk) -> do
E.on $ fileContentChunk E.^. FileContentChunkId E.==. fileContentEntry' E.^. FileContentEntryChunkHash
E.where_ $ fileContentEntry' E.^. FileContentEntryHash E.==. fileContentEntry E.^. FileContentEntryHash
return $ E.sum_ (E.length_ $ fileContentChunk E.^. FileContentChunkContent:: E.SqlExpr (E.Value Word64))
return ( fileContentEntry E.^. FileContentEntryHash
, size
)
rechunkFile :: FileContentReference -> Word64 -> DB (Sum Natural, Sum Word64)
rechunkFile fRef sz = do
fRef' <- sinkFileDB True $ sourceFileDB fRef
unless (fRef == fRef') $
throwM $ RechunkFileExceptionHashMismatch fRef fRef'
return (Sum 1, Sum sz)
(Sum rechunkedEntries, Sum rechunkedSize) <- runConduit $
getEntryCandidates
.| C.mapMaybe (\(E.Value fRef, E.Value sz) -> (fRef, ) <$> sz)
.| maybe (C.map id) (takeWhileTime . (/ 2)) interval
.| persistentTokenBucketTakeC' TokenBucketRechunkFiles (view _2 :: _ -> Word64)
.| C.mapM (uncurry rechunkFile)
.| C.fold
when (rechunkedEntries > 0 || rechunkedSize > 0) $
$logInfoS "RechunkFiles" [st|Rechunked #{tshow rechunkedEntries} files in database (#{tshow rechunkedSize} bytes)|]

View File

@ -27,5 +27,5 @@ dispatchJobDeleteTransactionLogIPs = JobHandlerAtomic . hoist lift $ do
retentionTime <- getsYesod $ view _appTransactionLogIPRetentionTime retentionTime <- getsYesod $ view _appTransactionLogIPRetentionTime
let cutoff = addUTCTime (- retentionTime) now let cutoff = addUTCTime (- retentionTime) now
n <- updateWhereCount [ TransactionLogTime <. cutoff ] [ TransactionLogRemote =. Nothing ] n <- updateWhereCount [ TransactionLogTime <. cutoff, TransactionLogRemote !=. Nothing ] [ TransactionLogRemote =. Nothing ]
$logInfoS "DeleteTransactionLogIPs" [st|Deleted #{n} IP entries from transaction log|] $logInfoS "DeleteTransactionLogIPs" [st|Deleted #{n} IP entries from transaction log|]

View File

@ -82,7 +82,7 @@ writeJobCtlBlock = writeJobCtlBlock' writeJobCtl
queueJobUnsafe :: Bool -> Job -> YesodDB UniWorX (Maybe QueuedJobId) queueJobUnsafe :: Bool -> Job -> YesodDB UniWorX (Maybe QueuedJobId)
queueJobUnsafe queuedJobWriteLastExec job = do queueJobUnsafe queuedJobWriteLastExec job = do
$logInfoS "queueJob" $ tshow job $logDebugS "queueJob" $ tshow job
doQueue <- fmap not . and2M (return $ jobNoQueueSame job) $ exists [ QueuedJobContent ==. toJSON job ] doQueue <- fmap not . and2M (return $ jobNoQueueSame job) $ exists [ QueuedJobContent ==. toJSON job ]

View File

@ -86,9 +86,13 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
, jDisplayEmail :: UserEmail , jDisplayEmail :: UserEmail
} }
| JobPruneSessionFiles | JobPruneSessionFiles
| JobPruneUnreferencedFiles | JobPruneUnreferencedFiles { jNumIterations
, jEpoch
, jIteration :: Natural
}
| JobInjectFiles | JobInjectFiles
| JobPruneFallbackPersonalisedSheetFilesKeys | JobPruneFallbackPersonalisedSheetFilesKeys
| JobRechunkFiles
deriving (Eq, Ord, Show, Read, Generic, Typeable) deriving (Eq, Ord, Show, Read, Generic, Typeable)
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId } data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
| NotificationSheetActive { nSheet :: SheetId } | NotificationSheetActive { nSheet :: SheetId }
@ -223,6 +227,7 @@ newWorkerId = JobWorkerId <$> liftIO newUnique
data JobContext = JobContext data JobContext = JobContext
{ jobCrontab :: TVar (Crontab JobCtl) { jobCrontab :: TVar (Crontab JobCtl)
, jobConfirm :: TVar (HashMap JobCtl (NonEmpty (TMVar (Maybe SomeException)))) , jobConfirm :: TVar (HashMap JobCtl (NonEmpty (TMVar (Maybe SomeException))))
, jobHeldLocks :: TVar (Set QueuedJobId)
} }
@ -251,6 +256,8 @@ jobNoQueueSame = \case
JobPruneSessionFiles{} -> True JobPruneSessionFiles{} -> True
JobPruneUnreferencedFiles{} -> True JobPruneUnreferencedFiles{} -> True
JobInjectFiles{} -> True JobInjectFiles{} -> True
JobPruneFallbackPersonalisedSheetFilesKeys{} -> True
JobRechunkFiles{} -> True
_ -> False _ -> False

View File

@ -7,6 +7,8 @@ module Model.Migration
import Import.NoModel hiding (Max(..), Last(..)) import Import.NoModel hiding (Max(..), Last(..))
import Model import Model
import Settings
import Foundation.Type
import Jobs.Types import Jobs.Types
import Audit.Types import Audit.Types
import Model.Migration.Version import Model.Migration.Version
@ -40,6 +42,8 @@ import qualified Data.CaseInsensitive as CI
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import Web.ServerSession.Backend.Persistent.Memcached (migrateMemcachedSqlStorage) import Web.ServerSession.Backend.Persistent.Memcached (migrateMemcachedSqlStorage)
import Data.Conduit.Algorithms.FastCDC (FastCDCParameters(fastCDCMinBlockSize))
-- Database versions must follow https://pvp.haskell.org: -- Database versions must follow https://pvp.haskell.org:
-- - Breaking changes are instances where manual migration is necessary (via customMigrations; i.e. changing a columns format) -- - Breaking changes are instances where manual migration is necessary (via customMigrations; i.e. changing a columns format)
@ -80,6 +84,7 @@ migrateAll' = sequence_
migrateAll :: ( MonadLogger m migrateAll :: ( MonadLogger m
, MonadResource m , MonadResource m
, MonadUnliftIO m , MonadUnliftIO m
, MonadReader UniWorX m
) )
=> ReaderT SqlBackend m () => ReaderT SqlBackend m ()
migrateAll = do migrateAll = do
@ -112,7 +117,7 @@ requiresMigration = mapReaderT (exceptT return return) $ do
$logInfoS "Migration" $ intercalate "; " initial $logInfoS "Migration" $ intercalate "; " initial
throwError True throwError True
customs <- mapReaderT lift $ getMissingMigrations @_ @m customs <- mapReaderT lift $ getMissingMigrations @_ @(ReaderT UniWorX m)
unless (Map.null customs) $ do unless (Map.null customs) $ do
$logInfoS "Migration" . intercalate ", " . map tshow $ Map.keys customs $logInfoS "Migration" . intercalate ", " . map tshow $ Map.keys customs
throwError True throwError True
@ -134,6 +139,7 @@ getMissingMigrations :: forall m m'.
( MonadLogger m ( MonadLogger m
, MonadIO m , MonadIO m
, MonadResource m' , MonadResource m'
, MonadReader UniWorX m'
) )
=> ReaderT SqlBackend m (Map (Key AppliedMigration) (ReaderT SqlBackend m' ())) => ReaderT SqlBackend m (Map (Key AppliedMigration) (ReaderT SqlBackend m' ()))
getMissingMigrations = do getMissingMigrations = do
@ -180,7 +186,9 @@ migrateManual = do
-} -}
customMigrations :: forall m. customMigrations :: forall m.
MonadResource m ( MonadResource m
, MonadReader UniWorX m
)
=> Map (Key AppliedMigration) (ReaderT SqlBackend m ()) => Map (Key AppliedMigration) (ReaderT SqlBackend m ())
customMigrations = Map.fromListWith (>>) customMigrations = Map.fromListWith (>>)
[ ( AppliedMigrationKey [migrationVersion|initial|] [version|0.0.0|] [ ( AppliedMigrationKey [migrationVersion|initial|] [version|0.0.0|]
@ -915,13 +923,32 @@ 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") $ do , 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;
UPDATE study_features SET first_observed = (SELECT MAX(last_observed) FROM study_features as other WHERE other."user" = study_features."user" AND other.degree = study_features.degree AND other.field = study_features.field AND other.type = study_features.type AND other.semester = study_features.semester - 1); UPDATE study_features SET first_observed = (SELECT MAX(last_observed) FROM study_features as other WHERE other."user" = study_features."user" AND other.degree = study_features.degree AND other.field = study_features.field AND other.type = study_features.type AND other.semester = study_features.semester - 1);
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|40.0.0|] [version|41.0.0|]
, whenM (tableExists "file_content") $ do
chunkingParams <- lift $ view _appFileChunkingParams
[executeQQ|
ALTER TABLE file_content RENAME TO file_content_chunk;
ALTER INDEX file_content_pkey RENAME TO file_content_chunk_pkey;
CREATE TABLE file_content_chunk_unreferenced (id bigserial, hash bytea NOT NULL, since timestamp with time zone NOT NULL);
INSERT INTO file_content_chunk_unreferenced (since, hash) (SELECT unreferenced_since as since, hash FROM file_content_chunk WHERE NOT (unreferenced_since IS NULL));
ALTER TABLE file_content_chunk DROP COLUMN unreferenced_since;
ALTER TABLE file_content_chunk ADD COLUMN content_based boolean NOT NULL DEFAULT false;
UPDATE file_content_chunk SET content_based = true WHERE length(content) <= #{fastCDCMinBlockSize chunkingParams};
CREATE TABLE file_content_entry (hash bytea NOT NULL, ix bigint NOT NULL, chunk_hash bytea NOT NULL);
INSERT INTO file_content_entry (hash, chunk_hash, ix) (SELECT hash, hash as chunk_hash, 0 as ix FROM file_content_chunk);
|]
)
] ]

View File

@ -54,5 +54,3 @@ type InstanceId = UUID
type ClusterId = UUID type ClusterId = UUID
type TokenId = UUID type TokenId = UUID
type TermCandidateIncidence = UUID type TermCandidateIncidence = UUID
type FileContentReference = Digest SHA3_512

View File

@ -1,23 +1,129 @@
module Model.Types.File module Model.Types.File
( File(..), _fileTitle, _fileContent, _fileModified ( FileContentChunkReference(..), FileContentReference(..)
, File(..), _fileTitle, _fileContent, _fileModified
, PureFile, toPureFile, fromPureFile, pureFileToFileReference, _pureFileContent
, transFile
, 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
import Model.Types.Common (FileContentReference)
import Database.Persist.Sql (PersistFieldSql)
import Web.HttpApiData (ToHttpApiData, FromHttpApiData)
import Data.ByteArray (ByteArrayAccess)
import qualified Data.ByteString.Base64.URL as Base64
import qualified Data.ByteArray as ByteArray
import qualified Network.Minio as Minio (Object)
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
data File = File import Text.Show
newtype FileContentChunkReference = FileContentChunkReference (Digest SHA3_512)
deriving (Eq, Ord, Read, Show, Lift, Generic, Typeable)
deriving newtype ( PersistField, PersistFieldSql
, PathPiece, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON
, Hashable, NFData
, ByteArrayAccess
)
makeWrapped ''FileContentChunkReference
newtype FileContentReference = FileContentReference (Digest SHA3_512)
deriving (Eq, Ord, Read, Show, Lift, Generic, Typeable)
deriving newtype ( PersistField, PersistFieldSql
, PathPiece, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON
, Hashable, NFData
, ByteArrayAccess
)
makeWrapped ''FileContentReference
minioFileReference :: Prism' Minio.Object FileContentReference
minioFileReference = prism' toObjectName fromObjectName
where toObjectName = decodeUtf8 . Base64.encodeUnpadded . ByteArray.convert
fromObjectName = fmap (review _Wrapped) . Crypto.digestFromByteString <=< preview _Right . Base64.decodeUnpadded . encodeUtf8
data File m = File
{ fileTitle :: FilePath { fileTitle :: FilePath
, fileContent :: Maybe ByteString , fileContent :: Maybe (ConduitT () ByteString m ())
, fileModified :: UTCTime , fileModified :: UTCTime
} deriving (Eq, Ord, Read, Show, Generic, Typeable) } deriving (Generic, Typeable)
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 l File{..} = File{ fileContent = transPipe l <$> fileContent, .. }
data FileReference = FileReference data FileReference = FileReference
{ fileReferenceTitle :: FilePath { fileReferenceTitle :: FilePath
, fileReferenceContent :: Maybe FileContentReference , fileReferenceContent :: Maybe FileContentReference
@ -36,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

@ -267,6 +267,7 @@ instance Csv.FromField Sex where
data TokenBucketIdent = TokenBucketInjectFiles data TokenBucketIdent = TokenBucketInjectFiles
| TokenBucketPruneFiles | TokenBucketPruneFiles
| TokenBucketRechunkFiles
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite, Hashable) deriving anyclass (Universe, Finite, Hashable)

View File

@ -70,6 +70,8 @@ import Text.Show (showParen, showString)
import qualified Data.List.PointedList as P import qualified Data.List.PointedList as P
import qualified Network.Minio as Minio import qualified Network.Minio as Minio
import Data.Conduit.Algorithms.FastCDC
-- | Runtime settings to configure this application. These settings can be -- | Runtime settings to configure this application. These settings can be
@ -113,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
@ -140,8 +142,10 @@ data AppSettings = AppSettings
, appLdapReTestFailover :: DiffTime , appLdapReTestFailover :: DiffTime
, appSessionFilesExpire :: NominalDiffTime , appSessionFilesExpire :: NominalDiffTime
, appPruneUnreferencedFiles :: Maybe NominalDiffTime
, appKeepUnreferencedFiles :: NominalDiffTime , appKeepUnreferencedFiles :: NominalDiffTime
, appPruneUnreferencedFilesWithin :: Maybe NominalDiffTime
, appPruneUnreferencedFilesInterval :: NominalDiffTime
, appInitialLogSettings :: LogSettings , appInitialLogSettings :: LogSettings
@ -172,6 +176,10 @@ data AppSettings = AppSettings
, appUploadCacheConf :: Maybe Minio.ConnectInfo , appUploadCacheConf :: Maybe Minio.ConnectInfo
, appUploadCacheBucket :: Minio.Bucket , appUploadCacheBucket :: Minio.Bucket
, appInjectFiles :: Maybe NominalDiffTime , appInjectFiles :: Maybe NominalDiffTime
, appRechunkFiles :: Maybe NominalDiffTime
, appFileUploadDBChunksize :: Int
, appFileChunkingParams :: FastCDCParameters
, appFavouritesQuickActionsBurstsize , appFavouritesQuickActionsBurstsize
, appFavouritesQuickActionsAvgInverseRate :: Word64 , appFavouritesQuickActionsAvgInverseRate :: Word64
@ -444,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"
@ -471,9 +479,17 @@ instance FromJSON AppSettings where
appLdapReTestFailover <- o .: "ldap-re-test-failover" appLdapReTestFailover <- o .: "ldap-re-test-failover"
appSessionFilesExpire <- o .: "session-files-expire" appSessionFilesExpire <- o .: "session-files-expire"
appPruneUnreferencedFiles <- o .:? "prune-unreferenced-files"
appKeepUnreferencedFiles <- o .:? "keep-unreferenced-files" .!= 0 appKeepUnreferencedFiles <- o .:? "keep-unreferenced-files" .!= 0
appInjectFiles <- o .:? "inject-files" appInjectFiles <- o .:? "inject-files"
appRechunkFiles <- o .:? "rechunk-files"
appFileUploadDBChunksize <- o .: "file-upload-db-chunksize"
appFileChunkingTargetExponent <- o .: "file-chunking-target-exponent"
appFileChunkingHashWindow <- o .: "file-chunking-hash-window"
appFileChunkingParams <- maybe (fail "Could not recommend FastCDCParameters") return $ recommendFastCDCParameters appFileChunkingTargetExponent appFileChunkingHashWindow
appPruneUnreferencedFilesWithin <- o .: "prune-unreferenced-files-within"
appPruneUnreferencedFilesInterval <- o .: "prune-unreferenced-files-interval"
appMaximumContentLength <- o .: "maximum-content-length" appMaximumContentLength <- o .: "maximum-content-length"

View File

@ -56,7 +56,8 @@ import Control.Arrow as Utils ((>>>))
import Control.Monad.Trans.Except (ExceptT(..), throwE, runExceptT) import Control.Monad.Trans.Except (ExceptT(..), throwE, runExceptT)
import Control.Monad.Except (MonadError(..)) import Control.Monad.Except (MonadError(..))
import Control.Monad.Trans.Maybe as Utils (MaybeT(..)) import Control.Monad.Trans.Maybe as Utils (MaybeT(..))
import Control.Monad.Trans.Writer.Lazy (WriterT, execWriterT, tell) import Control.Monad.Trans.Writer.Strict (execWriterT)
import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Catch import Control.Monad.Catch
import Control.Monad.Morph (hoist) import Control.Monad.Morph (hoist)
import Control.Monad.Fail import Control.Monad.Fail
@ -83,6 +84,9 @@ import qualified Crypto.Saltine.Class as Saltine
import qualified Crypto.Data.PKCS7 as PKCS7 import qualified Crypto.Data.PKCS7 as PKCS7
import Crypto.MAC.KMAC (KMAC, HashSHAKE) import Crypto.MAC.KMAC (KMAC, HashSHAKE)
import qualified Crypto.MAC.KMAC as KMAC import qualified Crypto.MAC.KMAC as KMAC
import qualified Crypto.Hash as Crypto
import Crypto.Hash (HashAlgorithm, Digest)
import Crypto.Hash.Instances ()
import Data.ByteArray (ByteArrayAccess) import Data.ByteArray (ByteArrayAccess)
@ -843,7 +847,7 @@ diffTimeout timeoutLength timeoutRes act = fromMaybe timeoutRes <$> timeout time
= let (MkFixed micro :: Micro) = realToFrac timeoutLength = let (MkFixed micro :: Micro) = realToFrac timeoutLength
in fromInteger micro in fromInteger micro
tellM :: (Monad m, Monoid x) => m x -> WriterT x m () tellM :: (MonadTrans t, MonadWriter x (t m), Monad m) => m x -> t m ()
tellM = tell <=< lift tellM = tell <=< lift
------------- -------------
@ -856,6 +860,19 @@ peekN n = do
mapM_ leftover peeked mapM_ leftover peeked
return peeked return peeked
peekWhile :: forall a o m. Monad m => (a -> Bool) -> ConduitT a o m [a]
peekWhile p = do
let go acc = do
next <- await
case next of
Nothing -> return (reverse acc, Nothing)
Just x
| p x -> go $ x : acc
| otherwise -> return (reverse acc, Just x)
(peeked, failed) <- go []
mapM_ leftover $ peeked ++ hoistMaybe failed
return peeked
anyMC, allMC :: forall a o m. Monad m => (a -> m Bool) -> ConduitT a o m Bool anyMC, allMC :: forall a o m. Monad m => (a -> m Bool) -> ConduitT a o m Bool
anyMC f = C.mapM f .| orC anyMC f = C.mapM f .| orC
allMC f = C.mapM f .| andC allMC f = C.mapM f .| andC
@ -1057,6 +1074,12 @@ kmaclazy :: forall a string key ba chunk.
-> KMAC a -> KMAC a
kmaclazy str k = KMAC.finalize . KMAC.updates (KMAC.initialize @a str k) . toChunks kmaclazy str k = KMAC.finalize . KMAC.updates (KMAC.initialize @a str k) . toChunks
emptyHash :: forall a. HashAlgorithm a => Q (TExp (Digest a))
-- ^ Hash of `mempty`
--
-- Computationally preferrable to computing the hash at runtime
emptyHash = TH.liftTyped $ Crypto.hashFinalize Crypto.hashInit
------------- -------------
-- Caching -- -- Caching --
------------- -------------

View File

@ -3,6 +3,7 @@ module Utils.Files
, sinkFile', sinkFiles' , sinkFile', sinkFiles'
, FileUploads , FileUploads
, replaceFileReferences, replaceFileReferences' , replaceFileReferences, replaceFileReferences'
, sinkFileDB
) where ) where
import Import.NoFoundation import Import.NoFoundation
@ -11,31 +12,54 @@ import Handler.Utils.Minio
import qualified Network.Minio as Minio import qualified Network.Minio as Minio
import qualified Crypto.Hash as Crypto (hash) import qualified Crypto.Hash as Crypto (hash)
import qualified Crypto.Hash.Conduit as Crypto (sinkHash)
import qualified Data.Conduit.Combinators as C import qualified Data.Conduit.Combinators as C
import qualified Data.Conduit.List as C (unfoldM)
import qualified Data.ByteString.Base64.URL as Base64
import qualified Data.ByteArray as ByteArray
import qualified Data.Map.Lazy as Map import qualified Data.Map.Lazy as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import Control.Monad.State.Class (modify) import Control.Monad.State.Class (modify)
import qualified Data.Sequence as Seq
import Database.Persist.Sql (deleteWhereCount) import Database.Persist.Sql (deleteWhereCount)
import Control.Monad.Trans.Resource (allocate) import Control.Monad.Trans.Resource (allocate)
import qualified Data.UUID.V4 as UUID
sinkFiles :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX) => ConduitT File FileReference (SqlPersistT m) () import qualified Database.Esqueleto as E
sinkFiles = C.mapM sinkFile import qualified Database.Esqueleto.Utils as E
import Data.Conduit.Algorithms.FastCDC (fastCDC)
sinkFileDB :: (MonadCatch m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX)
=> Bool -- ^ Replace? Use only in serializable transaction
-> ConduitT () ByteString (SqlPersistT m) ()
-> SqlPersistT m FileContentReference
sinkFileDB doReplace fileContentContent = do
chunkingParams <- getsYesod $ view _appFileChunkingParams
let sinkChunk fileContentChunkContent = do
fileChunkLockTime <- liftIO getCurrentTime
fileChunkLockInstance <- getsYesod appInstanceID
tellM $ Set.singleton <$> insert FileChunkLock{ fileChunkLockHash = fileContentChunkHash, .. }
existsChunk <- lift $ exists [FileContentChunkHash ==. fileContentChunkHash]
let setContentBased = updateWhere [FileContentChunkHash ==. fileContentChunkHash] [FileContentChunkContentBased =. fileContentChunkContentBased]
if | existsChunk -> lift setContentBased
| otherwise -> lift . handleIfSql isUniqueConstraintViolation (const setContentBased) $
insert_ FileContentChunk{..}
return $ FileContentChunkKey fileContentChunkHash
where fileContentChunkHash = _Wrapped # Crypto.hash fileContentChunkContent
((review _Wrapped -> fileContentHash, fileContentChunks), chunkLocks) <- runConduit . runWriterC $
transPipe lift fileContentContent
.| fastCDC chunkingParams
.| C.mapM (\c -> (c, ) <$> sinkChunk c)
.| transPipe lift (getZipConduit $ (,) <$> ZipConduit (C.map (view _1) .| Crypto.sinkHash) <*> ZipConduit (C.foldMap $ views _2 Seq.singleton))
sinkFile :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX) => File -> SqlPersistT m FileReference
sinkFile File{ fileContent = Nothing, .. } = return FileReference
{ fileReferenceContent = Nothing
, fileReferenceTitle = fileTitle
, fileReferenceModified = fileModified
}
sinkFile File{ fileContent = Just fileContentContent, .. } = do
void . withUnliftIO $ \UnliftIO{..} -> void . withUnliftIO $ \UnliftIO{..} ->
let takeLock = do let takeLock = do
fileLockTime <- liftIO getCurrentTime fileLockTime <- liftIO getCurrentTime
@ -44,35 +68,86 @@ sinkFile File{ fileContent = Just fileContentContent, .. } = do
releaseLock lId = liftHandler . runDB $ (withReaderT projectBackend $ setSerializable (delete lId :: SqlPersistT (HandlerFor UniWorX) ()) :: YesodDB UniWorX ()) releaseLock lId = liftHandler . runDB $ (withReaderT projectBackend $ setSerializable (delete lId :: SqlPersistT (HandlerFor UniWorX) ()) :: YesodDB UniWorX ())
in unliftIO $ allocate (unliftIO takeLock) (unliftIO . releaseLock) in unliftIO $ allocate (unliftIO takeLock) (unliftIO . releaseLock)
inDB <- exists [ FileContentHash ==. fileContentHash ] deleteWhere [ FileChunkLockId <-. Set.toList chunkLocks ]
let sinkFileDB = unless inDB $ repsert (FileContentKey fileContentHash) FileContent{ fileContentUnreferencedSince = Nothing, .. } let entryExists = E.selectExists . E.from $ \fileContentEntry -> E.where_ $ fileContentEntry E.^. FileContentEntryHash E.==. E.val fileContentHash
maybeT sinkFileDB $ do insertEntries = handleIfSql isUniqueConstraintViolation (const $ return ()) . void $ insertMany_
let uploadName = decodeUtf8 . Base64.encodeUnpadded $ ByteArray.convert fileContentHash [ FileContentEntry{ fileContentEntryHash = fileContentHash, .. }
uploadBucket <- getsYesod $ views appSettings appUploadCacheBucket | fileContentEntryChunkHash <- otoList fileContentChunks
unless inDB . runAppMinio $ do | fileContentEntryIx <- [0..]
uploadExists <- handleIf minioIsDoesNotExist (const $ return False) $ True <$ Minio.statObject uploadBucket uploadName Minio.defaultGetObjectOptions ]
unless uploadExists $ do if | not doReplace -> unlessM entryExists insertEntries
let | otherwise -> do
pooOptions = Minio.defaultPutObjectOptions deleteWhere [ FileContentEntryHash ==. fileContentHash ]
{ Minio.pooCacheControl = Just "immutable" insertEntries
}
Minio.putObject uploadBucket uploadName (C.sourceLazy $ fromStrict fileContentContent) (Just . fromIntegral $ olength fileContentContent) pooOptions
-- Note that MinIO does not accept length zero uploads without an explicit length specification (not `Nothing` in the line above for the api we use) return fileContentHash
where fileContentChunkContentBased = True
sinkFiles :: (MonadCatch m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX) => ConduitT (File (SqlPersistT m)) FileReference (SqlPersistT m) ()
sinkFiles = C.mapM sinkFile
sinkFile :: (MonadCatch m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX) => File (SqlPersistT m) -> SqlPersistT m FileReference
sinkFile File{ fileContent = Nothing, .. } = return FileReference
{ fileReferenceContent = Nothing
, fileReferenceTitle = fileTitle
, fileReferenceModified = fileModified
}
sinkFile File{ fileContent = Just fileContentContent, .. } = do
(unsealConduitT -> fileContentContent', isEmpty) <- fileContentContent $$+ is _Nothing <$> C.peekE
fileContentHash <- if
| not isEmpty -> maybeT (sinkFileDB False fileContentContent') $ do
uploadBucket <- getsYesod $ views appSettings appUploadCacheBucket
chunk <- liftIO newEmptyMVar
let putChunks = do
nextChunk <- await
case nextChunk of
Nothing
-> putMVar chunk Nothing
Just nextChunk'
-> putMVar chunk (Just nextChunk') >> yield nextChunk' >> putChunks
sinkAsync <- lift . allocateLinkedAsync . runConduit
$ fileContentContent'
.| putChunks
.| Crypto.sinkHash
runAppMinio $ do
tmpUUID <- liftIO UUID.nextRandom
let uploadName = ".tmp." <> toPathPiece tmpUUID
pooOptions = Minio.defaultPutObjectOptions
{ Minio.pooCacheControl = Just "immutable"
}
Minio.putObject uploadBucket uploadName (C.unfoldM (\x -> fmap (, x) <$> takeMVar chunk) ()) Nothing pooOptions
fileContentHash <- review _Wrapped <$> waitAsync sinkAsync
let dstName = minioFileReference # fileContentHash
copySrc = Minio.defaultSourceInfo
{ Minio.srcBucket = uploadBucket, Minio.srcObject = uploadName
}
copyDst = Minio.defaultDestinationInfo
{ Minio.dstBucket = uploadBucket
, Minio.dstObject = dstName
}
uploadExists <- handleIf minioIsDoesNotExist (const $ return False) $ True <$ Minio.statObject uploadBucket dstName Minio.defaultGetObjectOptions
unless uploadExists $
Minio.copyObject copyDst copySrc
Minio.removeObject uploadBucket uploadName
return fileContentHash
| otherwise -> return $$(liftTyped $ FileContentReference $$(emptyHash))
return FileReference return FileReference
{ fileReferenceContent = Just fileContentHash { fileReferenceContent = Just fileContentHash
, fileReferenceTitle = fileTitle , fileReferenceTitle = fileTitle
, fileReferenceModified = fileModified , fileReferenceModified = fileModified
} }
where
fileContentHash = Crypto.hash fileContentContent
sinkFiles' :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX, HasFileReference record) => ConduitT (File, FileReferenceResidual record) record (SqlPersistT m) () sinkFiles' :: (MonadCatch m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX, HasFileReference record) => ConduitT (File (SqlPersistT m), FileReferenceResidual record) record (SqlPersistT m) ()
sinkFiles' = C.mapM $ uncurry sinkFile' sinkFiles' = C.mapM $ uncurry sinkFile'
sinkFile' :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX, HasFileReference record) => File -> FileReferenceResidual record -> SqlPersistT m record sinkFile' :: (MonadCatch m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX, HasFileReference record) => File (SqlPersistT m) -> FileReferenceResidual record -> SqlPersistT m record
sinkFile' file residual = do sinkFile' file residual = do
reference <- sinkFile file reference <- sinkFile file
return $ _FileReference # (reference, residual) return $ _FileReference # (reference, residual)

View File

@ -1,22 +1,32 @@
module Utils.Sql module Utils.Sql
( setSerializable, setSerializable' ( setSerializable, setSerializable'
, catchSql, handleSql
, isUniqueConstraintViolation
, catchIfSql, handleIfSql
) where ) where
import ClassyPrelude.Yesod import ClassyPrelude.Yesod hiding (handle)
import Numeric.Natural import Numeric.Natural
import Settings.Log import Settings.Log
import Database.PostgreSQL.Simple (SqlError) import Database.PostgreSQL.Simple (SqlError(..))
import Database.PostgreSQL.Simple.Errors (isSerializationError) import Database.PostgreSQL.Simple.Errors (isSerializationError)
import Control.Monad.Catch (MonadMask) import Control.Monad.Catch
import Database.Persist.Sql import Database.Persist.Sql
import Database.Persist.Sql.Raw.QQ import Database.Persist.Sql.Raw.QQ
import qualified Data.ByteString as ByteString
import Control.Retry import Control.Retry
import Control.Lens ((&)) import Control.Lens ((&))
import qualified Data.UUID as UUID
import Control.Monad.Random.Class (MonadRandom(getRandom))
import Text.Shakespeare.Text (st)
setSerializable :: forall m a. (MonadLogger m, MonadMask m, MonadIO m, ReadLogSettings (SqlPersistT m)) => SqlPersistT m a -> SqlPersistT m a setSerializable :: forall m a. (MonadLogger m, MonadMask m, MonadIO m, ReadLogSettings (SqlPersistT m)) => SqlPersistT m a -> SqlPersistT m a
setSerializable = setSerializable' $ fullJitterBackoff 1e3 & limitRetriesByCumulativeDelay 10e6 setSerializable = setSerializable' $ fullJitterBackoff 1e3 & limitRetriesByCumulativeDelay 10e6
@ -54,5 +64,29 @@ setSerializable' policy act = do
transactionSaveWithIsolation ReadCommitted transactionSaveWithIsolation ReadCommitted
return res return res
catchSql :: forall m a. (MonadCatch m, MonadIO m) => SqlPersistT m a -> (SqlError -> SqlPersistT m a) -> SqlPersistT m a
catchSql = flip handleSql
handleSql :: forall m a. (MonadCatch m, MonadIO m) => (SqlError -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a
handleSql recover act = do
savepointName <- liftIO $ UUID.toString <$> getRandom
let recover' :: SqlError -> SqlPersistT m a
recover' exc = do
rawExecute [st|ROLLBACK TO SAVEPOINT "#{savepointName}"|] []
recover exc
handle recover' $ do
rawExecute [st|SAVEPOINT "#{savepointName}"|] []
res <- act
rawExecute [st|RELEASE SAVEPOINT "#{savepointName}"|] []
return res
catchIfSql :: forall m a. (MonadCatch m, MonadIO m) => (SqlError -> Bool) -> SqlPersistT m a -> (SqlError -> SqlPersistT m a) -> SqlPersistT m a
catchIfSql p = flip $ handleIfSql p
handleIfSql :: forall m a. (MonadCatch m, MonadIO m) => (SqlError -> Bool) -> (SqlError -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a
handleIfSql p recover = handleSql (\err -> bool throwM recover (p err) err)
isUniqueConstraintViolation :: SqlError -> Bool
isUniqueConstraintViolation SqlError{..} = "duplicate key value violates unique constraint" `ByteString.isPrefixOf` sqlErrorMsg

View File

@ -47,6 +47,15 @@ extra-deps:
- filepath-crypto - filepath-crypto
- uuid-crypto - uuid-crypto
- git: https://github.com/gkleen/FastCDC.git
commit: 7326e2931454282df9081105dad812845db5c530
subdirs:
- gearhash
- fastcdc
- git: git@gitlab2.rz.ifi.lmu.de:uni2work/zip-stream.git
commit: 843683d024f767de236f74d24a3348f69181a720
- 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
@ -65,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

@ -5,9 +5,6 @@
packages: packages:
- completed: - completed:
cabal-file:
size: 4229
sha256: 0dcfe3c4a67be4e96e1ae2e3c4b8744bc11e094853005a32f6074ab776caa3a9
name: encoding name: encoding
version: 0.8.2 version: 0.8.2
git: git@gitlab2.rz.ifi.lmu.de:uni2work/encoding.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/encoding.git
@ -19,9 +16,6 @@ packages:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/encoding.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/encoding.git
commit: 22fc3bb14841d8d50997aa47f1be3852e666f787 commit: 22fc3bb14841d8d50997aa47f1be3852e666f787
- completed: - completed:
cabal-file:
size: 2399
sha256: 20cdf97602abb8fd7356c1a64c69fa857e34ab4cfe7834460d2ad783f7e4e4e3
name: memcached-binary name: memcached-binary
version: 0.2.0 version: 0.2.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/memcached-binary.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/memcached-binary.git
@ -33,9 +27,6 @@ packages:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/memcached-binary.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/memcached-binary.git
commit: b7071df50bad3a251a544b984e4bf98fa09b8fae commit: b7071df50bad3a251a544b984e4bf98fa09b8fae
- completed: - completed:
cabal-file:
size: 1423
sha256: 49818ee0de2d55cbfbc15ca4de1761c3adac6ba3dfcdda960b413cad4f4fa47f
name: conduit-resumablesink name: conduit-resumablesink
version: '0.3' version: '0.3'
git: git@gitlab2.rz.ifi.lmu.de:uni2work/conduit-resumablesink.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/conduit-resumablesink.git
@ -47,9 +38,6 @@ packages:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/conduit-resumablesink.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/conduit-resumablesink.git
commit: cbea6159c2975d42f948525e03e12fc390da53c5 commit: cbea6159c2975d42f948525e03e12fc390da53c5
- completed: - completed:
cabal-file:
size: 2069
sha256: 9192ac19ea5da3cd4b8c86a4266592aff7b9256311aa5f42ae6de94ccacf1366
name: HaskellNet name: HaskellNet
version: 0.5.1 version: 0.5.1
git: git://github.com/jtdaugherty/HaskellNet.git git: git://github.com/jtdaugherty/HaskellNet.git
@ -61,9 +49,6 @@ packages:
git: git://github.com/jtdaugherty/HaskellNet.git git: git://github.com/jtdaugherty/HaskellNet.git
commit: 5aa1f3b009253b02c4822005ac59ee208a10a347 commit: 5aa1f3b009253b02c4822005ac59ee208a10a347
- completed: - completed:
cabal-file:
size: 1934
sha256: 9fbe7c3681e963eea213ab38be17966bb690788c1c55a67257916b677d7d2ec2
name: HaskellNet-SSL name: HaskellNet-SSL
version: 0.3.4.1 version: 0.3.4.1
git: git@gitlab2.rz.ifi.lmu.de:uni2work/HaskellNet-SSL.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/HaskellNet-SSL.git
@ -75,9 +60,6 @@ packages:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/HaskellNet-SSL.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/HaskellNet-SSL.git
commit: 40393c938111ac78232dc2c7eec5edb4a22d03e8 commit: 40393c938111ac78232dc2c7eec5edb4a22d03e8
- completed: - completed:
cabal-file:
size: 2208
sha256: 48f6e03d8f812bd24e2601497ffe9c8a78907fa2266ba05abeefdfe99221617d
name: ldap-client name: ldap-client
version: 0.4.0 version: 0.4.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/ldap-client.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/ldap-client.git
@ -90,9 +72,6 @@ packages:
commit: 01afaf599ba6f8a9d804c269e91d3190b249d3f0 commit: 01afaf599ba6f8a9d804c269e91d3190b249d3f0
- completed: - completed:
subdir: serversession subdir: serversession
cabal-file:
size: 2081
sha256: a958ff0007e5084e3e4c2a33acc9860c31186105f02f8ab99ecb847a7a8f9497
name: serversession name: serversession
version: 1.0.1 version: 1.0.1
git: git@gitlab2.rz.ifi.lmu.de:uni2work/serversession.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/serversession.git
@ -106,9 +85,6 @@ packages:
commit: 1c95b0100471279413485411032d639881012a5e commit: 1c95b0100471279413485411032d639881012a5e
- completed: - completed:
subdir: serversession-backend-acid-state subdir: serversession-backend-acid-state
cabal-file:
size: 1875
sha256: 6cc9d29e788334670bc102213a8aae73bc1b8b0a00c416f06d232376750443b7
name: serversession-backend-acid-state name: serversession-backend-acid-state
version: 1.0.3 version: 1.0.3
git: git@gitlab2.rz.ifi.lmu.de:uni2work/serversession.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/serversession.git
@ -121,9 +97,6 @@ packages:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/serversession.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/serversession.git
commit: 1c95b0100471279413485411032d639881012a5e commit: 1c95b0100471279413485411032d639881012a5e
- completed: - completed:
cabal-file:
size: 1966
sha256: 92d6d7be95a1ee6cb9783deb35e0d4e4959c7de20d518a45370084b57e20ba51
name: xss-sanitize name: xss-sanitize
version: 0.3.6 version: 0.3.6
git: git@gitlab2.rz.ifi.lmu.de:uni2work/xss-sanitize.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/xss-sanitize.git
@ -136,9 +109,6 @@ packages:
commit: 074ed7c8810aca81f60f2c535f9e7bad67e9d95a commit: 074ed7c8810aca81f60f2c535f9e7bad67e9d95a
- completed: - completed:
subdir: colonnade subdir: colonnade
cabal-file:
size: 2020
sha256: 28f603d097aee65ddf8fe032e7e0f87523a58c516253cba196922027c8fd54d5
name: colonnade name: colonnade
version: 1.2.0.2 version: 1.2.0.2
git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git
@ -151,9 +121,6 @@ packages:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git
commit: f8170266ab25b533576e96715bedffc5aa4f19fa commit: f8170266ab25b533576e96715bedffc5aa4f19fa
- completed: - completed:
cabal-file:
size: 9845
sha256: 674630347209bc5f7984e8e9d93293510489921f2d2d6092ad1c9b8c61b6560a
name: minio-hs name: minio-hs
version: 1.5.2 version: 1.5.2
git: git@gitlab2.rz.ifi.lmu.de:uni2work/minio-hs.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/minio-hs.git
@ -166,9 +133,6 @@ packages:
commit: 42103ab247057c04c8ce7a83d9d4c160713a3df1 commit: 42103ab247057c04c8ce7a83d9d4c160713a3df1
- completed: - completed:
subdir: cryptoids-class subdir: cryptoids-class
cabal-file:
size: 1155
sha256: 1fa96858ded816798f8e1c77d7945185c0d7ceb2536185d39fc72496da8a0125
name: cryptoids-class name: cryptoids-class
version: 0.0.0 version: 0.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
@ -182,9 +146,6 @@ packages:
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
- completed: - completed:
subdir: cryptoids-types subdir: cryptoids-types
cabal-file:
size: 1214
sha256: ee8966212554a156f2de236d4f005ff3a9d3098778ff6cc3f114ccaa0aff8825
name: cryptoids-types name: cryptoids-types
version: 1.0.0 version: 1.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
@ -198,9 +159,6 @@ packages:
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
- completed: - completed:
subdir: cryptoids subdir: cryptoids
cabal-file:
size: 1505
sha256: fcf07cd0dca21db976c25cbdf4dcc5c747cebcb7bf14c05804c8ae14223f6046
name: cryptoids name: cryptoids
version: 0.5.1.0 version: 0.5.1.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
@ -214,9 +172,6 @@ packages:
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
- completed: - completed:
subdir: filepath-crypto subdir: filepath-crypto
cabal-file:
size: 1716
sha256: 218da063bb7b00e3728deebf830904174b2b78bc29b3f203e6824b8caac92788
name: filepath-crypto name: filepath-crypto
version: 0.1.0.0 version: 0.1.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
@ -230,9 +185,6 @@ packages:
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
- completed: - completed:
subdir: uuid-crypto subdir: uuid-crypto
cabal-file:
size: 1460
sha256: 1db54db1b85303e50cec3c99ddb8de6c9bedc388fa9ce5a1fce61520023b9ee5
name: uuid-crypto name: uuid-crypto
version: 1.4.0.0 version: 1.4.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
@ -244,6 +196,43 @@ packages:
subdir: uuid-crypto subdir: uuid-crypto
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
- completed:
subdir: gearhash
name: gearhash
version: 0.0.0
git: https://github.com/gkleen/FastCDC.git
pantry-tree:
size: 504
sha256: 61a08cdd003dc8a418f410dacbcc3a91adea4c23864f8ddbaba3b762e4c2924d
commit: 7326e2931454282df9081105dad812845db5c530
original:
subdir: gearhash
git: https://github.com/gkleen/FastCDC.git
commit: 7326e2931454282df9081105dad812845db5c530
- completed:
subdir: fastcdc
name: fastcdc
version: 0.0.0
git: https://github.com/gkleen/FastCDC.git
pantry-tree:
size: 244
sha256: 80ae3d79344f7c6a73a8d7adf07ff2b7f0736fd34760b4b8a15e26f32d9773f6
commit: 7326e2931454282df9081105dad812845db5c530
original:
subdir: fastcdc
git: https://github.com/gkleen/FastCDC.git
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: 0da8bc38d73034962d2e2d1a7586b6dee848a629319fce9cbbf578348c61118c
commit: 843683d024f767de236f74d24a3348f69181a720
original:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/zip-stream.git
commit: 843683d024f767de236f74d24a3348f69181a720
- 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:
@ -363,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

@ -5,7 +5,6 @@ module Database.Fill
import "uniworx" Import hiding (Option(..), currentYear) import "uniworx" Import hiding (Option(..), currentYear)
import Handler.Utils.Form (SheetGrading'(..), SheetType'(..), SheetGroup'(..)) import Handler.Utils.Form (SheetGrading'(..), SheetType'(..), SheetGroup'(..))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text import qualified Data.Text as Text
-- import Data.Text.IO (hPutStrLn) -- import Data.Text.IO (hPutStrLn)
@ -30,6 +29,8 @@ import qualified Data.Csv as Csv
import Crypto.Random (getRandomBytes) import Crypto.Random (getRandomBytes)
import Data.List (genericLength) import Data.List (genericLength)
import qualified Data.Conduit.Combinators as C
testdataDir :: FilePath testdataDir :: FilePath
testdataDir = "testdata" testdataDir = "testdata"
@ -37,7 +38,7 @@ testdataDir = "testdata"
insertFile :: ( HasFileReference fRef, PersistRecordBackend fRef SqlBackend ) => FileReferenceResidual fRef -> FilePath -> DB (Key fRef) insertFile :: ( HasFileReference fRef, PersistRecordBackend fRef SqlBackend ) => FileReferenceResidual fRef -> FilePath -> DB (Key fRef)
insertFile residual fileTitle = do insertFile residual fileTitle = do
fileContent <- liftIO . fmap Just . BS.readFile $ testdataDir </> fileTitle let fileContent = Just . C.sourceFile $ testdataDir </> fileTitle
fileModified <- liftIO getCurrentTime fileModified <- liftIO getCurrentTime
sinkFile' File{..} residual >>= insert sinkFile' File{..} residual >>= insert

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

@ -2,10 +2,11 @@ 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 +15,59 @@ 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) (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