refactor(files): store content separately from metadata

This commit is contained in:
Gregor Kleen 2020-06-27 14:00:03 +02:00
parent a8b96a6f95
commit 14be8f61b4
62 changed files with 1070 additions and 576 deletions

View File

@ -333,6 +333,11 @@ SheetGeneratePseudonym: Generieren
SheetAnonymousCorrection: Anonymisierte Korrektur SheetAnonymousCorrection: Anonymisierte Korrektur
SheetAnonymousCorrectionTip: Wenn die Korrektur anonymisiert erfolgt, können Korrektoren die ihnen zugeteilten Abgaben nicht bestimmten Studierenden zuordnen (Name, Matrikelnummer und feste Abgabegruppe der Abgebenden werden versteckt) SheetAnonymousCorrectionTip: Wenn die Korrektur anonymisiert erfolgt, können Korrektoren die ihnen zugeteilten Abgaben nicht bestimmten Studierenden zuordnen (Name, Matrikelnummer und feste Abgabegruppe der Abgebenden werden versteckt)
SheetArchiveFileTypeDirectoryExercise: aufgabenstellung
SheetArchiveFileTypeDirectoryHint: hinweis
SheetArchiveFileTypeDirectorySolution: loesung
SheetArchiveFileTypeDirectoryMarking: korrektur
SheetFormType: Wertung & Abgabe SheetFormType: Wertung & Abgabe
SheetFormTimes: Zeiten SheetFormTimes: Zeiten
SheetFormFiles: Dateien SheetFormFiles: Dateien

12
minio-file-uploads.md Normal file
View File

@ -0,0 +1,12 @@
- `SessionFile` should only have `touched` and `content`; just there to keep `FileContent` alive
Store `fileTitle` and `fileModified` within actual session
Better symmetry with e.g. `SubmissionFile`
- Restrict `genericFileField` to not allow duplicate `fileTitle`s
Make `fieldAdditionalFiles` isomophoric to `[FileReference, FileFieldUserOption Bool]`
`type FileUploads = Map FileTitle (Maybe FileContentReference, UTCTime)` (`~ [FileReference]`)
- Route um Uploads nachzureichen
Cronjob, der `FileContent` aus MinIO füttert sollte panische E-Mail mit Link auf jene Route verschicken, wenn Datei nicht zur Hand ist

View File

@ -30,7 +30,7 @@ AllocationMatching
allocation AllocationId allocation AllocationId
fingerprint AllocationFingerprint fingerprint AllocationFingerprint
time UTCTime time UTCTime
log FileId log FileContentReference
AllocationCourse AllocationCourse
allocation AllocationId allocation AllocationId

View File

@ -5,3 +5,4 @@ TransactionLog
initiator UserId Maybe -- User associated with performing this action initiator UserId Maybe -- User associated with performing this action
remote IP Maybe -- Remote party that triggered this action via HTTP remote IP Maybe -- Remote party that triggered this action via HTTP
info Value -- JSON-encoded `Transaction` info Value -- JSON-encoded `Transaction`
deriving Eq Read Show Generic Typeable

View File

@ -35,9 +35,11 @@ CourseEvent
lastChanged UTCTime default=now() lastChanged UTCTime default=now()
CourseAppInstructionFile CourseAppInstructionFile
course CourseId course CourseId
file FileId title FilePath
UniqueCourseAppInstructionFile course file content FileContentReference Maybe
modified UTCTime
UniqueCourseAppInstructionFile course title
CourseEdit -- who edited when a row in table "Course", kept indefinitely (might be replaced by generic Audit Table; like all ...-Edit tables) CourseEdit -- who edited when a row in table "Course", kept indefinitely (might be replaced by generic Audit Table; like all ...-Edit tables)
user UserId user UserId

View File

@ -10,7 +10,10 @@ CourseApplication
allocationPriority Natural Maybe allocationPriority Natural Maybe
time UTCTime default=now() time UTCTime default=now()
ratingTime UTCTime Maybe ratingTime UTCTime Maybe
CourseApplicationFile CourseApplicationFile
application CourseApplicationId application CourseApplicationId
file FileId title FilePath
UniqueApplicationFile application file content FileContentReference Maybe
modified UTCTime
UniqueCourseApplicationFile application title

View File

@ -9,5 +9,7 @@ Material -- course material for disemination to course participants
deriving Generic deriving Generic
MaterialFile -- a file that is part of a material distribution MaterialFile -- a file that is part of a material distribution
material MaterialId material MaterialId
file FileId title FilePath
UniqueMaterialFile material file content FileContentReference Maybe
modified UTCTime
UniqueMaterialFile material title

View File

@ -7,6 +7,8 @@ CourseNews
summary Html Maybe summary Html Maybe
lastEdit UTCTime lastEdit UTCTime
CourseNewsFile CourseNewsFile
news CourseNewsId news CourseNewsId
file FileId title FilePath
UniqueCourseNewsFile news file content FileContentReference Maybe
modified UTCTime
UniqueCourseNewsFile news title

View File

@ -1,12 +1,8 @@
-- Table storing all kinds of larges files as 8bit-byte vectors (regardless of encoding) FileContent
-- PostgreSQL is intelligent enough to handle this in a sensible manner; hash FileContentReference
-- helps to ensure consistency of database snapshots, no data is stored outside database content ByteString
File Primary hash
title FilePath
content ByteString Maybe -- Nothing iff this is a directory
modified UTCTime
deriving Show Eq Generic
SessionFile SessionFile
file FileId content FileContentReference Maybe
touched UTCTime touched UTCTime

View File

@ -37,7 +37,9 @@ SheetCorrector -- grant corrector role to user for a sheet
UniqueSheetCorrector user sheet UniqueSheetCorrector user sheet
deriving Show Eq Ord deriving Show Eq Ord
SheetFile -- a file that is part of an exercise sheet SheetFile -- a file that is part of an exercise sheet
sheet SheetId sheet SheetId
file FileId type SheetFileType -- excercise, marking, hint or solution
type SheetFileType -- excercise, marking, hint or solution title FilePath
UniqueSheetFile file sheet type content FileContentReference Maybe
modified UTCTime
UniqueSheetFile sheet type title

View File

@ -11,11 +11,13 @@ SubmissionEdit -- user uploads new version of their submissio
time UTCTime time UTCTime
submission SubmissionId submission SubmissionId
SubmissionFile -- files that are part of a submission SubmissionFile -- files that are part of a submission
submission SubmissionId submission SubmissionId
file FileId title FilePath
isUpdate Bool -- is this the file updated by a corrector (original will always be retained) content FileContentReference Maybe
isDeletion Bool -- only set if isUpdate is also set, but file was deleted by corrector modified UTCTime
UniqueSubmissionFile file submission isUpdate isUpdate Bool -- is this the file updated by a corrector (original will always be retained)
isDeletion Bool -- only set if isUpdate is also set, but file was deleted by corrector
UniqueSubmissionFile submission title isUpdate
deriving Show deriving Show
SubmissionUser -- which submission belongs to whom SubmissionUser -- which submission belongs to whom
user UserId user UserId

View File

@ -24,6 +24,8 @@ import qualified Net.IPv6 as IPv6
import Control.Exception (ErrorCall(..)) import Control.Exception (ErrorCall(..))
import GHC.Stack
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-} {-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
@ -86,6 +88,7 @@ audit :: ( AuthId (HandlerSite m) ~ Key User
, MonadHandler m , MonadHandler m
, MonadCatch m , MonadCatch m
, HasAppSettings (HandlerSite m) , HasAppSettings (HandlerSite m)
, HasCallStack
) )
=> Transaction -- ^ Transaction to record => Transaction -- ^ Transaction to record
-> ReaderT (YesodPersistBackend (HandlerSite m)) m () -> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
@ -94,7 +97,7 @@ audit :: ( AuthId (HandlerSite m) ~ Key User
-- - `transactionLogTime` is now -- - `transactionLogTime` is now
-- - `transactionLogInitiator` is currently logged in user (or none) -- - `transactionLogInitiator` is currently logged in user (or none)
-- - `transactionLogRemote` is determined from current HTTP-Request -- - `transactionLogRemote` is determined from current HTTP-Request
audit (toJSON -> transactionLogInfo) = do audit transaction@(toJSON -> transactionLogInfo) = do
transactionLogTime <- liftIO getCurrentTime transactionLogTime <- liftIO getCurrentTime
transactionLogInstance <- getsYesod $ view instanceID transactionLogInstance <- getsYesod $ view instanceID
@ -102,3 +105,5 @@ audit (toJSON -> transactionLogInfo) = do
transactionLogRemote <- handle (throwM . AuditRemoteException) $ Just <$> getRemote transactionLogRemote <- handle (throwM . AuditRemoteException) $ Just <$> getRemote
insert_ TransactionLog{..} insert_ TransactionLog{..}
$logInfoS "Audit" $ tshow (transaction, transactionLogInitiator, transactionLogRemote) <> "\n" <> pack (prettyCallStack callStack)

View File

@ -92,28 +92,10 @@ data Transaction
| TransactionSubmissionFileEdit | TransactionSubmissionFileEdit
{ transactionSubmissionFile :: SubmissionFileId { transactionSubmissionFile :: SubmissionFileId
, transactionSubmission :: SubmissionId , transactionSubmission :: SubmissionId
, transactionFile :: FileId
} }
| TransactionSubmissionFileDelete | TransactionSubmissionFileDelete
{ transactionSubmissionFile :: SubmissionFileId { transactionSubmissionFile :: SubmissionFileId
, transactionSubmission :: SubmissionId , transactionSubmission :: SubmissionId
, transactionFile :: FileId
}
-- TODO: not yet audited
| TransactionUserEdit
{ transactionUser :: UserId
}
| TransactionUserDelete
{ transactionUser :: UserId
}
-- TODO: not yet audited
| TransactionFileEdit
{ transactionFile :: FileId
}
| TransactionFileDelete
{ transactionFile :: FileId
} }
| TransactionExamOfficeUserAdd | TransactionExamOfficeUserAdd

View File

@ -58,7 +58,6 @@ instance {-# OVERLAPPING #-} MonadThrow m => MonadCrypto (ReaderT CryptoIDKey m)
-- Generates CryptoUUID... and CryptoFileName... Datatypes -- Generates CryptoUUID... and CryptoFileName... Datatypes
decCryptoIDs [ ''SubmissionId decCryptoIDs [ ''SubmissionId
, ''FileId
, ''UserId , ''UserId
, ''SheetId , ''SheetId
, ''SystemMessageId , ''SystemMessageId

View File

@ -22,6 +22,7 @@ module Database.Esqueleto.Utils
, (->.) , (->.)
, fromSqlKey , fromSqlKey
, selectCountRows , selectCountRows
, selectMaybe
, module Database.Esqueleto.Utils.TH , module Database.Esqueleto.Utils.TH
) where ) where
@ -271,3 +272,6 @@ selectCountRows q = do
-> return res' -> return res'
_other _other
-> error "E.countRows did not return exactly one result" -> error "E.countRows did not return exactly one result"
selectMaybe :: (E.SqlSelect a r, MonadIO m) => E.SqlQuery a -> E.SqlReadT m (Maybe r)
selectMaybe = fmap listToMaybe . E.select . (<* E.limit 1)

View File

@ -7,6 +7,7 @@ module Foundation.I18n
, MsgLanguage(..) , MsgLanguage(..)
, ShortSex(..) , ShortSex(..)
, SheetTypeHeader(..) , SheetTypeHeader(..)
, SheetArchiveFileTypeDirectory(..)
, ShortStudyDegree(..) , ShortStudyDegree(..)
, ShortStudyTerms(..) , ShortStudyTerms(..)
, StudyDegreeTerm(..) , StudyDegreeTerm(..)
@ -212,6 +213,9 @@ embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>)
newtype SheetTypeHeader = SheetTypeHeader SheetType newtype SheetTypeHeader = SheetTypeHeader SheetType
embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>) embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>)
newtype SheetArchiveFileTypeDirectory = SheetArchiveFileTypeDirectory SheetFileType
embedRenderMessageVariant ''UniWorX ''SheetArchiveFileTypeDirectory $ ("SheetArchiveFileTypeDirectory" <>) . concat . drop 1 . splitCamel
instance RenderMessage UniWorX SheetType where instance RenderMessage UniWorX SheetType where
renderMessage foundation ls sheetType = case sheetType of renderMessage foundation ls sheetType = case sheetType of
NotGraded -> mr $ SheetTypeHeader NotGraded NotGraded -> mr $ SheetTypeHeader NotGraded

View File

@ -10,7 +10,6 @@ import qualified Crypto.Random as Random
import qualified Data.ByteString.Base64.URL as Base64 import qualified Data.ByteString.Base64.URL as Base64
import qualified Data.Conduit.List as C (mapMaybe)
import qualified Data.Conduit.Combinators as C import qualified Data.Conduit.Combinators as C
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
@ -81,13 +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.mapMaybe ((fmap length $!!) . fileContent . entityVal) .| C.map ((length $!!) . fileContentContent . entityVal)
.| takeLimit dlMaxSize .| takeLimit dlMaxSize
where where
sourceDBFiles = E.selectSource . E.from $ \file -> do sourceDBFiles = E.selectSource . E.from $ \fileContent -> do
E.orderBy [E.asc $ E.random_ @Int64] E.orderBy [E.asc $ E.random_ @Int64]
E.where_ . E.not_ . E.isNothing $ file E.^. FileContent return fileContent
return file
takeLimit n | n <= 0 = return () takeLimit n | n <= 0 = return ()
takeLimit n = do takeLimit n = do

View File

@ -15,12 +15,9 @@ import qualified Data.Text as Text
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import Crypto.Hash (hash)
import Control.Monad.Trans.State (execStateT) import Control.Monad.Trans.State (execStateT)
import Control.Monad.State.Class (modify) import Control.Monad.State.Class (modify)
@ -290,13 +287,8 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do
, courseApplicationTime = now , courseApplicationTime = now
, courseApplicationRatingTime = guardOn rated now , courseApplicationRatingTime = guardOn rated now
} }
let
sinkFile' (Right file) = runConduit $ transPipe liftHandler (traverse_ id afFiles) .| C.mapM_ (insert_ . review _FileReference . (, CourseApplicationFileResidual appId))
insert file >>= sinkFile' . Left
sinkFile' (Left fId) =
insert_ $ CourseApplicationFile appId fId
forM_ afFiles $ \afFiles' ->
runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile'
audit $ TransactionCourseApplicationEdit cid uid appId audit $ TransactionCourseApplicationEdit cid uid appId
addMessageI Success $ MsgCourseApplicationCreated courseShorthand addMessageI Success $ MsgCourseApplicationCreated courseShorthand
| is _BtnAllocationApplicationEdit afAction || is _BtnAllocationApplicationRate afAction | is _BtnAllocationApplicationEdit afAction || is _BtnAllocationApplicationRate afAction
@ -307,33 +299,15 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do
changes <- if changes <- if
| afmApplicantEdit afMode -> do | afmApplicantEdit afMode -> do
oldFiles <- Set.fromList . map (courseApplicationFileFile . entityVal) <$> selectList [CourseApplicationFileApplication ==. appId] [] oldFiles <- Set.fromList . map (courseApplicationFileTitle . entityVal) <$> selectList [CourseApplicationFileApplication ==. appId] []
changes <- flip execStateT oldFiles . forM_ afFiles $ \afFiles' -> changes <- flip execStateT oldFiles . forM_ afFiles $ \afFiles' ->
let sinkFile' (Right file) = do let sinkAppFile fRef@FileReference{..}
oldFiles' <- lift . E.select . E.from $ \(courseApplicationFile `E.InnerJoin` file') -> do | fileReferenceTitle `Set.member` oldFiles = modify $ Set.delete fileReferenceTitle
E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file' E.^. FileId
E.where_ $ file' E.^. FileTitle E.==. E.val (fileTitle file)
E.&&. E.maybe
(E.val . is _Nothing $ fileContent file)
(\fc' -> maybe E.false (\fc -> E.sha256 fc' E.==. E.val (hash fc)) $ fileContent file)
(file' E.^. FileContent)
E.&&. file' E.^. FileId `E.in_` E.valList (Set.toList oldFiles)
return $ file' E.^. FileId
if
| [E.Value oldFileId] <- oldFiles'
-> modify $ Set.delete oldFileId
| otherwise
-> do
fId <- lift $ insert file
lift . insert_ $ CourseApplicationFile appId fId
modify $ Set.insert fId
sinkFile' (Left fId)
| fId `Set.member` oldFiles = modify $ Set.delete fId
| otherwise = do | otherwise = do
lift . insert_ $ CourseApplicationFile appId fId lift . insert_ $ _FileReference # (fRef, CourseApplicationFileResidual appId)
modify $ Set.insert fId modify $ Set.insert fileReferenceTitle
in runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile' in runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkAppFile
deleteCascadeWhere [ FileId <-. Set.toList (oldFiles `Set.intersection` changes) ] deleteWhere [ CourseApplicationFileApplication ==. appId, CourseApplicationFileTitle <-. Set.toList (oldFiles `Set.intersection` changes) ]
return changes return changes
| otherwise | otherwise
-> return Set.empty -> return Set.empty

View File

@ -31,10 +31,9 @@ getCAFilesR tid ssh csh cID = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCourseApplicationArchiveName tid ssh csh cID userDisplayName archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCourseApplicationArchiveName tid ssh csh cID userDisplayName
let let
fsSource = E.selectSource . E.from $ \(courseApplicationFile `E.InnerJoin` file) -> do fsSource = E.selectSource . E.from $ \courseApplicationFile -> do
E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file E.^. FileId
E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. E.val appId E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. E.val appId
return file return courseApplicationFile
serveSomeFiles archiveName $ fsSource .| C.map entityVal serveSomeFiles archiveName $ fsSource .| C.map entityVal
@ -47,7 +46,7 @@ getCAppsFilesR tid ssh csh = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCourseAllApplicationsArchiveName tid ssh csh archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCourseAllApplicationsArchiveName tid ssh csh
let let
fsSource :: ConduitT () File DB () fsSource :: ConduitT () CourseApplicationFile DB ()
fsSource = do fsSource = do
apps <- lift . E.select . E.from $ \((course `E.InnerJoin` courseApplication `E.InnerJoin` user) `E.LeftOuterJoin` allocation) -> do apps <- lift . E.select . E.from $ \((course `E.InnerJoin` courseApplication `E.InnerJoin` user) `E.LeftOuterJoin` allocation) -> do
E.on $ allocation E.?. AllocationId E.==. courseApplication E.^. CourseApplicationAllocation E.on $ allocation E.?. AllocationId E.==. courseApplication E.^. CourseApplicationAllocation
@ -90,19 +89,19 @@ getCAppsFilesR tid ssh csh = do
forM_ apps' $ \(mbAlloc, Entity _ User{..}, Entity appId CourseApplication{..}) -> do forM_ apps' $ \(mbAlloc, Entity _ User{..}, Entity appId CourseApplication{..}) -> do
cID <- cachedByBinary appId $ encrypt appId :: _ CryptoFileNameCourseApplication cID <- cachedByBinary appId $ encrypt appId :: _ CryptoFileNameCourseApplication
let mkAppDir = mkAllocationDir (entityVal <$> mbAlloc) . (</>) (unpack [st|#{CI.foldedCase $ ciphertext cID}-#{CI.foldCase userSurname}|]) let mkAppDir = mkAllocationDir (entityVal <$> mbAlloc) . (</>) (unpack [st|#{CI.foldedCase $ ciphertext cID}-#{CI.foldCase userSurname}|])
dirFiles = C.map $ over _fileTitle mkAppDir . entityVal fileEntitySource = E.selectSource . E.from $ \courseApplicationFile -> do
fileEntitySource = E.selectSource . E.from $ \(courseApplicationFile `E.InnerJoin` file) -> do
E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file E.^. FileId
E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. E.val appId E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. E.val appId
return file return courseApplicationFile
yield File yield $ _FileReference # ( FileReference
{ fileModified = courseApplicationTime { fileReferenceModified = courseApplicationTime
, fileTitle = mkAppDir "" , fileReferenceTitle = mkAppDir ""
, fileContent = Nothing , fileReferenceContent = Nothing
} }
, CourseApplicationFileResidual appId
)
fileEntitySource .| dirFiles fileEntitySource .| C.map (view _entityVal) .| C.map (over (_FileReference . _1 . _fileReferenceTitle) mkAppDir)
serveSomeFiles archiveName fsSource serveSomeFiles archiveName fsSource

View File

@ -40,7 +40,7 @@ data CourseForm = CourseForm
, cfAllocation :: Maybe AllocationCourseForm , cfAllocation :: Maybe AllocationCourseForm
, cfAppRequired :: Bool , cfAppRequired :: Bool
, cfAppInstructions :: Maybe Html , cfAppInstructions :: Maybe Html
, cfAppInstructionFiles :: Maybe (ConduitT () (Either FileId File) Handler ()) , cfAppInstructionFiles :: Maybe FileUploads
, cfAppText :: Bool , cfAppText :: Bool
, cfAppFiles :: UploadMode , cfAppFiles :: UploadMode
, cfAppRatingsVisible :: Bool , cfAppRatingsVisible :: Bool
@ -84,10 +84,10 @@ courseToForm cEnt@(Entity cid Course{..}) lecs lecInvites alloc = CourseForm
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- Map.toList lecInvites ] ++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- Map.toList lecInvites ]
} }
where where
cfAppInstructionFiles = Just . transPipe runDB $ selectAppFiles .| C.map (Left . E.unValue) cfAppInstructionFiles = Just . transPipe runDB $ selectAppFiles .| C.map (view $ _entityVal . _FileReference . _1)
where selectAppFiles = E.selectSource . E.from $ \courseAppInstructionFile -> do where selectAppFiles = E.selectSource . E.from $ \courseAppInstructionFile -> do
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. E.val cid E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. E.val cid
return $ courseAppInstructionFile E.^. CourseAppInstructionFileFile return courseAppInstructionFile
allocationCourseToForm :: Entity Course -> Entity AllocationCourse -> AllocationCourseForm allocationCourseToForm :: Entity Course -> Entity AllocationCourse -> AllocationCourseForm
@ -521,14 +521,14 @@ courseEditHandler miButtonAction mbCourseForm = do
insert_ $ CourseEdit aid now cid insert_ $ CourseEdit aid now cid
let let
finsert val = do finsert fRef@FileReference{..} = do
fId <- lift $ either return insert val tell $ Set.singleton fileReferenceTitle
tell $ Set.singleton fId void . lift $ upsertBy (UniqueCourseAppInstructionFile cid fileReferenceTitle) (_FileReference # (fRef, CourseAppInstructionFileResidual cid))
lift $ [ CourseAppInstructionFileModified =. fileReferenceModified
void . insertUnique $ CourseAppInstructionFile cid fId , CourseAppInstructionFileContent =. fileReferenceContent
]
keep <- execWriterT . runConduit $ transPipe liftHandler (traverse_ id $ cfAppInstructionFiles res) .| C.mapM_ finsert keep <- execWriterT . runConduit $ transPipe liftHandler (traverse_ id $ cfAppInstructionFiles res) .| C.mapM_ finsert
acfs <- selectList [ CourseAppInstructionFileCourse ==. cid, CourseAppInstructionFileFile /<-. Set.toList keep ] [] deleteWhere [ CourseAppInstructionFileCourse ==. cid, CourseAppInstructionFileTitle /<-. Set.toList keep ]
mapM_ deleteCascade $ map (courseAppInstructionFileFile . entityVal) acfs
upsertAllocationCourse cid $ cfAllocation res upsertAllocationCourse cid $ cfAllocation res

View File

@ -19,10 +19,9 @@ getCNArchiveR tid ssh csh cID = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgCourseNewsArchiveName tid ssh csh (fromMaybe (toPathPiece courseNewsLastEdit) courseNewsTitle) archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgCourseNewsArchiveName tid ssh csh (fromMaybe (toPathPiece courseNewsLastEdit) courseNewsTitle)
let getFilesQuery = (.| C.map entityVal) . E.selectSource . E.from $ let getFilesQuery = (.| C.map entityVal) . E.selectSource . E.from $
\(newsFile `E.InnerJoin` file) -> do \newsFile -> do
E.on $ file E.^. FileId E.==. newsFile E.^. CourseNewsFileFile
E.where_ $ newsFile E.^. CourseNewsFileNews E.==. E.val nId E.where_ $ newsFile E.^. CourseNewsFileNews E.==. E.val nId
return file return newsFile
serveSomeFiles archiveName getFilesQuery serveSomeFiles archiveName getFilesQuery
@ -32,10 +31,9 @@ getCNFileR _ _ _ cID title = do
nId <- decrypt cID nId <- decrypt cID
let let
fileQuery = E.selectSource . E.from $ \(newsFile `E.InnerJoin` file) -> do fileQuery = E.selectSource . E.from $ \newsFile -> do
E.on $ newsFile E.^. CourseNewsFileFile E.==. file E.^. FileId E.where_ $ newsFile E.^. CourseNewsFileNews E.==. E.val nId
E.where_ $ newsFile E.^. CourseNewsFileNews E.==. E.val nId E.&&. newsFile E.^. CourseNewsFileTitle E.==. E.val title
E.&&. file E.^. FileTitle E.==. E.val title return newsFile
return file
serveOneFile $ fileQuery .| C.map entityVal serveOneFile $ fileQuery .| C.map entityVal

View File

@ -20,7 +20,7 @@ postCNEditR tid ssh csh cID = do
courseNews <- get404 nId courseNews <- get404 nId
cnfs <- selectList [CourseNewsFileNews ==. nId] [] cnfs <- selectList [CourseNewsFileNews ==. nId] []
return ( courseNews return ( courseNews
, setOf (folded . _entityVal . _courseNewsFileFile) cnfs , cnfs ^.. folded . _entityVal . _FileReference . _1
) )
((newsRes, newsWgt'), newsEnctype) <- runFormPost . courseNewsForm . Just $ courseNewsToForm courseNews fids ((newsRes, newsWgt'), newsEnctype) <- runFormPost . courseNewsForm . Just $ courseNewsToForm courseNews fids
@ -38,10 +38,12 @@ postCNEditR tid ssh csh cID = do
, courseNewsLastEdit = now , courseNewsLastEdit = now
} }
let let
insertFile (Left fId) = fId <$ upsertBy (UniqueCourseNewsFile nId fId) (CourseNewsFile nId fId) [] insertFile fRef@FileReference{..} = fileReferenceTitle <$ upsertBy (UniqueCourseNewsFile nId fileReferenceTitle) (_FileReference # (fRef, CourseNewsFileResidual nId))
insertFile (Right f ) = insert f >>= \fId -> fId <$ insert_ (CourseNewsFile nId fId) [ CourseNewsFileModified =. fileReferenceModified
newFids <- runConduit $ transPipe lift (fromMaybe (return ()) cnfFiles) .| C.mapM insertFile .| C.foldMap Set.singleton , CourseNewsFileContent =. fileReferenceContent
deleteWhere [ CourseNewsFileNews ==. nId, CourseNewsFileFile /<-. Set.toList newFids ] ]
newTitles <- runConduit $ transPipe lift (fromMaybe (return ()) cnfFiles) .| C.mapM insertFile .| C.foldMap Set.singleton
deleteWhere [ CourseNewsFileNews ==. nId, CourseNewsFileTitle /<-. Set.toList newTitles ]
addMessageI Success MsgCourseNewsEdited addMessageI Success MsgCourseNewsEdited
redirect $ CourseR tid ssh csh CShowR :#: [st|news-#{toPathPiece cID}|] redirect $ CourseR tid ssh csh CShowR :#: [st|news-#{toPathPiece cID}|]

View File

@ -9,8 +9,6 @@ import Handler.Utils
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import qualified Data.Set as Set
data CourseNewsForm = CourseNewsForm data CourseNewsForm = CourseNewsForm
{ cnfTitle :: Maybe Text { cnfTitle :: Maybe Text
@ -18,14 +16,14 @@ data CourseNewsForm = CourseNewsForm
, cnfContent :: Html , cnfContent :: Html
, cnfParticipantsOnly :: Bool , cnfParticipantsOnly :: Bool
, cnfVisibleFrom :: Maybe UTCTime , cnfVisibleFrom :: Maybe UTCTime
, cnfFiles :: Maybe (ConduitT () (Either FileId File) Handler ()) , cnfFiles :: Maybe FileUploads
} }
courseNewsForm :: Maybe CourseNewsForm -> Form CourseNewsForm courseNewsForm :: Maybe CourseNewsForm -> Form CourseNewsForm
courseNewsForm template = identifyForm FIDCourseNews . renderWForm FormStandard $ do courseNewsForm template = identifyForm FIDCourseNews . renderWForm FormStandard $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let oldFileIds = maybe (return mempty) (\s -> runConduit $ s .| C.foldMap (either opoint $ const mempty)) $ template >>= cnfFiles let oldFileIds = fromMaybe (return ()) $ template >>= cnfFiles
cTime = ceilingQuarterHour now cTime = ceilingQuarterHour now
visibleFromTip visibleFromTip
| Just vFrom <- template >>= cnfVisibleFrom | Just vFrom <- template >>= cnfVisibleFrom
@ -48,7 +46,7 @@ courseNewsForm template = identifyForm FIDCourseNews . renderWForm FormStandard
(cnfContent <$> template) (cnfContent <$> template)
cnfParticipantsOnly' <- wpopt checkBoxField (fslI MsgCourseNewsParticipantsOnly) (cnfParticipantsOnly <$> template) cnfParticipantsOnly' <- wpopt checkBoxField (fslI MsgCourseNewsParticipantsOnly) (cnfParticipantsOnly <$> template)
cnfVisibleFrom' <- wopt utcTimeField (fslI MsgCourseNewsVisibleFrom & setTooltip visibleFromTip) (cnfVisibleFrom <$> template <|> Just (Just cTime)) cnfVisibleFrom' <- wopt utcTimeField (fslI MsgCourseNewsVisibleFrom & setTooltip visibleFromTip) (cnfVisibleFrom <$> template <|> Just (Just cTime))
cnfFiles' <- wopt (multiFileField oldFileIds) (fslI MsgCourseNewsFiles) (cnfFiles <$> template) cnfFiles' <- wopt (multiFileField' oldFileIds) (fslI MsgCourseNewsFiles) (cnfFiles <$> template)
return $ CourseNewsForm return $ CourseNewsForm
<$> cnfTitle' <$> cnfTitle'
@ -58,12 +56,12 @@ courseNewsForm template = identifyForm FIDCourseNews . renderWForm FormStandard
<*> cnfVisibleFrom' <*> cnfVisibleFrom'
<*> cnfFiles' <*> cnfFiles'
courseNewsToForm :: CourseNews -> Set FileId -> CourseNewsForm courseNewsToForm :: CourseNews -> [FileReference] -> CourseNewsForm
courseNewsToForm CourseNews{..} fs = CourseNewsForm courseNewsToForm CourseNews{..} fs = CourseNewsForm
{ cnfTitle = courseNewsTitle { cnfTitle = courseNewsTitle
, cnfSummary = courseNewsSummary , cnfSummary = courseNewsSummary
, cnfContent = courseNewsContent , cnfContent = courseNewsContent
, cnfParticipantsOnly = courseNewsParticipantsOnly , cnfParticipantsOnly = courseNewsParticipantsOnly
, cnfVisibleFrom = courseNewsVisibleFrom , cnfVisibleFrom = courseNewsVisibleFrom
, cnfFiles = guardOn (not $ Set.null fs) $ C.sourceList (Left <$> Set.toList fs) , cnfFiles = guardOn (not $ null fs) $ C.sourceList fs
} }

View File

@ -30,8 +30,7 @@ postCNewsNewR tid ssh csh = do
, courseNewsLastEdit = now , courseNewsLastEdit = now
} }
let let
insertFile (Left fId) = insert_ $ CourseNewsFile nId fId insertFile = insert_ . review _FileReference . (, CourseNewsFileResidual nId)
insertFile (Right f ) = insert_ . CourseNewsFile nId =<< insert f
forM_ cnfFiles $ \fSource -> forM_ cnfFiles $ \fSource ->
runConduit $ transPipe lift fSource .| C.mapM_ insertFile runConduit $ transPipe lift fSource .| C.mapM_ insertFile
encrypt nId :: DB CryptoUUIDCourseNews encrypt nId :: DB CryptoUUIDCourseNews

View File

@ -195,7 +195,7 @@ postCRegisterR tid ssh csh = do
whenIsJust appRes $ whenIsJust appRes $
audit . TransactionCourseApplicationEdit cid uid audit . TransactionCourseApplicationEdit cid uid
whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do
runConduit $ transPipe liftHandler fSource .| C.mapM_ (insert_ . CourseApplicationFile appId <=< either return insert) runConduit $ transPipe liftHandler fSource .| C.mapM_ (insert_ . review _FileReference . (, CourseApplicationFileResidual appId))
return appRes return appRes
| otherwise | otherwise
= return $ Just () = return $ Just ()
@ -256,9 +256,7 @@ deleteApplications uid cid = do
audit $ TransactionCourseApplicationDeleted cid uid appId audit $ TransactionCourseApplicationDeleted cid uid appId
deleteApplicationFiles :: CourseApplicationId -> DB () deleteApplicationFiles :: CourseApplicationId -> DB ()
deleteApplicationFiles appId = do deleteApplicationFiles appId = deleteWhere [ CourseApplicationFileApplication ==. appId ]
fs <- selectList [ CourseApplicationFileApplication ==. appId ] []
deleteCascadeWhere [ FileId <-. map (courseApplicationFileFile . entityVal) fs ]
deregisterParticipant :: UserId -> CourseId -> DB () deregisterParticipant :: UserId -> CourseId -> DB ()
deregisterParticipant uid cid = do deregisterParticipant uid cid = do

View File

@ -79,10 +79,9 @@ getCShowR tid ssh csh = do
cID <- encrypt nId :: MaybeT (MaybeT DB) CryptoUUIDCourseNews cID <- encrypt nId :: MaybeT (MaybeT DB) CryptoUUIDCourseNews
guardM . hasReadAccessTo $ CNewsR tid ssh csh cID CNShowR guardM . hasReadAccessTo $ CNewsR tid ssh csh cID CNShowR
let visible = cTime >= NTop courseNewsVisibleFrom let visible = cTime >= NTop courseNewsVisibleFrom
files' <- lift . lift . E.select . E.from $ \(newsFile `E.InnerJoin` file) -> do files' <- lift . lift . E.select . E.from $ \newsFile -> do
E.on $ file E.^. FileId E.==. newsFile E.^. CourseNewsFileFile
E.where_ $ newsFile E.^. CourseNewsFileNews E.==. E.val nId E.where_ $ newsFile E.^. CourseNewsFileNews E.==. E.val nId
return (E.isNothing $ file E.^. FileContent, file E.^. FileTitle) return (E.isNothing $ newsFile E.^. CourseNewsFileContent, newsFile E.^. CourseNewsFileTitle)
let files = files' let files = files'
& over (mapped . _1) E.unValue & over (mapped . _1) E.unValue
& over (mapped . _2) E.unValue & over (mapped . _2) E.unValue
@ -228,11 +227,10 @@ getCShowR tid ssh csh = do
getCRegisterTemplateR :: TermId -> SchoolId -> CourseShorthand -> Handler TypedContent getCRegisterTemplateR :: TermId -> SchoolId -> CourseShorthand -> Handler TypedContent
getCRegisterTemplateR tid ssh csh = do getCRegisterTemplateR tid ssh csh = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgCourseApplicationTemplateArchiveName tid ssh csh archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgCourseApplicationTemplateArchiveName tid ssh csh
let source = (.| C.map entityVal) . E.selectSource . E.from $ \(file `E.InnerJoin` courseAppInstructionFile `E.InnerJoin` course) -> do let source = (.| C.map entityVal) . E.selectSource . E.from $ \(courseAppInstructionFile `E.InnerJoin` course) -> do
E.on $ course E.^. CourseId E.==. courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.on $ course E.^. CourseId E.==. courseAppInstructionFile E.^. CourseAppInstructionFileCourse
E.on $ courseAppInstructionFile E.^. CourseAppInstructionFileFile E.==. file E.^. FileId
E.where_ $ course E.^. CourseTerm E.==. E.val tid E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
return file return courseAppInstructionFile
serveSomeFiles archiveName source serveSomeFiles archiveName source

View File

@ -22,19 +22,13 @@ data MaterialForm = MaterialForm
, mfType :: Maybe (CI Text) , mfType :: Maybe (CI Text)
, mfDescription :: Maybe Html , mfDescription :: Maybe Html
, mfVisibleFrom :: Maybe UTCTime , mfVisibleFrom :: Maybe UTCTime
, mfFiles :: Maybe (ConduitT () (Either FileId File) Handler ()) , mfFiles :: Maybe FileUploads
} }
makeMaterialForm :: CourseId -> Maybe MaterialForm -> Form MaterialForm makeMaterialForm :: CourseId -> Maybe MaterialForm -> Form MaterialForm
makeMaterialForm cid template = identifyForm FIDmaterial $ \html -> do makeMaterialForm cid template = identifyForm FIDmaterial $ \html -> do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
let setIds :: Either FileId File -> Set FileId let typeOptions :: HandlerFor UniWorX (OptionList (CI Text))
setIds = either Set.singleton $ const Set.empty
oldFileIds
| Just source <- template >>= mfFiles
= runConduit $ source .| C.foldMap setIds
| otherwise = return Set.empty
typeOptions :: HandlerFor UniWorX (OptionList (CI Text))
typeOptions = do typeOptions = do
let defaults = Set.fromList $ map (CI.mk . mr) [MsgMaterialTypeSlides,MsgMaterialTypeCode,MsgMaterialTypeExample] let defaults = Set.fromList $ map (CI.mk . mr) [MsgMaterialTypeSlides,MsgMaterialTypeCode,MsgMaterialTypeExample]
previouslyUsed <- runDB $ previouslyUsed <- runDB $
@ -62,7 +56,7 @@ makeMaterialForm cid template = identifyForm FIDmaterial $ \html -> do
(mfDescription <$> template) (mfDescription <$> template)
<*> aopt utcTimeField (fslI MsgMaterialVisibleFrom & setTooltip visibleToolTip) <*> aopt utcTimeField (fslI MsgMaterialVisibleFrom & setTooltip visibleToolTip)
((mfVisibleFrom <$> template) <|> pure (Just ctime)) ((mfVisibleFrom <$> template) <|> pure (Just ctime))
<*> aopt (multiFileField oldFileIds) <*> aopt (multiFileField' . fromMaybe (return ()) $ mfFiles =<< template)
(fslI MsgMaterialFiles) (mfFiles <$> template) (fslI MsgMaterialFiles) (mfFiles <$> template)
getMaterialKeyBy404 :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> DB (Key Material) getMaterialKeyBy404 :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> DB (Key Material)
@ -163,20 +157,19 @@ getMFileR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> FilePath -
getMFileR tid ssh csh mnm title = serveOneFile $ fileQuery .| C.map entityVal getMFileR tid ssh csh mnm title = serveOneFile $ fileQuery .| C.map entityVal
where where
fileQuery = E.selectSource $ E.from $ fileQuery = E.selectSource $ E.from $
\(course `E.InnerJoin` material `E.InnerJoin` matFile `E.InnerJoin` file) -> do \(course `E.InnerJoin` material `E.InnerJoin` matFile) -> do
-- Restrict to consistent rows that correspond to each other -- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. matFile E.^. MaterialFileFile)
E.on (matFile E.^. MaterialFileMaterial E.==. material E.^. MaterialId) E.on (matFile E.^. MaterialFileMaterial E.==. material E.^. MaterialId)
E.on (material E.^. MaterialCourse E.==. course E.^. CourseId) E.on (material E.^. MaterialCourse E.==. course E.^. CourseId)
-- filter to requested file -- filter to requested file
E.where_ ((file E.^. FileTitle E.==. E.val title) E.where_ ((matFile E.^. MaterialFileTitle E.==. E.val title)
E.&&. (material E.^. MaterialName E.==. E.val mnm ) E.&&. (material E.^. MaterialName E.==. E.val mnm )
E.&&. (course E.^. CourseShorthand E.==. E.val csh ) E.&&. (course E.^. CourseShorthand E.==. E.val csh )
E.&&. (course E.^. CourseSchool E.==. E.val ssh ) E.&&. (course E.^. CourseSchool E.==. E.val ssh )
E.&&. (course E.^. CourseTerm E.==. E.val tid ) E.&&. (course E.^. CourseTerm E.==. E.val tid )
) )
-- return file entity -- return file entity
return file return matFile
getMShowR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> Handler Html getMShowR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> Handler Html
getMShowR tid ssh csh mnm = do getMShowR tid ssh csh mnm = do
@ -197,12 +190,11 @@ getMShowR tid ssh csh mnm = do
else colFileModificationWhen $ \t -> NTop (Just t) > NTop (materialVisibleFrom $ entityVal matEnt) else colFileModificationWhen $ \t -> NTop (Just t) > NTop (materialVisibleFrom $ entityVal matEnt)
let psValidator = def & defaultSortingByFileTitle let psValidator = def & defaultSortingByFileTitle
fileTable' <- dbTable psValidator DBTable fileTable' <- dbTable psValidator DBTable
{ dbtSQLQuery = \(matFile `E.InnerJoin` file) -> do { dbtSQLQuery = \matFile -> do
E.on $ matFile E.^. MaterialFileFile E.==. file E.^. FileId
E.where_ $ matFile E.^. MaterialFileMaterial E.==. E.val (entityKey matEnt) E.where_ $ matFile E.^. MaterialFileMaterial E.==. E.val (entityKey matEnt)
E.&&. E.not_ (E.isNothing $ file E.^. FileContent) -- don't show directories E.&&. E.not_ (E.isNothing $ matFile E.^. MaterialFileContent) -- don't show directories
return (file E.^. FileTitle, file E.^. FileModified) return (matFile E.^. MaterialFileTitle, matFile E.^. MaterialFileModified)
, dbtRowKey = \(_ `E.InnerJoin` file) -> file E.^. FileId , dbtRowKey = (E.^. MaterialFileId)
, dbtColonnade = widgetColonnade $ mconcat , dbtColonnade = widgetColonnade $ mconcat
[ (<> indicatorCell) <$> colFilePathSimple (view $ _dbrOutput . _1) matLink [ (<> indicatorCell) <$> colFilePathSimple (view $ _dbrOutput . _1) matLink
, materialModDateCol (view $ _dbrOutput . _2) , materialModDateCol (view $ _dbrOutput . _2)
@ -214,9 +206,9 @@ getMShowR tid ssh csh mnm = do
, dbtFilterUI = mempty , dbtFilterUI = mempty
, dbtIdent = "material-files" :: Text , dbtIdent = "material-files" :: Text
, dbtSorting = Map.fromList , dbtSorting = Map.fromList
[ sortFilePath $(sqlIJproj 2 2) [ sortFilePath id
, sortFileModification $(sqlIJproj 2 2) , sortFileModification id
] ]
, dbtCsvEncode = noCsvEncode , dbtCsvEncode = noCsvEncode
, dbtCsvDecode = Nothing , dbtCsvDecode = Nothing
} }
@ -239,18 +231,15 @@ getMEditR = postMEditR
postMEditR tid ssh csh mnm = do postMEditR tid ssh csh mnm = do
(Entity mid Material{..}, files) <- runDB $ do (Entity mid Material{..}, files) <- runDB $ do
matEnt <- fetchMaterial tid ssh csh mnm matEnt <- fetchMaterial tid ssh csh mnm
fileIds <- E.select . E.from $ \(matFile `E.InnerJoin` file) -> do mFileEnts <- selectList [ MaterialFileMaterial ==. entityKey matEnt ] []
E.on $ matFile E.^. MaterialFileFile E.==. file E.^. FileId return (matEnt, mFileEnts)
E.where_ $ matFile E.^. MaterialFileMaterial E.==. E.val (entityKey matEnt)
return $ file E.^. FileId
return (matEnt, Left . E.unValue <$> fileIds)
-- let cid = materialCourse -- let cid = materialCourse
let template = Just MaterialForm let template = Just MaterialForm
{ mfName = materialName { mfName = materialName
, mfType = materialType , mfType = materialType
, mfDescription = materialDescription , mfDescription = materialDescription
, mfVisibleFrom = materialVisibleFrom , mfVisibleFrom = materialVisibleFrom
, mfFiles = Just $ yieldMany files , mfFiles = Just $ yieldMany [ matFile ^. _FileReference . _1 | Entity _ matFile <- files ]
} }
editWidget <- handleMaterialEdit tid ssh csh materialCourse template $ uniqueReplace mid editWidget <- handleMaterialEdit tid ssh csh materialCourse template $ uniqueReplace mid
let headingLong = prependCourseTitle tid ssh csh $ MsgMaterialEditHeading mnm let headingLong = prependCourseTitle tid ssh csh $ MsgMaterialEditHeading mnm
@ -305,23 +294,21 @@ handleMaterialEdit tid ssh csh cid template dbMaterial = do
when saveOk $ redirect -- redirect must happen outside of runDB when saveOk $ redirect -- redirect must happen outside of runDB
$ CourseR tid ssh csh (MaterialR mfName MShowR) $ CourseR tid ssh csh (MaterialR mfName MShowR)
insertMaterialFile' :: MaterialId -> ConduitT () (Either FileId File) Handler () -> DB () insertMaterialFile' :: MaterialId -> FileUploads -> DB ()
insertMaterialFile' mid fs = do insertMaterialFile' mid fs = do
oldFileIdVals <- E.select . E.from $ \(file `E.InnerJoin` materialFile) -> do oldFiles <- fmap (Map.fromList . map $(unValueN 2)) . E.select . E.from $ \materialFile -> do
E.on $ materialFile E.^. MaterialFileFile E.==. file E.^. FileId E.where_ $ materialFile E.^. MaterialFileMaterial E.==. E.val mid
E.where_ $ materialFile E.^. MaterialFileMaterial E.==. E.val mid return (materialFile E.^. MaterialFileTitle, materialFile E.^. MaterialFileId)
return $ file E.^. FileId keep <- execWriterT . runConduit $ transPipe liftHandler fs .| C.mapM_ (finsert oldFiles)
let oldFileIds = setFromList $ map E.unValue oldFileIdVals deleteWhere [ MaterialFileMaterial ==. mid, MaterialFileId <-. Set.toList (setOf folded oldFiles \\ keep) ]
keep <- execWriterT . runConduit $ transPipe (lift . lift) fs .| C.mapM_ finsert
mapM_ deleteCascade (oldFileIds \\ keep :: Set FileId)
where where
finsert (Left fid) = do finsert oldFiles fRef
lift . void $ upsertBy (UniqueMaterialFile mid fid) (MaterialFile mid fid) [] | Just sfId <- fileReferenceTitle fRef `Map.lookup` oldFiles
tell $ singleton fid = tell $ Set.singleton sfId
finsert (Right file) = lift $ do | otherwise
fid <- insert file = do
void . insert $ MaterialFile mid fid -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step sfId <- lift . insert $ _FileReference # (fRef, MaterialFileResidual mid)
tell $ Set.singleton sfId
getMDelR, postMDelR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> Handler Html getMDelR, postMDelR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> Handler Html
getMDelR = postMDelR getMDelR = postMDelR
@ -363,15 +350,14 @@ getMArchiveR tid ssh csh mnm = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgMaterialArchiveName tid ssh csh mnm archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgMaterialArchiveName tid ssh csh mnm
let getMatQuery = (.| C.map entityVal) . E.selectSource . E.from $ let getMatQuery = (.| C.map entityVal) . E.selectSource . E.from $
\(course `E.InnerJoin` material `E.InnerJoin` materialFile `E.InnerJoin` file) -> do \(course `E.InnerJoin` material `E.InnerJoin` materialFile) -> do
E.on $ file E.^. FileId E.==. materialFile E.^. MaterialFileFile
E.on $ material E.^. MaterialId E.==. materialFile E.^. MaterialFileMaterial E.on $ material E.^. MaterialId E.==. materialFile E.^. MaterialFileMaterial
E.on $ material E.^. MaterialCourse E.==. course E.^. CourseId E.on $ material E.^. MaterialCourse E.==. course E.^. CourseId
E.where_ $ course E.^. CourseTerm E.==. E.val tid E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. material E.^. MaterialName E.==. E.val mnm E.&&. material E.^. MaterialName E.==. E.val mnm
return file return materialFile
serveSomeFiles archiveName getMatQuery serveSomeFiles archiveName getMatQuery

View File

@ -57,7 +57,7 @@ type Loads = Map (Either UserEmail UserId) (InvitationData SheetCorrector)
data SheetForm = SheetForm data SheetForm = SheetForm
{ sfName :: SheetName { sfName :: SheetName
, sfDescription :: Maybe Html , sfDescription :: Maybe Html
, sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe (ConduitT () (Either FileId File) Handler ()) , sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe FileUploads
, sfVisibleFrom :: Maybe UTCTime , sfVisibleFrom :: Maybe UTCTime
, sfActiveFrom :: Maybe UTCTime , sfActiveFrom :: Maybe UTCTime
, sfActiveTo :: Maybe UTCTime , sfActiveTo :: Maybe UTCTime
@ -85,13 +85,12 @@ instance Button UniWorX ButtonGeneratePseudonym where
btnClasses BtnGenerate = [BCIsButton, BCDefault] btnClasses BtnGenerate = [BCIsButton, BCDefault]
getFtIdMap :: Key Sheet -> DB (SheetFileType -> Set FileId) getFtIdMap :: Key Sheet -> DB (SheetFileType -> Set FileReference)
getFtIdMap sId = do getFtIdMap sId = do
allfIds <- E.select . E.from $ \(sheetFile `E.InnerJoin` file) -> do allSheetFiles <- E.select . E.from $ \sheetFile -> do
E.on $ sheetFile E.^. SheetFileFile E.==. file E.^. FileId
E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sId E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sId
return (sheetFile E.^. SheetFileType, file E.^. FileId) return sheetFile
return $ partitionFileType [(t,i)|(E.Value t, E.Value i) <- allfIds] return $ partitionFileType [ (sheetFileType, sf ^. _FileReference . _1) | Entity _ sf@SheetFile{..} <- allSheetFiles ]
makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
makeSheetForm msId template = identifyForm FIDsheet . validateForm validateSheet $ \html -> do makeSheetForm msId template = identifyForm FIDsheet . validateForm validateSheet $ \html -> do
@ -353,14 +352,12 @@ getSShowR tid ssh csh shn = do
| NTop (Just mtime) > NTop (sheetFileTypeDates sheet sft) = dateTimeCell mtime | NTop (Just mtime) > NTop (sheetFileTypeDates sheet sft) = dateTimeCell mtime
| otherwise = mempty | otherwise = mempty
let fileData (sheetFile `E.InnerJoin` file) = do let fileData sheetFile = do
-- Restrict to consistent rows that correspond to each other
E.on (sheetFile E.^. SheetFileFile E.==. file E.^. FileId)
-- filter to requested file -- filter to requested file
E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid
E.&&. E.not_ (E.isNothing $ file E.^. FileContent) -- don't show directories E.&&. E.not_ (E.isNothing $ sheetFile E.^. SheetFileContent) -- don't show directories
-- return desired columns -- return desired columns
return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType) return $ (sheetFile E.^. SheetFileTitle, sheetFile E.^. SheetFileModified, sheetFile E.^. SheetFileType)
let colonnadeFiles = widgetColonnade $ mconcat let colonnadeFiles = widgetColonnade $ mconcat
[ sortable (Just "type") (i18nCell MsgSheetFileTypeHeader) $ \(_,_, E.Value ftype) -> [ sortable (Just "type") (i18nCell MsgSheetFileTypeHeader) $ \(_,_, E.Value ftype) ->
let link = CSheetR tid ssh csh shn $ SZipR ftype in let link = CSheetR tid ssh csh shn $ SZipR ftype in
@ -382,7 +379,7 @@ getSShowR tid ssh csh shn = do
& forceFilter "may-access" (Any True) & forceFilter "may-access" (Any True)
(Any hasFiles, fileTable) <- runDB $ dbTable psValidator DBTable (Any hasFiles, fileTable) <- runDB $ dbTable psValidator DBTable
{ dbtSQLQuery = fileData { dbtSQLQuery = fileData
, dbtRowKey = \(_ `E.InnerJoin` file) -> file E.^. FileId , dbtRowKey = (E.^. SheetFileId)
, dbtColonnade = colonnadeFiles , dbtColonnade = colonnadeFiles
, dbtProj = return . dbrOutput :: DBRow _ -> DB (E.Value FilePath, E.Value UTCTime, E.Value SheetFileType) , dbtProj = return . dbrOutput :: DBRow _ -> DB (E.Value FilePath, E.Value UTCTime, E.Value SheetFileType)
, dbtStyle = def , dbtStyle = def
@ -395,16 +392,16 @@ getSShowR tid ssh csh shn = do
, dbtIdent = "files" :: Text , dbtIdent = "files" :: Text
, dbtSorting = Map.fromList , dbtSorting = Map.fromList
[ ( "type" [ ( "type"
, SortColumn $ \(sheetFile `E.InnerJoin` _file) -> E.orderByEnum $ sheetFile E.^. SheetFileType , SortColumn $ \sheetFile -> E.orderByEnum $ sheetFile E.^. SheetFileType
) )
, ( "path" , ( "path"
, SortColumn $ \(_sheetFile `E.InnerJoin` file) -> file E.^. FileTitle , SortColumn $ \sheetFile -> sheetFile E.^. SheetFileTitle
) )
-- , ( "visible" -- , ( "visible"
-- , SortColumn $ \(sheetFile `E.InnerJoin` _file) -> sheetFileTypeDates sheet $ sheetFile E.^. SheetFileType -- not possible without another join for the sheet -- , SortColumn $ \(sheetFile `E.InnerJoin` _file) -> sheetFileTypeDates sheet $ sheetFile E.^. SheetFileType -- not possible without another join for the sheet
-- ) -- )
, ( "time" , ( "time"
, SortColumn $ \(_sheetFile `E.InnerJoin` file) -> file E.^. FileModified , SortColumn $ \sheetFile -> sheetFile E.^. SheetFileModified
) )
] ]
, dbtParams = def , dbtParams = def
@ -442,10 +439,44 @@ getSShowR tid ssh csh shn = do
getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
getSArchiveR tid ssh csh shn = do getSArchiveR tid ssh csh shn = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgSheetArchiveName tid ssh csh shn shId <- runDB $ fetchSheetId tid ssh csh shn
MsgRenderer mr <- getMsgRenderer
let archiveName = flip addExtension (unpack extensionZip) . unpack . mr $ MsgSheetArchiveName tid ssh csh shn
let sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes let sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes
allowedSFTs <- filterM (hasReadAccessTo . sftArchive) [minBound..maxBound] allowedSFTs <- filterM (hasReadAccessTo . sftArchive) universeF
serveZipArchive archiveName $ sheetFilesSFTsQuery tid ssh csh shn allowedSFTs .| C.map entityVal multipleSFTs <- if
| length allowedSFTs < 2 -> return False
| otherwise -> runDB . E.selectExists . E.from $ \(sheet `E.InnerJoin` (sFile1 `E.InnerJoin` sFile2)) -> do
E.on $ sFile1 E.^. SheetFileType E.!=. sFile2 E.^. SheetFileType
E.&&. sFile1 E.^. SheetFileTitle E.==. sFile2 E.^. SheetFileTitle
E.on $ sheet E.^. SheetId E.==. sFile1 E.^. SheetFileSheet
E.&&. sheet E.^. SheetId E.==. sFile2 E.^. SheetFileSheet
E.where_ $ sheet E.^. SheetId E.==. E.val shId
E.&&. sFile1 E.^. SheetFileType `E.in_` E.valList allowedSFTs
E.&&. sFile2 E.^. SheetFileType `E.in_` E.valList allowedSFTs
let modifyTitles SheetFile{..}
| not multipleSFTs = SheetFile{..}
| otherwise = SheetFile
{ sheetFileTitle = unpack (mr $ SheetArchiveFileTypeDirectory sheetFileType) </> sheetFileTitle
, ..
}
sftDirectories <- if
| not multipleSFTs -> return mempty
| otherwise -> runDB . fmap (mapMaybe $ \(sft, mTime) -> (sft, ) <$> mTime) . forM allowedSFTs $ \sft -> fmap ((sft, ) . (=<<) E.unValue) . E.selectMaybe . E.from $ \sFile -> do
E.where_ $ sFile E.^. SheetFileSheet E.==. E.val shId
E.&&. sFile E.^. SheetFileType E.==. E.val sft
return . E.max_ $ sFile E.^. SheetFileModified
serveZipArchive archiveName $ do
forM_ sftDirectories $ \(sft, mTime) -> yield $ SheetFile
{ sheetFileType = sft
, sheetFileTitle = unpack . mr $ SheetArchiveFileTypeDirectory sft
, sheetFileModified = mTime
, sheetFileContent = Nothing
, sheetFileSheet = shId
}
sheetFilesSFTsQuery tid ssh csh shn allowedSFTs .| C.map entityVal .| C.map modifyTitles
getSPseudonymR, postSPseudonymR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent getSPseudonymR, postSPseudonymR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
@ -562,12 +593,12 @@ getSEditR tid ssh csh shn = do
, sfActiveFrom = sheetActiveFrom , sfActiveFrom = sheetActiveFrom
, sfActiveTo = sheetActiveTo , sfActiveTo = sheetActiveTo
, sfSubmissionMode = sheetSubmissionMode , sfSubmissionMode = sheetSubmissionMode
, sfSheetF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetExercise , sfSheetF = Just . yieldMany . Set.elems $ sheetFileIds SheetExercise
, sfHintFrom = sheetHintFrom , sfHintFrom = sheetHintFrom
, sfHintF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetHint , sfHintF = Just . yieldMany . Set.elems $ sheetFileIds SheetHint
, sfSolutionFrom = sheetSolutionFrom , sfSolutionFrom = sheetSolutionFrom
, sfSolutionF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetSolution , sfSolutionF = Just . yieldMany . Set.elems $ sheetFileIds SheetSolution
, sfMarkingF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetMarking , sfMarkingF = Just . yieldMany . Set.elems $ sheetFileIds SheetMarking
, sfMarkingText = sheetMarkingText , sfMarkingText = sheetMarkingText
, sfAutoDistribute = sheetAutoDistribute , sfAutoDistribute = sheetAutoDistribute
, sfAnonymousCorrection = sheetAnonymousCorrection , sfAnonymousCorrection = sheetAnonymousCorrection
@ -674,22 +705,22 @@ postSDelR tid ssh csh shn = do
} }
insertSheetFile' :: SheetId -> SheetFileType -> ConduitT () (Either FileId File) Handler () -> YesodJobDB UniWorX () insertSheetFile' :: SheetId -> SheetFileType -> FileUploads -> YesodJobDB UniWorX ()
insertSheetFile' sid ftype fs = do insertSheetFile' sid ftype fs = do
oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do oldFiles <- fmap (Map.fromList . map $(E.unValueN 2)) . E.select . E.from $ \sheetFile -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
return (file E.^. FileId) return (sheetFile E.^. SheetFileTitle, sheetFile E.^. SheetFileId)
keep <- execWriterT . runConduit $ transPipe liftHandler fs .| C.mapM_ finsert keep <- execWriterT . runConduit $ transPipe liftHandler fs .| C.mapM_ (finsert oldFiles)
mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId) deleteWhere [ SheetFileSheet ==. sid, SheetFileType ==. ftype, SheetFileId <-. Set.toList (setOf folded oldFiles \\ keep) ]
where where
finsert (Left fid) = do finsert oldFiles fRef
lift . void $ upsertBy (UniqueSheetFile fid sid ftype) (SheetFile sid fid ftype) [] | Just sfId <- fileReferenceTitle fRef `Map.lookup` oldFiles
tell $ singleton fid = tell $ Set.singleton sfId
finsert (Right file) = lift $ do | otherwise
fid <- insert file = do
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step sfId <- lift . insert $ _FileReference # (fRef, SheetFileResidual sid ftype)
tell $ Set.singleton sfId
defaultLoads :: CourseId -> DB Loads defaultLoads :: CourseId -> DB Loads

View File

@ -17,8 +17,6 @@ import qualified Control.Monad.State.Class as State
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Data.Conduit.List as C
correctionData :: TermId -> SchoolId -> CourseShorthand -> SheetName -> _ -- CryptoFileNameSubmission -> _ correctionData :: TermId -> SchoolId -> CourseShorthand -> SheetName -> _ -- CryptoFileNameSubmission -> _
correctionData tid ssh csh shn sub = E.select . E.from $ \((course `E.InnerJoin` sheet `E.InnerJoin` submission) `E.LeftOuterJoin` corrector) -> do correctionData tid ssh csh shn sub = E.select . E.from $ \((course `E.InnerJoin` sheet `E.InnerJoin` submission) `E.LeftOuterJoin` corrector) -> do
@ -30,13 +28,11 @@ correctionData tid ssh csh shn sub = E.select . E.from $ \((course `E.InnerJoin`
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. sheet E.^. SheetName E.==. E.val shn E.&&. sheet E.^. SheetName E.==. E.val shn
E.&&. submission E.^. SubmissionId E.==. E.val sub E.&&. submission E.^. SubmissionId E.==. E.val sub
let filesCorrected = E.exists . E.from $ \((f1 `E.InnerJoin` sFile1) `E.LeftOuterJoin` (f2 `E.InnerJoin` sFile2)) -> do let filesCorrected = E.exists . E.from $ \(sFile1 `E.LeftOuterJoin` sFile2) -> do
E.on $ f2 E.?. FileId E.==. sFile2 E.?. SubmissionFileFile E.on $ E.just (sFile1 E.^. SubmissionFileTitle) E.==. sFile2 E.?. SubmissionFileTitle
E.on $ E.just (f1 E.^. FileTitle) E.==. f2 E.?. FileTitle
E.&&. E.just (sFile1 E.^. SubmissionFileSubmission) E.==. sFile2 E.?. SubmissionFileSubmission E.&&. E.just (sFile1 E.^. SubmissionFileSubmission) E.==. sFile2 E.?. SubmissionFileSubmission
-- E.&&. f1 E.^. FileContent E.!=. E.joinV (f2 E.?. FileContent) E.&&. sFile1 E.^. SubmissionFileContent E.!=. E.joinV (sFile2 E.?. SubmissionFileContent)
E.&&. sFile1 E.^. SubmissionFileIsUpdate E.&&. E.maybe E.false E.not_ (sFile2 E.?. SubmissionFileIsUpdate) E.&&. sFile1 E.^. SubmissionFileIsUpdate E.&&. E.maybe E.false E.not_ (sFile2 E.?. SubmissionFileIsUpdate)
E.on $ f1 E.^. FileId E.==. sFile1 E.^. SubmissionFileFile
E.where_ $ sFile1 E.^. SubmissionFileSubmission E.==. submission E.^. SubmissionId E.where_ $ sFile1 E.^. SubmissionFileSubmission E.==. submission E.^. SubmissionId
E.&&. sFile2 E.?. SubmissionFileSubmission E.==. E.just (submission E.^. SubmissionId) E.&&. sFile2 E.?. SubmissionFileSubmission E.==. E.just (submission E.^. SubmissionId)
return (course, sheet, submission, corrector, filesCorrected) return (course, sheet, submission, corrector, filesCorrected)
@ -133,7 +129,7 @@ postCorrectionR tid ssh csh shn cid = do
formResult uploadResult $ \fileUploads -> do formResult uploadResult $ \fileUploads -> do
uid <- maybeAuthId uid <- maybeAuthId
res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| C.mapM (either get404 return) .| extractRatingsMsg .| sinkSubmission uid (Right sub) True res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| extractRatingsMsg .| sinkSubmission uid (Right sub) True
when (is _Just res) $ do when (is _Just res) $ do
addMessageI Success MsgRatingFilesUpdated addMessageI Success MsgRatingFilesUpdated

View File

@ -37,14 +37,13 @@ getSubDownloadR tid ssh csh shn cID (submissionFileTypeIsUpdate -> isUpdate) pat
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file 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 `E.InnerJoin` f) -> do let results = (.| Conduit.map entityVal) . E.selectSource . E.from $ \sf -> do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.&&. f E.^. FileTitle E.==. E.val path E.&&. sf E.^. SubmissionFileTitle E.==. E.val path
E.&&. E.not_ (sf E.^. SubmissionFileIsDeletion) E.&&. E.not_ (sf E.^. SubmissionFileIsDeletion)
E.&&. sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate E.&&. sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate
-- E.&&. E.not_ (E.isNothing $ f E.^. FileContent) -- This is fine, we just return 204 -- E.&&. E.not_ (E.isNothing $ f E.^. FileContent) -- This is fine, we just return 204
return f return sf
serveOneFile results serveOneFile results
@ -61,17 +60,16 @@ getSubArchiveR tid ssh csh shn cID sfType = do
rating <- lift $ getRating submissionID rating <- lift $ getRating submissionID
case sfType of case sfType of
SubmissionOriginal -> (.| Conduit.map entityVal) . E.selectSource . E.from $ \(sf `E.InnerJoin` f) -> do SubmissionOriginal -> (.| Conduit.map (Left . entityVal)) . E.selectSource . E.from $ \sf -> do
E.on $ f E.^. FileId E.==. sf E.^. SubmissionFileFile
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.&&. sf E.^. SubmissionFileIsUpdate E.==. E.val False E.&&. sf E.^. SubmissionFileIsUpdate E.==. E.val False
return f return sf
_ -> submissionFileSource submissionID .| Conduit.map entityVal _other -> E.selectSource (E.from $ submissionFileQuery submissionID) .| Conduit.map (Left . entityVal)
when (sfType == SubmissionCorrected) $ when (sfType == SubmissionCorrected) $
maybe (return ()) (yieldM . ratingFile cID) rating maybe (return ()) (yieldM . fmap Right . ratingFile cID) rating
serveSomeFiles archiveName source serveSomeFiles' archiveName source
getCorrectionsDownloadR :: Handler TypedContent getCorrectionsDownloadR :: Handler TypedContent

View File

@ -18,8 +18,6 @@ import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Database.Esqueleto.Internal.Sql as E (unsafeSqlFunction) import qualified Database.Esqueleto.Internal.Sql as E (unsafeSqlFunction)
import qualified Data.Conduit.Combinators as Conduit
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Map ((!), (!?)) import Data.Map ((!), (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
@ -396,7 +394,7 @@ submissionHelper tid ssh csh shn mcid = do
(Nothing, Just smid) -- no new files, existing submission partners updated (Nothing, Just smid) -- no new files, existing submission partners updated
-> return smid -> return smid
(Just files, _) -> -- new files (Just files, _) -> -- new files
runConduit $ transPipe (lift . lift) files .| Conduit.mapM (either get404 return) .| extractRatingsMsg .| sinkSubmission muid (maybe (Left shid) Right msmid) False runConduit $ transPipe (lift . lift) files .| extractRatingsMsg .| sinkSubmission muid (maybe (Left shid) Right msmid) False
(Nothing, Nothing) -- new submission, no file upload requested (Nothing, Nothing) -- new submission, no file upload requested
-> do -> do
sid <- insert Submission sid <- insert Submission
@ -482,58 +480,53 @@ submissionHelper tid ssh csh shn mcid = do
-- Maybe construct a table to display uploaded archive files -- Maybe construct a table to display uploaded archive files
let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (HandlerFor UniWorX) ()) let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (HandlerFor UniWorX) ())
colonnadeFiles cid = mconcat $ catMaybes colonnadeFiles cid = mconcat $ catMaybes
[ Just . sortable (Just "path") (i18nCell MsgFileTitle) $ \(coalesce -> (mOrig, mCorr)) -> let [ Just . sortable (Just "path") (i18nCell MsgFileTitle) $ \(mOrig, mCorr) -> let
Just fileTitle' = fileTitle . entityVal . snd <$> (mOrig <|> mCorr) Just fileTitle' = submissionFileTitle . entityVal <$> (mOrig <|> mCorr)
origIsFile = fmap (isJust . fileContent . entityVal . snd) mOrig origIsFile = fmap (isJust . submissionFileContent . entityVal) mOrig
corrIsFile = fmap (isJust . fileContent . entityVal . snd) mCorr corrIsFile = fmap (isJust . submissionFileContent . entityVal) mCorr
Just isFile = origIsFile <|> corrIsFile Just isFile = origIsFile <|> corrIsFile
in if in if
| Just True <- origIsFile -> anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionOriginal fileTitle') | Just True <- origIsFile -> anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionOriginal fileTitle')
[whamlet|#{fileTitle'}|] [whamlet|#{fileTitle'}|]
| otherwise -> textCell $ bool (<> "/") id isFile fileTitle' | otherwise -> textCell $ bool (<> "/") id isFile fileTitle'
, guardOn showCorrection . sortable (toNothing "state") (i18nCell MsgCorState) $ \(coalesce -> (_, mCorr)) -> case mCorr of , guardOn showCorrection . sortable (toNothing "state") (i18nCell MsgCorState) $ \(_, mCorr) -> case mCorr of
Nothing -> cell mempty Nothing -> cell mempty
Just (_, Entity _ File{..}) Just (Entity _ SubmissionFile{..})
| isJust fileContent -> | isJust submissionFileContent -> anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionCorrected submissionFileTitle)
anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionCorrected fileTitle) [whamlet|_{MsgFileCorrected}|]
[whamlet|_{MsgFileCorrected}|]
| otherwise -> i18nCell MsgCorrected | otherwise -> i18nCell MsgCorrected
, Just . sortable (Just "time") (i18nCell MsgFileModified) $ \(coalesce -> (mOrig, mCorr)) -> let , Just . sortable (Just "time") (i18nCell MsgFileModified) $ \(mOrig, mCorr) -> let
origTime = fileModified . entityVal . snd <$> mOrig origTime = submissionFileModified . entityVal <$> mOrig
corrTime = fileModified . entityVal . snd <$> mCorr corrTime = submissionFileModified . entityVal <$> mCorr
Just fileTime = (max <$> origTime <*> corrTime) <|> origTime <|> corrTime Just fileTime = (max <$> origTime <*> corrTime) <|> origTime <|> corrTime
in dateTimeCell fileTime in dateTimeCell fileTime
] ]
coalesce :: ((Maybe (Entity SubmissionFile), Maybe (Entity File)), (Maybe (Entity SubmissionFile), Maybe (Entity File))) -> (Maybe (Entity SubmissionFile, Entity File), Maybe (Entity SubmissionFile, Entity File))
coalesce ((ma, mb), (mc, md)) = ((,) <$> ma <*> mb, (,) <$> mc <*> md)
submissionFiles :: _ -> _ -> E.SqlQuery _ submissionFiles :: _ -> _ -> E.SqlQuery _
submissionFiles smid ((sf1 `E.InnerJoin` f1) `E.FullOuterJoin` (sf2 `E.InnerJoin` f2)) = do submissionFiles smid (sf1 `E.FullOuterJoin` sf2) = do
E.on $ f2 E.?. FileId E.==. sf2 E.?. SubmissionFileFile E.on $ sf1 E.?. SubmissionFileTitle E.==. sf2 E.?. SubmissionFileTitle
E.on $ f1 E.?. FileTitle E.==. f2 E.?. FileTitle
E.&&. sf1 E.?. SubmissionFileSubmission E.==. sf2 E.?. SubmissionFileSubmission E.&&. sf1 E.?. SubmissionFileSubmission E.==. sf2 E.?. SubmissionFileSubmission
E.&&. sf1 E.?. SubmissionFileId E.!=. sf2 E.?. SubmissionFileId E.&&. sf1 E.?. SubmissionFileId E.!=. sf2 E.?. SubmissionFileId
E.&&. sf2 E.?. SubmissionFileIsDeletion E.==. E.val (Just False) E.&&. sf2 E.?. SubmissionFileIsDeletion E.==. E.val (Just False)
E.on $ f1 E.?. FileId E.==. sf1 E.?. SubmissionFileFile
E.where_ $ (sf1 E.?. SubmissionFileIsUpdate E.==. E.val (Just False) E.||. E.isNothing (sf1 E.?. SubmissionFileIsUpdate)) E.where_ $ (sf1 E.?. SubmissionFileIsUpdate E.==. E.val (Just False) E.||. E.isNothing (sf1 E.?. SubmissionFileIsUpdate))
E.&&. (sf2 E.?. SubmissionFileIsUpdate E.==. E.val (Just True) E.||. E.isNothing (sf2 E.?. SubmissionFileIsUpdate)) E.&&. (sf2 E.?. SubmissionFileIsUpdate E.==. E.val (Just True) E.||. E.isNothing (sf2 E.?. SubmissionFileIsUpdate))
E.&&. (sf2 E.?. SubmissionFileIsDeletion E.==. E.val (Just False) E.||. E.isNothing (sf2 E.?. SubmissionFileIsDeletion)) E.&&. (sf2 E.?. SubmissionFileIsDeletion E.==. E.val (Just False) E.||. E.isNothing (sf2 E.?. SubmissionFileIsDeletion))
E.&&. (sf1 E.?. SubmissionFileSubmission E.==. E.val (Just smid) E.||. sf2 E.?. SubmissionFileSubmission E.==. E.val (Just smid)) E.&&. (sf1 E.?. SubmissionFileSubmission E.==. E.val (Just smid) E.||. sf2 E.?. SubmissionFileSubmission E.==. E.val (Just smid))
return ((sf1, f1), (sf2, f2)) return (sf1, sf2)
smid2ArchiveTable (smid,cid) = DBTable smid2ArchiveTable (smid,cid) = DBTable
{ dbtSQLQuery = submissionFiles smid { dbtSQLQuery = submissionFiles smid
, dbtRowKey = \((_ `E.InnerJoin` f1) `E.FullOuterJoin` (_ `E.InnerJoin` f2)) -> (f1 E.?. FileId, f2 E.?. FileId) , dbtRowKey = \(sf1 `E.FullOuterJoin` sf2) -> (sf1 E.?. SubmissionFileId, sf2 E.?. SubmissionFileId)
, dbtColonnade = colonnadeFiles cid , dbtColonnade = colonnadeFiles cid
, dbtProj = return . dbrOutput , dbtProj = return . dbrOutput
, dbtStyle = def , dbtStyle = def
, dbtIdent = "files" :: Text , dbtIdent = "files" :: Text
, dbtSorting = Map.fromList , dbtSorting = Map.fromList
[ ( "path" [ ( "path"
, SortColumn $ \((_sf1 `E.InnerJoin` f1) `E.FullOuterJoin` (_sf2 `E.InnerJoin` f2)) -> E.coalesce [f1 E.?. FileTitle, f2 E.?. FileTitle] , SortColumn $ \(sf1 `E.FullOuterJoin` sf2) -> E.coalesce [sf1 E.?. SubmissionFileTitle, sf2 E.?. SubmissionFileTitle]
) )
, ( "time" , ( "time"
, SortColumn $ \((_sf1 `E.InnerJoin` f1) `E.FullOuterJoin` (_sf2 `E.InnerJoin` f2)) -> (E.unsafeSqlFunction "GREATEST" ([f1 E.?. FileModified, f2 E.?. FileModified] :: [E.SqlExpr (E.Value (Maybe UTCTime))]) :: E.SqlExpr (E.Value (Maybe UTCTime))) , SortColumn $ \(sf1 `E.FullOuterJoin` sf2) -> (E.unsafeSqlFunction "GREATEST" ([sf1 E.?. SubmissionFileModified, sf2 E.?. SubmissionFileModified] :: [E.SqlExpr (E.Value (Maybe UTCTime))]) :: E.SqlExpr (E.Value (Maybe UTCTime)))
) )
] ]
, dbtFilter = mempty , dbtFilter = mempty
@ -545,13 +538,11 @@ submissionHelper tid ssh csh shn mcid = do
mFileTable <- traverse (runDB . dbTableWidget' def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid mFileTable <- traverse (runDB . dbTableWidget' def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid
filesCorrected <- fmap (fromMaybe False) . for msmid $ \subId -> runDB . E.selectExists . E.from $ \((f1 `E.InnerJoin` sFile1) `E.LeftOuterJoin` (f2 `E.InnerJoin` sFile2)) -> do filesCorrected <- fmap (fromMaybe False) . for msmid $ \subId -> runDB . E.selectExists . E.from $ \(sFile1 `E.LeftOuterJoin` sFile2) -> do
E.on $ f2 E.?. FileId E.==. sFile2 E.?. SubmissionFileFile E.on $ E.just (sFile1 E.^. SubmissionFileTitle) E.==. sFile2 E.?. SubmissionFileTitle
E.on $ E.just (f1 E.^. FileTitle) E.==. f2 E.?. FileTitle
E.&&. E.just (sFile1 E.^. SubmissionFileSubmission) E.==. sFile2 E.?. SubmissionFileSubmission E.&&. E.just (sFile1 E.^. SubmissionFileSubmission) E.==. sFile2 E.?. SubmissionFileSubmission
-- E.&&. f1 E.^. FileContent E.!=. E.joinV (f2 E.?. FileContent) E.&&. sFile1 E.^. SubmissionFileContent E.!=. E.joinV (sFile2 E.?. SubmissionFileContent)
E.&&. sFile1 E.^. SubmissionFileIsUpdate E.&&. E.maybe E.false E.not_ (sFile2 E.?. SubmissionFileIsUpdate) E.&&. sFile1 E.^. SubmissionFileIsUpdate E.&&. E.maybe E.false E.not_ (sFile2 E.?. SubmissionFileIsUpdate)
E.on $ f1 E.^. FileId E.==. sFile1 E.^. SubmissionFileFile
E.where_ $ sFile1 E.^. SubmissionFileSubmission E.==. E.val subId E.where_ $ sFile1 E.^. SubmissionFileSubmission E.==. E.val subId
E.where_ $ sFile2 E.?. SubmissionFileSubmission E.==. E.just (E.val subId) E.where_ $ sFile2 E.?. SubmissionFileSubmission E.==. E.just (E.val subId)

View File

@ -54,7 +54,6 @@ postCorrectionsUploadR = do
uid <- requireAuthId uid <- requireAuthId
mbSubs <- msgSubmissionErrors . runDBJobs . runConduit $ mbSubs <- msgSubmissionErrors . runDBJobs . runConduit $
transPipe (lift . lift) files transPipe (lift . lift) files
.| C.mapM (either get404 return)
.| extractRatingsMsg .| extractRatingsMsg
.| C.map setDone .| C.map setDone
.| sinkMultiSubmission uid True .| sinkMultiSubmission uid True

View File

@ -430,9 +430,7 @@ deleteUser duid = do
singleSubmissions <- selectSubmissionsWhere (\numBuddies -> numBuddies E.==. E.val (0::Int64)) singleSubmissions <- selectSubmissionsWhere (\numBuddies -> numBuddies E.==. E.val (0::Int64))
deleteCascade duid deleteCascade duid
forM_ singleSubmissions $ \(E.Value submissionId) -> do forM_ singleSubmissions $ \(E.Value submissionId) -> do
deleteFileIds <- map E.unValue <$> getSubmissionFiles submissionId
deleteCascade submissionId deleteCascade submissionId
deleteCascadeWhere [FileId <-. deleteFileIds] -- TODO: throws exception for de-duplicated files
deletedSubmissionGroups <- deleteSingleSubmissionGroups deletedSubmissionGroups <- deleteSingleSubmissionGroups
return ((length singleSubmissions, length groupSubmissions),deletedSubmissionGroups) return ((length singleSubmissions, length groupSubmissions),deletedSubmissionGroups)
@ -447,13 +445,6 @@ deleteUser duid = do
E.&&. whereBuddies numBuddies E.&&. whereBuddies numBuddies
return $ submission E.^. SubmissionId return $ submission E.^. SubmissionId
getSubmissionFiles :: SubmissionId -> DB [E.Value (Key File)]
getSubmissionFiles subId = E.select $ E.from $ \file -> do
E.where_ $ E.exists $ E.from $ \submissionFile ->
E.where_ $ submissionFile E.^. SubmissionFileSubmission E.==. E.val subId
E.&&. submissionFile E.^. SubmissionFileFile E.==. file E.^. FileId
return $ file E.^. FileId
deleteSingleSubmissionGroups = E.deleteCount $ E.from $ \submissionGroup -> do deleteSingleSubmissionGroups = E.deleteCount $ E.from $ \submissionGroup -> do
E.where_ $ E.exists $ E.from $ \subGroupUser -> E.where_ $ E.exists $ E.from $ \subGroupUser ->
E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId

View File

@ -9,6 +9,7 @@ import Data.Map ((!))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
import qualified Data.Conduit.Combinators as C
import Handler.Utils.DateTime as Handler.Utils import Handler.Utils.DateTime as Handler.Utils
import Handler.Utils.Form as Handler.Utils import Handler.Utils.Form as Handler.Utils
@ -25,6 +26,7 @@ import Handler.Utils.Widgets as Handler.Utils
import Handler.Utils.Database as Handler.Utils import Handler.Utils.Database as Handler.Utils
import Handler.Utils.Occurrences as Handler.Utils import Handler.Utils.Occurrences as Handler.Utils
import Handler.Utils.Memcached as Handler.Utils import Handler.Utils.Memcached as Handler.Utils
import Handler.Utils.Files as Handler.Utils
import Handler.Utils.Term as Handler.Utils import Handler.Utils.Term as Handler.Utils
@ -36,44 +38,47 @@ sendThisFile :: File -> Handler TypedContent
sendThisFile File{..} sendThisFile File{..}
| Just fileContent' <- fileContent = do | Just fileContent' <- fileContent = do
setContentDisposition' . Just $ takeFileName fileTitle setContentDisposition' . Just $ takeFileName fileTitle
return $ TypedContent (mimeLookup (pack fileTitle) <> "; charset=utf-8") (toContent fileContent') return $ TypedContent (simpleContentType (mimeLookup $ pack fileTitle) <> "; charset=utf-8") (toContent fileContent')
| 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
serveOneFile :: ConduitT () File (YesodDB UniWorX) () -> Handler TypedContent serveOneFile :: forall file. HasFileReference file => ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent
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 file [file] -> sendThisFile =<< runDB (sourceFile' file)
[] -> notFound [] -> notFound
other -> do _other -> do
$logErrorS "SFileR" $ "Multiple matching files: " <> tshow other $logErrorS "SFileR" "Multiple matching files found."
error "Multiple matching files found." error "Multiple matching files found."
-- | Serve one file directly or a zip-archive of files, identified through a given DB query -- | Serve one file directly or a zip-archive of files, identified through a given DB query
-- --
-- Like `serveOneFile`, but sends a zip-archive if multiple results are returned -- Like `serveOneFile`, but sends a zip-archive if multiple results are returned
serveSomeFiles :: FilePath -> ConduitT () File (YesodDB UniWorX) () -> Handler TypedContent serveSomeFiles :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent
serveSomeFiles archiveName source = do serveSomeFiles archiveName source = serveSomeFiles' archiveName $ source .| C.map Left
serveSomeFiles' :: forall file. HasFileReference file => FilePath -> ConduitT () (Either file File) (YesodDB UniWorX) () -> Handler TypedContent
serveSomeFiles' archiveName source = do
results <- runDB . runConduit $ source .| peekN 2 results <- runDB . runConduit $ source .| peekN 2
$logDebugS "serveSomeFiles" . tshow $ length results $logDebugS "serveSomeFiles" . tshow $ length results
case results of case results of
[] -> notFound [] -> notFound
[file] -> sendThisFile file [file] -> sendThisFile =<< either (runDB . sourceFile') return file
_moreFiles -> do _moreFiles -> do
setContentDisposition' $ Just archiveName setContentDisposition' $ Just archiveName
respondSourceDB typeZip $ do respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder source .| eitherC sourceFiles' (C.map id) .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
-- | Serve any number of files as a zip-archive of files, identified through a given DB query -- | Serve any number of files as a zip-archive of files, identified through a given DB query
-- --
-- Like `serveSomeFiles`, but always sends a zip-archive, even if a single file is returned -- Like `serveSomeFiles`, but always sends a zip-archive, even if a single file is returned
serveZipArchive :: FilePath -> ConduitT () File (YesodDB UniWorX) () -> Handler TypedContent serveZipArchive :: forall file. HasFileReference file => FilePath -> ConduitT () file (YesodDB UniWorX) () -> Handler TypedContent
serveZipArchive archiveName source = do serveZipArchive archiveName source = do
results <- runDB . runConduit $ source .| peekN 2 results <- runDB . runConduit $ source .| peekN 1
$logDebugS "serveZipArchive" . tshow $ length results $logDebugS "serveZipArchive" . tshow $ length results
@ -83,7 +88,7 @@ serveZipArchive archiveName source = do
setContentDisposition' $ Just archiveName setContentDisposition' $ Just archiveName
respondSourceDB typeZip $ do respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder source .| sourceFiles' .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
-- | Prefix a message with a short course id, -- | Prefix a message with a short course id,

View File

@ -278,6 +278,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
insert_ . AllocationMatching allocId allocFp now <=< insert $ File "matchings.log" (Just $ encodeUtf8 allocLog) now FileReference{..} <- sinkFile $ File "matchings.log" (Just $ encodeUtf8 allocLog) now
insert_ . AllocationMatching allocId allocFp now $ fromMaybe (error "allocation result stored without fileReferenceContent") fileReferenceContent
doAllocation allocId now allocMatchings doAllocation allocId now allocMatchings

View File

@ -215,7 +215,7 @@ fileSourceCsv :: ( FromNamedRecord csv
, MonadHandler m , MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> ConduitT (Either FileId File) csv m () => ConduitT FileReference csv m ()
fileSourceCsv = uploadContents .| decodeCsv fileSourceCsv = uploadContents .| decodeCsv
fileSourceCsvPositional :: ( MonadHandler m fileSourceCsvPositional :: ( MonadHandler m
@ -224,7 +224,7 @@ fileSourceCsvPositional :: ( MonadHandler m
, FromRecord csv , FromRecord csv
) )
=> HasHeader => HasHeader
-> ConduitT (Either FileId File) csv m () -> ConduitT FileReference csv m ()
fileSourceCsvPositional hdr = uploadContents .| decodeCsvPositional hdr fileSourceCsvPositional hdr = uploadContents .| decodeCsvPositional hdr

View File

@ -0,0 +1,45 @@
module Handler.Utils.Files
( sourceFile, sourceFile'
, sourceFiles, sourceFiles'
, SourceFilesException(..)
) where
import Import
import qualified Data.Conduit.Combinators as C
data SourceFilesException
= SourceFilesMismatchedHashes
| SourceFilesContentUnavailable
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (Exception)
sourceFiles :: ConduitT FileReference File (YesodDB UniWorX) ()
sourceFiles = C.mapM sourceFile
sourceFile :: FileReference -> DB File
sourceFile FileReference{..} = do
mFileContent <- traverse get $ FileContentKey <$> fileReferenceContent
fileContent <- if
| is (_Just . _Nothing) mFileContent
-> throwM SourceFilesContentUnavailable
| fmap (fmap fileContentHash) mFileContent /= fmap Just fileReferenceContent
-> throwM SourceFilesMismatchedHashes
| Just fileContent' <- fileContentContent <$> join mFileContent
-> return $ Just fileContent'
| otherwise
-> return Nothing
return File
{ fileTitle = fileReferenceTitle
, fileContent
, fileModified = fileReferenceModified
}
sourceFiles' :: forall file. HasFileReference file => ConduitT file File (YesodDB UniWorX) ()
sourceFiles' = C.mapM sourceFile'
sourceFile' :: forall file. HasFileReference file => file -> DB File
sourceFile' = sourceFile . view (_FileReference . _1)

View File

@ -7,6 +7,7 @@ module Handler.Utils.Form
) where ) where
import Utils.Form import Utils.Form
import Utils.Files
import Handler.Utils.Form.Types import Handler.Utils.Form.Types
@ -18,6 +19,8 @@ import Handler.Utils.Widgets
import Handler.Utils.I18n import Handler.Utils.I18n
import Handler.Utils.Files
import Import import Import
import Data.Char (chr, ord) import Data.Char (chr, ord)
import qualified Data.Char as Char import qualified Data.Char as Char
@ -31,7 +34,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, mapMaybeM) import qualified Data.Conduit.List as C (mapMaybe)
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
@ -61,8 +64,6 @@ import Handler.Utils.Form.MassInput
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import qualified Data.ByteString.Base64.URL as Base64 import qualified Data.ByteString.Base64.URL as Base64
import Data.Time.Clock.System (systemEpochDay)
---------------------------- ----------------------------
-- Buttons (new version ) -- -- Buttons (new version ) --
@ -831,11 +832,10 @@ pseudonymWordField = checkMMap doCheck id $ ciField & addDatalist (return $ mkOp
= return . Left $ MsgUnknownPseudonymWord (CI.original w) = return . Left $ MsgUnknownPseudonymWord (CI.original w)
type FileUploads = ConduitT () (Either FileId File) Handler () type FileUploads = ConduitT () FileReference Handler ()
uploadContents :: (MonadHandler m, HandlerSite m ~ UniWorX) => ConduitT (Either FileId File) ByteString m () uploadContents :: (MonadHandler m, HandlerSite m ~ UniWorX) => ConduitT FileReference ByteString m ()
uploadContents = C.mapMaybeM $ either dbContents (return . fileContent) uploadContents = transPipe (liftHandler . runDB) sourceFiles .| C.mapMaybe fileContent
where dbContents = fmap (fileContent =<<) . liftHandler . runDB . get
data FileFieldUserOption a = FileFieldUserOption data FileFieldUserOption a = FileFieldUserOption
{ fieldOptionForce :: Bool { fieldOptionForce :: Bool
@ -847,9 +847,9 @@ data FileField = FileField
, fieldUnpackZips :: FileFieldUserOption Bool , fieldUnpackZips :: FileFieldUserOption Bool
, fieldMultiple :: Bool , fieldMultiple :: Bool
, fieldRestrictExtensions :: Maybe (NonNull (Set Extension)) , fieldRestrictExtensions :: Maybe (NonNull (Set Extension))
, fieldAdditionalFiles :: Map FileId (FileFieldUserOption Bool) , fieldAdditionalFiles :: Map FilePath (Maybe FileContentReference, UTCTime, FileFieldUserOption Bool)
, fieldMaxFileSize :: Maybe Natural -- ^ Applied to each file separately , fieldMaxFileSize :: Maybe Natural
} deriving (Eq, Ord, Read, Show, Generic, Typeable) } deriving (Generic, Typeable)
genericFileField :: forall m. genericFileField :: forall m.
( MonadHandler m ( MonadHandler m
@ -860,6 +860,8 @@ genericFileField mkOpts = Field{..}
where where
permittedExtension :: FileField -> FileName -> Bool permittedExtension :: FileField -> FileName -> Bool
permittedExtension FileField{..} fTitle permittedExtension FileField{..} fTitle
| unpack fTitle `Map.member` fieldAdditionalFiles
= True
| Just exts <- fieldRestrictExtensions | Just exts <- fieldRestrictExtensions
= anyOf (re _nullable . folded . unpacked) ((flip isExtensionOf `on` CI.foldCase) $ unpack fTitle) exts = anyOf (re _nullable . folded . unpacked) ((flip isExtensionOf `on` CI.foldCase) $ unpack fTitle) exts
| otherwise | otherwise
@ -880,38 +882,37 @@ genericFileField mkOpts = Field{..}
$logDebugS "genericFileField.getIdent" $ tshow ident $logDebugS "genericFileField.getIdent" $ tshow ident
return ident return ident
getPermittedFiles :: Maybe Text -> FileField -> DB (Map FileId (FileFieldUserOption Bool)) getPermittedFiles :: Maybe Text -> FileField -> DB (Map FilePath (Maybe FileContentReference, UTCTime, FileFieldUserOption Bool))
getPermittedFiles mIdent opts@FileField{..} = do getPermittedFiles mIdent opts@FileField{..} = do
sessionFiles <- fmap fold . for mIdent $ \fieldIdent' -> sessionFiles <- for mIdent $ \fieldIdent' ->
fold . (HashMap.lookup fieldIdent' . unMergeHashMap =<<) <$> lookupSessionJson @_ @(MergeHashMap Text (Set SessionFileId)) @_ SessionFiles foldMap (HashMap.findWithDefault mempty fieldIdent' . unMergeHashMap) <$> lookupSessionJson @_ @(MergeHashMap Text (Map FilePath (SessionFileId, UTCTime))) @_ SessionFiles
sessionFiles' <- flip foldMapM sessionFiles $ \sfId -> maybeT (return Map.empty) $ do sessionFiles' <- flip foldMapM sessionFiles $ \sFiles -> flip foldMapM (Map.toList sFiles) $ \(fTitle, (sfId, fModified)) -> maybeT (return Map.empty) $ do
SessionFile{..} <- MaybeT $ get sfId SessionFile{..} <- MaybeT $ get sfId
when (is _Just fieldRestrictExtensions) $ do guard $ is _Nothing sessionFileContent || permittedExtension opts (pack fTitle)
(fTitle, isDirectory) <- MaybeT . fmap (getFirst . foldMap (First . Just . $(E.unValueN 2))) . E.select . E.from $ \file -> do return $ Map.singleton fTitle (sessionFileContent, fModified, FileFieldUserOption False True)
E.where_ $ file E.^. FileId E.==. E.val sessionFileFile
return $ (file E.^. FileTitle, E.isNothing $ file E.^. FileContent)
guard $ isDirectory || permittedExtension opts (pack fTitle)
return . Map.singleton sessionFileFile $ FileFieldUserOption False True
$logDebugS "genericFileField.getPermittedFiles" $ "Additional: " <> tshow fieldAdditionalFiles $logDebugS "genericFileField.getPermittedFiles" $ "Additional: " <> tshow fieldAdditionalFiles
$logDebugS "genericFileField.getPermittedFiles" $ "Session: " <> tshow sessionFiles' $logDebugS "genericFileField.getPermittedFiles" $ "Session: " <> tshow sessionFiles'
return $ fieldAdditionalFiles <> sessionFiles' return $ mconcat
[ Map.filter (views _3 $ (||) <$> not . fieldOptionForce <*> not . fieldOptionDefault) fieldAdditionalFiles
, sessionFiles'
, Map.filter (views _3 $ (&&) <$> fieldOptionForce <*> fieldOptionDefault) fieldAdditionalFiles
]
handleUpload :: FileField -> Maybe Text -> File -> DB (Maybe FileId) handleUpload :: FileField -> Maybe Text -> ConduitT File FileReference (YesodDB UniWorX) ()
handleUpload FileField{fieldMaxFileSize} mIdent file handleUpload FileField{fieldMaxFileSize} mIdent
| maybe (const False) (<) fieldMaxFileSize $ maybe 0 (fromIntegral . olength) (fileContent file) = C.filter (\File{..} -> maybe (const True) (<) fieldMaxFileSize $ maybe 0 (fromIntegral . olength) fileContent)
= return Nothing -- Don't save files that are too large .| sinkFiles
| otherwise = do .| maybe (C.map id) mkSessionFile mIdent
for mIdent $ \ident -> do where
now <- liftIO getCurrentTime mkSessionFile ident = C.mapM $ \fRef@FileReference{..} -> fRef <$ do
oldSFIds <- fmap (Set.fromList . map E.unValue) . E.select . E.from $ \sessionFile -> do now <- liftIO getCurrentTime
E.where_ $ E.subSelectForeign sessionFile SessionFileFile (E.^. FileTitle) E.==. E.val (fileTitle file) sfId <- insert $ SessionFile fileReferenceContent now
E.&&. sessionFile E.^. SessionFileTouched E.<=. E.val now modifySessionJson SessionFiles $ \(fromMaybe mempty -> MergeHashMap old) ->
return $ sessionFile E.^. SessionFileId Just . MergeHashMap $ HashMap.insert ident (Map.insert fileReferenceTitle (sfId, fileReferenceModified) $ HashMap.findWithDefault mempty ident old) old
fId <- insert file
sfId <- insert $ SessionFile fId now
modifySessionJson SessionFiles $ \(fromMaybe mempty -> MergeHashMap old) -> _FileTitle :: Prism' Text FilePath
Just . MergeHashMap $ HashMap.insert ident (Set.insert sfId . maybe Set.empty (`Set.difference` oldSFIds) $ HashMap.lookup ident old) old _FileTitle = prism' (("f." <>) . pack) $ fmap unpack . Text.stripPrefix "f."
return fId
fieldEnctype = Multipart fieldEnctype = Multipart
fieldParse :: [Text] -> [FileInfo] -> m (Either (SomeMessage (HandlerSite m)) (Maybe FileUploads)) fieldParse :: [Text] -> [FileInfo] -> m (Either (SomeMessage (HandlerSite m)) (Maybe FileUploads))
@ -921,10 +922,6 @@ genericFileField mkOpts = Field{..}
mIdent <- fmap getFirst . flip foldMapM vals $ \v -> mIdent <- fmap getFirst . flip foldMapM vals $ \v ->
fmap First . runMaybeT . exceptTMaybe $ encodedSecretBoxOpen v fmap First . runMaybeT . exceptTMaybe $ encodedSecretBoxOpen v
let
decrypt' :: CryptoUUIDFile -> Handler (Maybe FileId)
decrypt' = fmap (either (\(_ :: CryptoIDError) -> Nothing) Just) . try . decrypt
let uploadedFilenames = fileName <$> bool (take 1) id fieldMultiple files let uploadedFilenames = fileName <$> bool (take 1) id fieldMultiple files
let let
@ -933,7 +930,7 @@ genericFileField mkOpts = Field{..}
| otherwise = unpackZips `elem` vals | otherwise = unpackZips `elem` vals
handleFile :: FileInfo -> ConduitT () File Handler () handleFile :: FileInfo -> ConduitT () File Handler ()
handleFile handleFile
| doUnpack = sourceFiles | doUnpack = receiveFiles
| otherwise = yieldM . acceptFile | otherwise = yieldM . acceptFile
invalidUploadExtension fName invalidUploadExtension fName
= not (permittedExtension opts fName) = not (permittedExtension opts fName)
@ -942,36 +939,42 @@ genericFileField mkOpts = Field{..}
whenIsJust fieldMaxFileSize $ \maxSize -> forM_ files $ \fInfo -> do whenIsJust fieldMaxFileSize $ \maxSize -> forM_ files $ \fInfo -> do
fLength <- runConduit $ fileSource fInfo .| C.takeE (fromIntegral $ succ maxSize) .| C.lengthE fLength <- runConduit $ fileSource fInfo .| C.takeE (fromIntegral $ succ maxSize) .| C.lengthE
when (fLength > maxSize) $ do when (fLength > maxSize) $ do
liftHandler . runDB . runConduit $ when (is _Just mIdent) $
mapM_ (transPipe lift . handleFile) files liftHandler . runDB . runConduit $
.| C.mapM_ (void . handleUpload opts mIdent) mapM_ (transPipe lift . handleFile) files
.| handleUpload opts mIdent
.| C.sinkNull
throwE . SomeMessage . MsgGenericFileFieldFileTooLarge . unpack $ fileName fInfo throwE . SomeMessage . MsgGenericFileFieldFileTooLarge . unpack $ fileName fInfo
if | invExt : _ <- filter invalidUploadExtension uploadedFilenames if | invExt : _ <- filter invalidUploadExtension uploadedFilenames
-> do -> do
liftHandler . runDB . runConduit $ when (is _Just mIdent) $
mapM_ (transPipe lift . handleFile) files liftHandler . runDB . runConduit $
.| C.mapM_ (void . handleUpload opts mIdent) mapM_ (transPipe lift . handleFile) files
.| handleUpload opts mIdent
.| C.sinkNull
throwE . SomeMessage . MsgGenericFileFieldInvalidExtension $ unpack invExt throwE . SomeMessage . MsgGenericFileFieldInvalidExtension $ unpack invExt
| otherwise | otherwise
-> do -> do
let fSrc = do let fSrc = do
permittedFiles <- liftHandler . runDB $ getPermittedFiles mIdent opts permittedFiles <- liftHandler . runDB $ getPermittedFiles mIdent opts
yieldMany [ Left fId yieldMany [ FileReference{..}
| (fId, FileFieldUserOption{..}) <- Map.toList permittedFiles | ( fileReferenceTitle
, (fileReferenceContent, fileReferenceModified, FileFieldUserOption{..})
) <- Map.toList permittedFiles
, fieldOptionForce, fieldOptionDefault , fieldOptionForce, fieldOptionDefault
] ]
yieldMany vals yieldMany vals
.| C.mapMaybe fromPathPiece .| C.mapMaybe (preview _FileTitle)
.| C.mapMaybeM (\enc -> fmap (, enc) <$> decrypt' enc) .| C.mapMaybe (\fTitle -> fmap (fTitle, ) . assertM (views _3 $ not . fieldOptionForce) $ Map.lookup fTitle permittedFiles)
.| C.filter (\(fId, _) -> maybe False (not . fieldOptionForce) $ Map.lookup fId permittedFiles) .| C.filter (\(fTitle, _) ->
.| C.filter (\(_, enc) -> fieldMultiple fieldMultiple
|| ( (bool (\n h -> [n] == h) elem fieldMultiple) enc (mapMaybe fromPathPiece vals) || ( (bool (\n h -> h == pure n) elem fieldMultiple) fTitle (mapMaybe (preview _FileTitle) vals)
&& null files && null files
) )
) )
.| C.map (\(fId, _) -> Left fId) .| C.map (\(fileReferenceTitle, (fileReferenceContent, fileReferenceModified, _)) -> FileReference{..})
mapM_ handleFile (bool (take 1) id fieldMultiple files) .| C.map Right mapM_ handleFile (bool (take 1) id fieldMultiple files) .| transPipe runDB (handleUpload opts mIdent)
(unsealConduitT -> fSrc', length -> nFiles) <- liftHandler $ fSrc $$+ peekN 2 (unsealConduitT -> fSrc', length -> nFiles) <- liftHandler $ fSrc $$+ peekN 2
$logDebugS "genericFileField.fieldParse" $ tshow nFiles $logDebugS "genericFileField.fieldParse" $ tshow nFiles
if if
@ -980,7 +983,8 @@ genericFileField mkOpts = Field{..}
| not fieldMultiple -> do | not fieldMultiple -> do
liftHandler . runDB . runConduit $ liftHandler . runDB . runConduit $
mapM_ (transPipe lift . handleFile) files mapM_ (transPipe lift . handleFile) files
.| C.mapM_ (void . handleUpload opts mIdent) .| handleUpload opts mIdent
.| sinkNull
throwE $ SomeMessage MsgOnlyUploadOneFile throwE $ SomeMessage MsgOnlyUploadOneFile
| otherwise -> return $ Just fSrc' | otherwise -> return $ Just fSrc'
@ -990,39 +994,33 @@ genericFileField mkOpts = Field{..}
mIdent <- getIdent opts mIdent <- getIdent opts
identSecret <- for mIdent $ encodedSecretBox SecretBoxShort identSecret <- for mIdent $ encodedSecretBox SecretBoxShort
fileInfos <- liftHandler . runDB $ do fileInfos <- liftHandler $ do
(uploads, references) <- runWriterT . for val $ \src -> do references <- for val $ fmap (Map.fromList . map (\FileReference{..} -> (fileReferenceTitle, (fileReferenceContent, fileReferenceModified)))) . sourceToList
fmap Set.fromList . sourceToList
$ transPipe (lift . lift) src
.| C.mapMaybeM (either (\fId -> Nothing <$ tell (Set.singleton fId)) $ lift . handleUpload opts mIdent)
permittedFiles <- getPermittedFiles mIdent opts permittedFiles <- runDB $ getPermittedFiles mIdent opts
let let
sentVals :: Either Text (Set FileId) sentVals :: Either Text (Set FilePath)
sentVals = uploads <&> Set.union (references `Set.intersection` Map.keysSet permittedFiles) sentVals = references <&> (`Set.intersection` Map.keysSet permittedFiles) . Map.keysSet
let let
toFUI (E.Value fuiId', E.Value fuiTitle) = do toFUI fuiTitle
fuiId <- encrypt fuiId' = let fuiHtmlId = [st|#{fieldId}--#{fuiTitle}|]
let fuiHtmlId = [st|#{fieldId}--#{toPathPiece fuiId}|] fuiChecked
fuiChecked | Right sentVals' <- sentVals
| Right sentVals' <- sentVals = fuiTitle `Set.member` sentVals'
= fuiId' `Set.member` sentVals' | Just (_, _, FileFieldUserOption{..}) <- Map.lookup fuiTitle fieldAdditionalFiles
| Just FileFieldUserOption{..} <- Map.lookup fuiId' fieldAdditionalFiles = fieldOptionDefault
= fieldOptionDefault | otherwise = False
| otherwise = False fuiSession = fuiTitle `Map.notMember` fieldAdditionalFiles
fuiSession = fuiId' `Map.notMember` fieldAdditionalFiles fuiForced
fuiForced | Just (_, _, FileFieldUserOption{..}) <- Map.lookup fuiTitle permittedFiles
| Just FileFieldUserOption{..} <- Map.lookup fuiId' permittedFiles = fieldOptionForce
= fieldOptionForce | otherwise
| otherwise = False
= False in FileUploadInfo{..}
return FileUploadInfo{..}
fileInfos' <- mapM toFUI <=< E.select . E.from $ \file -> do fileInfos' = map toFUI . Set.toList $ fold sentVals <> Map.keysSet permittedFiles
E.where_ $ file E.^. FileId `E.in_` E.valList (Set.toList $ fold sentVals <> Map.keysSet permittedFiles)
E.orderBy [E.asc $ file E.^. FileTitle]
return (file E.^. FileId, file E.^. FileTitle)
return $ sortOn (splitPath . fuiTitle) fileInfos' return $ sortOn (splitPath . fuiTitle) fileInfos'
@ -1063,7 +1061,7 @@ fileField = genericFileField $ return FileField
} }
specificFileField :: UploadSpecificFile -> Field Handler FileUploads specificFileField :: UploadSpecificFile -> Field Handler FileUploads
specificFileField UploadSpecificFile{..} = convertField fixupFileTitles id . genericFileField $ return FileField specificFileField UploadSpecificFile{..} = convertField (.| fixupFileTitles) id . genericFileField $ return FileField
{ fieldIdent = Nothing { fieldIdent = Nothing
, fieldUnpackZips = FileFieldUserOption True False , fieldUnpackZips = FileFieldUserOption True False
, fieldMultiple = False , fieldMultiple = False
@ -1072,25 +1070,7 @@ specificFileField UploadSpecificFile{..} = convertField fixupFileTitles id . gen
, fieldMaxFileSize = specificFileMaxSize , fieldMaxFileSize = specificFileMaxSize
} }
where where
fixupFileTitles = flip (.|) . C.mapM $ either (fmap Left . updateFileReference) (fmap Right . updateFile) fixupFileTitles = C.map $ set _fileReferenceTitle (unpack specificFileName)
where updateFileReference fId = runDB . maybeT (return fId) $ do
oldTitle <- MaybeT . fmap (getFirst . foldMap (First . Just)) . E.select . E.from $ \file -> do
E.where_ $ file E.^. FileId E.==. E.val fId
return $ file E.^. FileTitle
if | oldTitle == E.Value (unpack specificFileName)
-> return fId
| otherwise -> lift $ do
fId' <- insert $ File (unpack specificFileName) Nothing (toMidnight systemEpochDay) {- temporary -}
E.update $ \file' -> do
let newModified = E.subSelect . E.from $ \file -> do
E.where_ $ file E.^. FileId E.==. E.val fId
return $ file E.^. FileModified
newContent = E.subSelect . E.from $ \file -> do
E.where_ $ file E.^. FileId E.==. E.val fId
return $ file E.^. FileContent
E.set file' [ FileModified E.=. E.maybe (E.val $ toMidnight systemEpochDay) id newModified, FileContent E.=. E.joinV newContent ]
return fId'
updateFile = return . set _fileTitle (unpack specificFileName)
zipFileField :: Bool -- ^ Unpack zips? zipFileField :: Bool -- ^ Unpack zips?
-> Maybe (NonNull (Set Extension)) -- ^ Restrictions on file extensions -> Maybe (NonNull (Set Extension)) -- ^ Restrictions on file extensions
@ -1126,9 +1106,9 @@ fileUploadForm isReq mkFs = \case
multiFileField' :: FileUploads -- ^ Permitted files in same format as produced by `multiFileField` multiFileField' :: FileUploads -- ^ Permitted files in same format as produced by `multiFileField`
-> Field Handler FileUploads -> Field Handler FileUploads
multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.mapMaybe (preview _Left) .| C.foldMap Set.singleton multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.foldMap Set.singleton
multiFileField :: Handler (Set FileId) -- ^ Set of files that may be submitted by id-reference multiFileField :: Handler (Set FileReference) -- ^ Set of files that may be submitted by id-reference
-> Field Handler FileUploads -> Field Handler FileUploads
multiFileField mkPermitted = genericFileField $ mkField <$> mkPermitted multiFileField mkPermitted = genericFileField $ mkField <$> mkPermitted
where mkField permitted = FileField where mkField permitted = FileField
@ -1136,7 +1116,10 @@ multiFileField mkPermitted = genericFileField $ mkField <$> mkPermitted
, fieldUnpackZips = FileFieldUserOption False False , fieldUnpackZips = FileFieldUserOption False False
, fieldMultiple = True , fieldMultiple = True
, fieldRestrictExtensions = Nothing , fieldRestrictExtensions = Nothing
, fieldAdditionalFiles = Map.fromSet (const $ FileFieldUserOption False True) permitted , fieldAdditionalFiles = Map.fromList
[ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True))
| FileReference{..} <- Set.toList permitted
]
, fieldMaxFileSize = Nothing , fieldMaxFileSize = Nothing
} }

View File

@ -3,8 +3,7 @@ module Handler.Utils.Form.Types where
import Import import Import
data FileUploadInfo = FileUploadInfo data FileUploadInfo = FileUploadInfo
{ fuiId :: CryptoUUIDFile { fuiTitle :: FilePath
, fuiTitle :: FilePath
, fuiHtmlId :: Text , fuiHtmlId :: Text
, fuiChecked, fuiSession, fuiForced :: Bool , fuiChecked, fuiSession, fuiForced :: Bool
} }

View File

@ -8,6 +8,7 @@ module Handler.Utils.Mail
import Import import Import
import Handler.Utils.Pandoc import Handler.Utils.Pandoc
import Handler.Utils.Files
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
@ -69,15 +70,15 @@ userMailT uid mAct = do
addFileDB :: ( MonadMail m addFileDB :: ( MonadMail m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => FileId -> m (Maybe MailObjectId) ) => FileReference -> m (Maybe MailObjectId)
addFileDB fId = runMaybeT $ do addFileDB fRef = runMaybeT $ do
File{fileTitle = pack . takeBaseName -> fileName, fileContent = Just fileContent} <- MaybeT . liftHandler . runDB $ get fId File{fileTitle = pack . takeBaseName -> fileName, fileContent = Just fileContent} <- lift . liftHandler . runDB $ sourceFile fRef
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 (LBS.fromStrict fileContent)
setMailObjectIdCrypto fId :: StateT Part (HandlerFor UniWorX) MailObjectId setMailObjectIdPseudorandom (fileName, fileContent) :: StateT Part (HandlerFor UniWorX) MailObjectId
class YesodMail site => ToMailHtml site a where class YesodMail site => ToMailHtml site a where

View File

@ -11,6 +11,7 @@ module Handler.Utils.Rating
import Import import Import
import Handler.Utils.Files
import Handler.Utils.DateTime (getDateTimeFormatter) import Handler.Utils.DateTime (getDateTimeFormatter)
import qualified Data.Text as Text import qualified Data.Text as Text
@ -102,23 +103,23 @@ ratingFile cID rating@Rating{ ratingValues = Rating'{..}, .. } = do
return File{..} return File{..}
where ensureExtension ext fName = bool (`addExtension` ext) id (ext `isExtensionOf` fName) fName where ensureExtension ext fName = bool (`addExtension` ext) id (ext `isExtensionOf` fName) fName
type SubmissionContent = Either File (SubmissionId, Rating') type SubmissionContent = Either FileReference (SubmissionId, Rating')
extractRatings :: ( MonadHandler m extractRatings :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => ConduitT FileReference SubmissionContent m ()
) => ConduitT File SubmissionContent m () extractRatings = Conduit.mapM $ \fRef@FileReference{..} -> liftHandler $ do
extractRatings = Conduit.mapM $ \f@File{..} -> liftHandler $ do msId <- isRatingFile fileReferenceTitle
msId <- isRatingFile fileTitle if
case () of | Just sId <- msId
_ | Just sId <- msId , isJust fileReferenceContent -> do
, isJust fileContent -> do f <- runDB $ sourceFile fRef
(rating, cID) <- handle (throwM . RatingFileException fileTitle) $ parseRating f (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 fileTitle RatingSubmissionIDIncorrect throwM $ RatingFileException fileReferenceTitle RatingSubmissionIDIncorrect
return $ Right (sId, rating) return $ Right (sId, rating)
| otherwise -> return $ Left f | otherwise -> return $ Left fRef
isRatingFile :: forall m. isRatingFile :: forall m.
( MonadHandler m ( MonadHandler m

View File

@ -39,6 +39,7 @@ import qualified Database.Esqueleto.Utils.TH as E
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
import Data.Conduit.ResumableSink import Data.Conduit.ResumableSink
import qualified Data.Conduit.Combinators as C
import System.FilePath import System.FilePath
import System.FilePath.Glob import System.FilePath.Glob
@ -255,21 +256,21 @@ 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 () (Entity File) (YesodDB UniWorX) () submissionFileSource :: SubmissionId -> ConduitT () File (YesodDB UniWorX) ()
submissionFileSource = E.selectSource . fmap snd . E.from . submissionFileQuery submissionFileSource subId = E.selectSource (E.from $ submissionFileQuery subId)
.| C.map entityVal
.| sourceFiles'
submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File) submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile)
-> E.SqlQuery (E.SqlExpr (Entity SubmissionFile), E.SqlExpr (Entity File)) -> E.SqlQuery (E.SqlExpr (Entity SubmissionFile))
submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do submissionFileQuery submissionID sf = E.distinctOnOrderBy [E.asc $ sf E.^. SubmissionFileTitle] $ do
E.on $ f E.^. FileId E.==. sf E.^. SubmissionFileFile
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.where_ . E.not_ . E.exists . E.from $ \(sf' `E.InnerJoin` f') -> do E.where_ . E.not_ . E.exists . E.from $ \sf' ->
E.on $ f' E.^. FileId E.==. sf' E.^. SubmissionFileFile
E.where_ $ sf' E.^. SubmissionFileIsDeletion E.where_ $ sf' E.^. SubmissionFileIsDeletion
E.&&. sf' E.^. SubmissionFileSubmission E.==. sf E.^. SubmissionFileSubmission E.&&. sf' E.^. SubmissionFileSubmission E.==. sf E.^. SubmissionFileSubmission
E.&&. f' E.^. FileTitle E.==. f E.^. FileTitle E.&&. sf' E.^. SubmissionFileTitle E.==. sf E.^. SubmissionFileTitle
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
return (sf, f) return sf
data SubmissionDownloadAnonymous = SubmissionDownloadAnonymous data SubmissionDownloadAnonymous = SubmissionDownloadAnonymous
| SubmissionDownloadSurnames | SubmissionDownloadSurnames
@ -366,8 +367,8 @@ submissionMultiArchive anonymous (Set.toList -> ids) = do
| otherwise = submissionDirectory | otherwise = submissionDirectory
fileEntitySource = do fileEntitySource = do
submissionFileSource submissionID .| Conduit.map entityVal yieldM $ ratingFile cID rating
yieldM (ratingFile cID rating) submissionFileSource submissionID
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle } withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle }
@ -400,13 +401,13 @@ instance Monoid SubmissionSinkState where
mempty = memptydefault mempty = memptydefault
mappend = (<>) mappend = (<>)
filterSubmission :: MonadLogger m => ConduitM File File m (Set FilePath) filterSubmission :: MonadLogger m => ConduitM FileReference FileReference m (Set FilePath)
-- ^ Removes all `File`s matching `submissionBlacklist`, returning their `fileTitle`s -- ^ Removes all `File`s matching `submissionBlacklist`, returning their `fileTitle`s
filterSubmission = do filterSubmission = do
$logDebugS "filterSubmission" $ tshow submissionBlacklist $logDebugS "filterSubmission" $ tshow submissionBlacklist
execWriterLC . awaitForever $ \case execWriterLC . awaitForever $ \case
File{fileTitle} FileReference{fileReferenceTitle}
| any (`match'` fileTitle) submissionBlacklist -> tell $ Set.singleton fileTitle | any (`match'` fileReferenceTitle) submissionBlacklist -> tell $ Set.singleton fileReferenceTitle
file -> yield file file -> yield file
where where
match' = matchWith $ matchDefault match' = matchWith $ matchDefault
@ -415,12 +416,12 @@ filterSubmission = do
extractRatings :: ( MonadHandler m extractRatings :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => ConduitM File SubmissionContent m (Set FilePath) ) => ConduitM FileReference SubmissionContent m (Set FilePath)
extractRatings = filterSubmission `fuseUpstream` Rating.extractRatings extractRatings = filterSubmission `fuseUpstream` Rating.extractRatings
extractRatingsMsg :: ( MonadHandler m extractRatingsMsg :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) => ConduitT File SubmissionContent m () ) => ConduitT FileReference SubmissionContent m ()
extractRatingsMsg = do extractRatingsMsg = do
ignored' <- filterSubmission `fuseUpstream` Rating.extractRatings ignored' <- filterSubmission `fuseUpstream` Rating.extractRatings
let ignoredFiles :: Set (Either CryptoFileNameSubmission FilePath) let ignoredFiles :: Set (Either CryptoFileNameSubmission FilePath)
@ -558,10 +559,10 @@ sinkSubmission userId mExists isUpdate = do
, not isUpdate , not isUpdate
, Just (map unpack . Set.toList . toNullable -> exts) <- extensionRestriction , Just (map unpack . Set.toList . toNullable -> exts) <- extensionRestriction
= Conduit.mapM $ \x -> if = Conduit.mapM $ \x -> if
| Left File{..} <- x | Left FileReference{..} <- x
, none ((flip isExtensionOf `on` CI.foldCase) fileTitle) exts , none ((flip isExtensionOf `on` CI.foldCase) fileReferenceTitle) exts
, isn't _Nothing fileContent -- File record is not a directory, we don't care about those , isn't _Nothing fileReferenceContent -- File record is not a directory, we don't care about those
-> throwM $ InvalidFileTitleExtension fileTitle -> throwM $ InvalidFileTitleExtension fileReferenceTitle
| otherwise | otherwise
-> return x -> return x
| otherwise = Conduit.map id | otherwise = Conduit.map id
@ -569,55 +570,57 @@ sinkSubmission userId mExists isUpdate = do
sinkSubmission' :: SubmissionId sinkSubmission' :: SubmissionId
-> ConduitT SubmissionContent Void (YesodJobDB UniWorX) () -> ConduitT SubmissionContent Void (YesodJobDB UniWorX) ()
sinkSubmission' submissionId = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case sinkSubmission' submissionId = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case
Left file@(File{..}) -> do Left file@(FileReference{..}) -> do
$logDebugS "sinkSubmission" . tshow $ (submissionId, fileTitle) $logDebugS "sinkSubmission" . tshow $ (submissionId, fileReferenceTitle)
alreadySeen <- gets (Set.member fileTitle . sinkFilenames) alreadySeen <- gets (Set.member fileReferenceTitle . sinkFilenames)
when alreadySeen . throwM $ DuplicateFileTitle fileTitle when alreadySeen . throwM $ DuplicateFileTitle fileReferenceTitle
tellSt $ mempty{ sinkFilenames = Set.singleton fileTitle } tellSt $ mempty{ sinkFilenames = Set.singleton fileReferenceTitle }
otherVersions <- lift . E.select . E.from $ \(sf `E.InnerJoin` f) -> do otherVersions <- lift . E.select . E.from $ \sf -> do
E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId
-- E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate -- E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate
E.where_ $ f E.^. FileTitle E.==. E.val fileTitle -- 'Zip.hs' normalises filenames already, so this should work E.where_ $ sf E.^. SubmissionFileTitle E.==. E.val fileReferenceTitle -- 'Zip.hs' normalises filenames already, so this should work
return (f, sf) return sf
let collidingFiles = [ t | t@(_, Entity _ sf) <- otherVersions let collidingFiles = [ t | t@(Entity _ sf) <- otherVersions
, submissionFileIsUpdate sf == isUpdate , submissionFileIsUpdate sf == isUpdate
] ]
underlyingFiles = [ t | t@(_, Entity _ sf) <- otherVersions underlyingFiles = [ t | t@(Entity _ sf) <- otherVersions
, submissionFileIsUpdate sf == False , submissionFileIsUpdate sf == False
] ]
anyChanges anyChanges
| not (null collidingFiles) = any (/~ file) [ f | (Entity _ f, _) <- collidingFiles ] | not (null collidingFiles) = any (/~ file) [ view (_FileReference . _1) sf | Entity _ sf <- collidingFiles ]
| otherwise = True | otherwise = True
matchesUnderlying matchesUnderlying
| not (null underlyingFiles) = all (~~ file) [ f | (Entity _ f, Entity _ _sf) <- underlyingFiles ] | not (null underlyingFiles) = all (~~ file) [ view (_FileReference . _1) sf | Entity _ sf <- underlyingFiles ]
| otherwise = False | otherwise = False
undoneDeletion = any submissionFileIsDeletion [ sf | (_, Entity _ sf) <- collidingFiles ] undoneDeletion = any submissionFileIsDeletion [ sf | Entity _ sf <- collidingFiles ]
when anyChanges $ do when anyChanges $ do
touchSubmission touchSubmission
when (not $ null collidingFiles) $ forM_ collidingFiles $ \(Entity sfId' _) -> lift $ do
lift $ deleteCascadeWhere [ FileId <-. [ fId | (Entity fId _, _) <- collidingFiles ] ] delete sfId'
lift $ case () of audit $ TransactionSubmissionFileDelete sfId' submissionId
_ | matchesUnderlying lift $ if
, isUpdate | matchesUnderlying
-> return () , isUpdate
_ -> do -> return ()
fileId <- insert file | otherwise -> do
subFileId <- insert $ SubmissionFile subFileId <- insert $
{ submissionFileSubmission = submissionId _FileReference # ( file
, submissionFileFile = fileId , SubmissionFileResidual
, submissionFileIsUpdate = isUpdate { submissionFileResidualSubmission = submissionId
, submissionFileIsDeletion = False , submissionFileResidualIsUpdate = isUpdate
} , submissionFileResidualIsDeletion = False
audit $ TransactionSubmissionFileEdit subFileId submissionId fileId }
)
audit $ TransactionSubmissionFileEdit subFileId submissionId
when undoneDeletion $ do when undoneDeletion $ do
touchSubmission touchSubmission
lift $ forM_ [ (sfId, submissionFileFile sf) | (_, Entity sfId sf) <- collidingFiles, submissionFileIsDeletion sf ] $ \(sfId, fId) -> audit $ TransactionSubmissionFileDelete sfId submissionId fId forM_ (filter (submissionFileIsDeletion . entityVal) collidingFiles) $ \(Entity sfId' _) -> lift $ do
lift $ deleteWhere [ SubmissionFileId <-. [ sfId | (_, Entity sfId sf) <- collidingFiles, submissionFileIsDeletion sf ] ] delete sfId'
audit $ TransactionSubmissionFileDelete sfId' submissionId
Right (submissionId', r) -> do Right (submissionId', r) -> do
$logDebugS "sinkSubmission" $ tshow submissionId' $logDebugS "sinkSubmission" $ tshow submissionId'
@ -673,9 +676,10 @@ sinkSubmission userId mExists isUpdate = do
where where
a /~ b = not $ a ~~ b a /~ b = not $ a ~~ b
(~~) :: File -> File -> Bool (~~) :: FileReference -> FileReference -> Bool
(~~) a b (~~) a b
| isUpdate = fileTitle a == fileTitle b && fileContent a == fileContent b | isUpdate = fileReferenceTitle a == fileReferenceTitle b
&& fileReferenceContent a == fileReferenceContent b
| otherwise = a == b | otherwise = a == b
-- The Eq Instance for File compares modification time exactly even -- The Eq Instance for File compares modification time exactly even
-- though zip archives have very limited accuracy and range regarding -- though zip archives have very limited accuracy and range regarding
@ -710,40 +714,46 @@ sinkSubmission userId mExists isUpdate = do
finalize :: SubmissionSinkState -> YesodJobDB UniWorX () finalize :: SubmissionSinkState -> YesodJobDB UniWorX ()
finalize SubmissionSinkState{..} = do finalize SubmissionSinkState{..} = do
missingFiles <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do missingFiles <- E.select . E.from $ \sf -> E.distinctOnOrderBy [E.asc $ sf E.^. SubmissionFileTitle] $ do
E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId
when (not isUpdate) $ when (not isUpdate) $
E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate E.where_ . E.not_ $ sf E.^. SubmissionFileIsUpdate
E.where_ $ f E.^. FileTitle `E.notIn` E.valList (Set.toList sinkFilenames) E.where_ $ sf E.^. SubmissionFileTitle `E.notIn` E.valList (Set.toList sinkFilenames)
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
return (f, sf) return sf
case isUpdate of case isUpdate of
False -> deleteCascadeWhere [ FileId <-. [ fileId | (Entity fileId _, _) <- missingFiles ] ] False -> do
True -> forM_ missingFiles $ \(Entity fileId File{..}, Entity sfId SubmissionFile{..}) -> do shadowed <- selectKeysList
shadowing <- E.select . E.from $ \(sf `E.InnerJoin` f) -> do [ SubmissionFileSubmission ==. submissionId
E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId , SubmissionFileIsUpdate ==. False
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionId , SubmissionFileId <-. map entityKey missingFiles
E.where_ $ sf E.^. SubmissionFileIsUpdate E.==. E.val (not isUpdate) ] []
E.where_ $ f E.^. FileTitle E.==. E.val fileTitle forM_ shadowed $ \sfId' -> do
return $ f E.^. FileId delete sfId'
audit $ TransactionSubmissionFileDelete sfId' submissionId
True -> forM_ missingFiles $ \(Entity sfId SubmissionFile{..}) -> do
shadowing <- existsBy $ UniqueSubmissionFile submissionFileSubmission submissionFileTitle False
case (shadowing, submissionFileIsUpdate) of if
([], _) -> deleteCascade fileId | not shadowing -> do
(E.Value f:_, False) -> do delete sfId
sfId' <- insert $ SubmissionFile audit $ TransactionSubmissionFileDelete sfId submissionId
{ submissionFileSubmission = submissionId | submissionFileIsUpdate -> do
, submissionFileFile = f update sfId [ SubmissionFileContent =. Nothing, SubmissionFileIsDeletion =. True ]
, submissionFileIsUpdate = True audit $ TransactionSubmissionFileEdit sfId submissionId
, submissionFileIsDeletion = True | otherwise -> do
} now <- liftIO getCurrentTime
audit $ TransactionSubmissionFileEdit sfId' submissionId f sfId' <- insert $ SubmissionFile
(E.Value f:_, True) -> do { submissionFileSubmission = submissionId
update sfId [ SubmissionFileFile =. f, SubmissionFileIsDeletion =. True ] , submissionFileTitle
deleteCascade fileId , submissionFileModified = now
audit $ TransactionSubmissionFileDelete sfId submissionId f , submissionFileContent = Nothing
, submissionFileIsUpdate = True
, submissionFileIsDeletion = True
}
audit $ TransactionSubmissionFileEdit sfId' submissionId
if if
| isUpdate | isUpdate
@ -805,7 +815,7 @@ sinkMultiSubmission userId isUpdate = do
cID <- encrypt sId cID <- encrypt sId
$logDebugS "sinkMultiSubmission" $ "Feeding rating for " <> toPathPiece cID $logDebugS "sinkMultiSubmission" $ "Feeding rating for " <> toPathPiece cID
lift (feed sId v `catches` [ E.Handler (throwM . SubmissionSinkException cID Nothing), E.Handler (void . handleHCError (Left cID)) ]) lift (feed sId v `catches` [ E.Handler (throwM . SubmissionSinkException cID Nothing), E.Handler (void . handleHCError (Left cID)) ])
(Left f@File{..}) -> do (Left f@FileReference{..}) -> do
let let
acc :: (Maybe SubmissionId, [FilePath]) -> FilePath -> _ (Maybe SubmissionId, [FilePath]) acc :: (Maybe SubmissionId, [FilePath]) -> FilePath -> _ (Maybe SubmissionId, [FilePath])
acc (Just sId, fp) segment = return (Just sId, fp ++ [segment]) acc (Just sId, fp) segment = return (Just sId, fp ++ [segment])
@ -817,17 +827,17 @@ sinkMultiSubmission userId isUpdate = do
sId <- decrypt (cID :: CryptoFileNameSubmission) sId <- decrypt (cID :: CryptoFileNameSubmission)
Just sId <$ get404 sId Just sId <$ get404 sId
| otherwise = return Nothing | otherwise = return Nothing
Dual (Alt msId) <- lift . flip foldMapM segments' $ \seg -> Dual . Alt <$> lift (tryDecrypt seg) `catches` [ E.Handler handleCryptoID, E.Handler (handleHCError $ Right fileTitle) ] Dual (Alt msId) <- lift . flip foldMapM segments' $ \seg -> Dual . Alt <$> lift (tryDecrypt seg) `catches` [ E.Handler handleCryptoID, E.Handler (handleHCError $ Right fileReferenceTitle) ]
return (msId, fp) return (msId, fp)
(msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileTitle (msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileReferenceTitle
case msId of case msId of
Nothing -> do Nothing -> do
$logDebugS "sinkMultiSubmission" $ "Dropping " <> tshow (splitDirectories fileTitle, msId, fileTitle') $logDebugS "sinkMultiSubmission" $ "Dropping " <> tshow (splitDirectories fileReferenceTitle, msId, fileTitle')
Just sId -> do Just sId -> do
$logDebugS "sinkMultiSubmission" $ "Feeding " <> tshow (splitDirectories fileTitle, msId, fileTitle') $logDebugS "sinkMultiSubmission" $ "Feeding " <> tshow (splitDirectories fileReferenceTitle, msId, fileTitle')
cID <- encrypt sId cID <- encrypt sId
lift . handle (throwM . SubmissionSinkException cID (Just fileTitle)) $ lift . handle (throwM . SubmissionSinkException cID (Just fileReferenceTitle)) $
feed sId $ Left f{ fileTitle = fileTitle' } feed sId $ Left f{ fileReferenceTitle = fileTitle' }
when (not $ null ignoredFiles) $ do when (not $ null ignoredFiles) $ do
mr <- (toHtml .) <$> getMessageRender mr <- (toHtml .) <$> getMessageRender
addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr) addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr)

View File

@ -417,11 +417,11 @@ colFileModificationWhen condition row2time = sortable (Just "time") (i18nCell Ms
where conDTCell = ifCell condition dateTimeCell $ const mempty where conDTCell = ifCell condition dateTimeCell $ const mempty
sortFilePath :: IsString s => (t -> E.SqlExpr (Entity File)) -> (s, SortColumn t r') sortFilePath :: (HasFileReference record, IsString s) => (t -> E.SqlExpr (Entity record)) -> (s, SortColumn t r')
sortFilePath queryPath = ("path", SortColumn $ queryPath >>> (E.^. FileTitle)) sortFilePath queryPath = ("path", SortColumn $ queryPath >>> (E.^. fileReferenceTitleField))
sortFileModification :: IsString s => (t -> E.SqlExpr (Entity File)) -> (s, SortColumn t r') sortFileModification :: (HasFileReference record, IsString s) => (t -> E.SqlExpr (Entity record)) -> (s, SortColumn t r')
sortFileModification queryModification = ("time", SortColumn $ queryModification >>> (E.^. FileModified)) sortFileModification queryModification = ("time", SortColumn $ queryModification >>> (E.^. fileReferenceModifiedField))
defaultSortingByFileTitle :: PSValidator m x -> PSValidator m x defaultSortingByFileTitle :: PSValidator m x -> PSValidator m x
defaultSortingByFileTitle = defaultSorting [SortAscBy "path"] defaultSortingByFileTitle = defaultSorting [SortAscBy "path"]

View File

@ -8,7 +8,7 @@ module Handler.Utils.Zip
, produceZip , produceZip
, consumeZip , consumeZip
, modifyFileTitle , modifyFileTitle
, sourceFiles, acceptFile , receiveFiles, acceptFile
) where ) where
import Import import Import
@ -123,8 +123,8 @@ modifyFileTitle :: Monad m => (FilePath -> FilePath) -> ConduitT File File 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
sourceFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadBase IO m) => FileInfo -> ConduitT () File m () receiveFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadBase IO m) => FileInfo -> ConduitT () File m ()
sourceFiles 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 fileSource fInfo .| void consumeZip

View File

@ -7,5 +7,6 @@ import Import.NoFoundation as Import
import Utils.SystemMessage as Import import Utils.SystemMessage as Import
import Utils.Metrics as Import import Utils.Metrics as Import
import Utils.Files as Import
import Jobs.Types as Import (JobHandler(..)) import Jobs.Types as Import (JobHandler(..))

View File

@ -19,6 +19,7 @@ import ClassyPrelude.Yesod as Import
, try, embed, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_ , try, embed, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_
, htmlField, fileField , htmlField, fileField
, mreq, areq, wreq -- Use `mreqMsg`, `areqMsg`, `wreqMsg` , mreq, areq, wreq -- Use `mreqMsg`, `areqMsg`, `wreqMsg`
, sinkFile, sourceFile
) )
import UnliftIO.Async.Utils as Import import UnliftIO.Async.Utils as Import
@ -161,7 +162,7 @@ import Web.Cookie.Instances as Import ()
import Network.HTTP.Types.Method.Instances as Import () import Network.HTTP.Types.Method.Instances as Import ()
import Crypto.Random.Instances as Import () import Crypto.Random.Instances as Import ()
import Crypto.Hash as Import (Digest, SHA3_256) import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
import Crypto.Random as Import (ChaChaDRG, Seed) import Crypto.Random as Import (ChaChaDRG, Seed)
import Control.Lens as Import import Control.Lens as Import

View File

@ -21,18 +21,18 @@ dispatchJobPruneSessionFiles = JobHandlerAtomic . hoist lift $ do
dispatchJobPruneUnreferencedFiles :: JobHandler UniWorX dispatchJobPruneUnreferencedFiles :: JobHandler UniWorX
dispatchJobPruneUnreferencedFiles = JobHandlerAtomic . hoist lift $ do dispatchJobPruneUnreferencedFiles = JobHandlerAtomic . hoist lift $ do
n <- E.deleteCount . E.from $ \file -> n <- E.deleteCount . E.from $ \fileContent ->
E.where_ . E.not_ . E.any E.exists $ references file E.where_ . E.not_ . E.any E.exists $ references fileContent
$logInfoS "PruneUnreferencedFiles" [st|Deleted #{n} unreferenced files|] $logInfoS "PruneUnreferencedFiles" [st|Deleted #{n} unreferenced files|]
where where
references :: E.SqlExpr (Entity File) -> [E.SqlQuery ()] references :: E.SqlExpr (Entity FileContent) -> [E.SqlQuery ()]
references ((E.^. FileId) -> fId) = references (E.just . (E.^. FileContentHash) -> fHash) =
[ E.from $ \appFile -> E.where_ $ appFile E.^. CourseApplicationFileFile E.==. fId [ E.from $ \appFile -> E.where_ $ appFile E.^. CourseApplicationFileContent E.==. fHash
, E.from $ \matFile -> E.where_ $ matFile E.^. MaterialFileFile E.==. fId , E.from $ \matFile -> E.where_ $ matFile E.^. MaterialFileContent E.==. fHash
, E.from $ \newsFile -> E.where_ $ newsFile E.^. CourseNewsFileFile E.==. fId , E.from $ \newsFile -> E.where_ $ newsFile E.^. CourseNewsFileContent E.==. fHash
, E.from $ \sheetFile -> E.where_ $ sheetFile E.^. SheetFileFile E.==. fId , E.from $ \sheetFile -> E.where_ $ sheetFile E.^. SheetFileContent E.==. fHash
, E.from $ \appInstr -> E.where_ $ appInstr E.^. CourseAppInstructionFileFile E.==. fId , E.from $ \appInstr -> E.where_ $ appInstr E.^. CourseAppInstructionFileContent E.==. fHash
, E.from $ \matching -> E.where_ $ matching E.^. AllocationMatchingLog E.==. fId , E.from $ \matching -> E.where_ $ E.just (matching E.^. AllocationMatchingLog) E.==. fHash
, E.from $ \subFile -> E.where_ $ subFile E.^. SubmissionFileFile E.==. fId , E.from $ \subFile -> E.where_ $ subFile E.^. SubmissionFileContent E.==. fHash
, E.from $ \sessFile -> E.where_ $ sessFile E.^. SessionFileFile E.==. fId , E.from $ \sessFile -> E.where_ $ sessFile E.^. SessionFileContent E.==. fHash
] ]

View File

@ -38,8 +38,6 @@ deriving newtype instance ToJSONKey UserId
deriving newtype instance FromJSONKey UserId deriving newtype instance FromJSONKey UserId
deriving newtype instance ToJSONKey ExamOccurrenceId deriving newtype instance ToJSONKey ExamOccurrenceId
deriving newtype instance FromJSONKey ExamOccurrenceId deriving newtype instance FromJSONKey ExamOccurrenceId
deriving newtype instance ToJSONKey FileId
deriving newtype instance FromJSONKey FileId
-- ToMarkup and ToMessage instances for displaying selected database primary keys -- ToMarkup and ToMessage instances for displaying selected database primary keys
@ -54,3 +52,185 @@ instance ToMarkup (Key Term) where
instance ToMessage (Key Term) where instance ToMessage (Key Term) where
toMessage = termToText . unTermKey toMessage = termToText . unTermKey
instance HasFileReference CourseApplicationFile where
newtype FileReferenceResidual CourseApplicationFile
= CourseApplicationFileResidual { courseApplicationFileResidualApplication :: CourseApplicationId }
_FileReference
= iso (\CourseApplicationFile{..} -> ( FileReference
{ fileReferenceTitle = courseApplicationFileTitle
, fileReferenceContent = courseApplicationFileContent
, fileReferenceModified = courseApplicationFileModified
}
, CourseApplicationFileResidual courseApplicationFileApplication
)
)
(\( FileReference{..}
, CourseApplicationFileResidual courseApplicationFileApplication
) -> CourseApplicationFile
{ courseApplicationFileApplication
, courseApplicationFileTitle = fileReferenceTitle
, courseApplicationFileContent = fileReferenceContent
, courseApplicationFileModified = fileReferenceModified
}
)
fileReferenceTitleField = CourseApplicationFileTitle
fileReferenceContentField = CourseApplicationFileContent
fileReferenceModifiedField = CourseApplicationFileModified
instance HasFileReference CourseAppInstructionFile where
newtype FileReferenceResidual CourseAppInstructionFile
= CourseAppInstructionFileResidual { courseAppInstructionFileResidualCourse :: CourseId }
_FileReference
= iso (\CourseAppInstructionFile{..} -> ( FileReference
{ fileReferenceTitle = courseAppInstructionFileTitle
, fileReferenceContent = courseAppInstructionFileContent
, fileReferenceModified = courseAppInstructionFileModified
}
, CourseAppInstructionFileResidual courseAppInstructionFileCourse
)
)
(\( FileReference{..}
, CourseAppInstructionFileResidual courseAppInstructionFileCourse
) -> CourseAppInstructionFile
{ courseAppInstructionFileCourse
, courseAppInstructionFileTitle = fileReferenceTitle
, courseAppInstructionFileContent = fileReferenceContent
, courseAppInstructionFileModified = fileReferenceModified
}
)
fileReferenceTitleField = CourseAppInstructionFileTitle
fileReferenceContentField = CourseAppInstructionFileContent
fileReferenceModifiedField = CourseAppInstructionFileModified
instance HasFileReference SheetFile where
data FileReferenceResidual SheetFile = SheetFileResidual
{ sheetFileResidualSheet :: SheetId
, sheetFileResidualType :: SheetFileType
}
_FileReference
= iso (\SheetFile{..} -> ( FileReference
{ fileReferenceTitle = sheetFileTitle
, fileReferenceContent = sheetFileContent
, fileReferenceModified = sheetFileModified
}
, SheetFileResidual
{ sheetFileResidualSheet = sheetFileSheet
, sheetFileResidualType = sheetFileType
}
)
)
(\( FileReference{..}
, SheetFileResidual{..}
) -> SheetFile
{ sheetFileSheet = sheetFileResidualSheet
, sheetFileType = sheetFileResidualType
, sheetFileTitle = fileReferenceTitle
, sheetFileContent = fileReferenceContent
, sheetFileModified = fileReferenceModified
}
)
fileReferenceTitleField = SheetFileTitle
fileReferenceContentField = SheetFileContent
fileReferenceModifiedField = SheetFileModified
instance HasFileReference SubmissionFile where
data FileReferenceResidual SubmissionFile = SubmissionFileResidual
{ submissionFileResidualSubmission :: SubmissionId
, submissionFileResidualIsUpdate
, submissionFileResidualIsDeletion :: Bool
}
_FileReference
= iso (\SubmissionFile{..} -> ( FileReference
{ fileReferenceTitle = submissionFileTitle
, fileReferenceContent = submissionFileContent
, fileReferenceModified = submissionFileModified
}
, SubmissionFileResidual
{ submissionFileResidualSubmission = submissionFileSubmission
, submissionFileResidualIsUpdate = submissionFileIsUpdate
, submissionFileResidualIsDeletion = submissionFileIsDeletion
}
)
)
(\( FileReference{..}
, SubmissionFileResidual{..}
) -> SubmissionFile
{ submissionFileSubmission = submissionFileResidualSubmission
, submissionFileIsUpdate = submissionFileResidualIsUpdate
, submissionFileIsDeletion = submissionFileResidualIsDeletion
, submissionFileTitle = fileReferenceTitle
, submissionFileContent = fileReferenceContent
, submissionFileModified = fileReferenceModified
}
)
fileReferenceTitleField = SubmissionFileTitle
fileReferenceContentField = SubmissionFileContent
fileReferenceModifiedField = SubmissionFileModified
instance HasFileReference CourseNewsFile where
newtype FileReferenceResidual CourseNewsFile
= CourseNewsFileResidual { courseNewsFileResidualNews :: CourseNewsId }
_FileReference
= iso (\CourseNewsFile{..} -> ( FileReference
{ fileReferenceTitle = courseNewsFileTitle
, fileReferenceContent = courseNewsFileContent
, fileReferenceModified = courseNewsFileModified
}
, CourseNewsFileResidual courseNewsFileNews
)
)
(\( FileReference{..}
, CourseNewsFileResidual courseNewsFileNews
) -> CourseNewsFile
{ courseNewsFileNews
, courseNewsFileTitle = fileReferenceTitle
, courseNewsFileContent = fileReferenceContent
, courseNewsFileModified = fileReferenceModified
}
)
fileReferenceTitleField = CourseNewsFileTitle
fileReferenceContentField = CourseNewsFileContent
fileReferenceModifiedField = CourseNewsFileModified
instance HasFileReference MaterialFile where
data FileReferenceResidual MaterialFile = MaterialFileResidual
{ materialFileResidualMaterial :: MaterialId
}
_FileReference
= iso (\MaterialFile{..} -> ( FileReference
{ fileReferenceTitle = materialFileTitle
, fileReferenceContent = materialFileContent
, fileReferenceModified = materialFileModified
}
, MaterialFileResidual
{ materialFileResidualMaterial = materialFileMaterial
}
)
)
(\( FileReference{..}
, MaterialFileResidual{..}
) -> MaterialFile
{ materialFileMaterial = materialFileResidualMaterial
, materialFileTitle = fileReferenceTitle
, materialFileContent = fileReferenceContent
, materialFileModified = fileReferenceModified
}
)
fileReferenceTitleField = MaterialFileTitle
fileReferenceContentField = MaterialFileContent
fileReferenceModifiedField = MaterialFileModified

View File

@ -426,7 +426,7 @@ customMigrations = Map.fromListWith (>>)
|] |]
let getFileEntries = rawQuery [st|SELECT "allocation_course_file"."id", "allocation_course"."course", "allocation_course_file"."file" FROM "allocation_course_file" INNER JOIN "allocation_course" ON "allocation_course"."id" = "allocation_course_file"."allocation_course"|] [] let getFileEntries = rawQuery [st|SELECT "allocation_course_file"."id", "allocation_course"."course", "allocation_course_file"."file" FROM "allocation_course_file" INNER JOIN "allocation_course" ON "allocation_course"."id" = "allocation_course_file"."allocation_course"|] []
moveFileEntry [fromPersistValue -> Right (acfId :: Int64), fromPersistValue -> Right (cid :: CourseId), fromPersistValue -> Right (fid :: FileId)] = moveFileEntry [fromPersistValue -> Right (acfId :: Int64), fromPersistValue -> Right (cid :: CourseId), fromPersistValue -> Right (fid :: Int64)] =
[executeQQ| [executeQQ|
INSERT INTO "course_app_instruction_file" ("course", "file") VALUES (#{cid}, #{fid}); INSERT INTO "course_app_instruction_file" ("course", "file") VALUES (#{cid}, #{fid});
DELETE FROM "allocation_course_file" WHERE "id" = #{acfId}; DELETE FROM "allocation_course_file" WHERE "id" = #{acfId};
@ -662,6 +662,191 @@ customMigrations = Map.fromListWith (>>)
, whenM (tableExists "session_file") $ , whenM (tableExists "session_file") $
tableDropEmpty "session_file" tableDropEmpty "session_file"
) )
, ( AppliedMigrationKey [migrationVersion|37.0.0|] [version|38.0.0|]
, do
unlessM (tableExists "file_content") $
[executeQQ|
CREATE TABLE "file_content" ( PRIMARY KEY ("hash")
, "hash" BYTEA NOT NULL
, "content" BYTEA NOT NULL
);
|]
let
migrateFromFile :: forall fRef.
( HasFileReference fRef
, PersistRecordBackend fRef SqlBackend
)
=> ([PersistValue] -> (Key fRef, FileReferenceResidual fRef))
-> (Entity fRef -> ReaderT SqlBackend m ())
-> [PersistValue]
-> ReaderT SqlBackend m ()
migrateFromFile toResidual doUpdate ((fromPersistValue -> Right (fId :: Int64)):rest) = do
let (fRefKey, residual) = toResidual rest
fileDat <- [sqlQQ|
SELECT "file".title, "file".modified FROM "file" WHERE "id" = #{fId};
|]
insertedContent <- [sqlQQ|
INSERT INTO "file_content" (SELECT digest("content", 'sha3-512') as "hash", "content" FROM "file" WHERE id = #{fId} AND NOT ("content" IS NULL)) ON CONFLICT("hash") DO UPDATE SET "hash" = EXCLUDED."hash" RETURNING "hash";
|]
forM_ fileDat $ \case
(fromPersistValue . unSingle -> Right (fileReferenceTitle' :: FilePath), fromPersistValue . unSingle -> Right (fileReferenceModified :: UTCTime)) -> do
let fileReferenceContent = listToMaybe $ mapMaybe (preview _Right . fromPersistValue . unSingle) insertedContent
fileRef fileReferenceTitle = _FileReference # (FileReference{..}, residual)
candidateTitles = fileReferenceTitle' : [ fName <.> ("old-" <> show n) <.> ext | n <- [1..1000] ]
where (fName, ext) = splitExtension fileReferenceTitle'
validTitles <- dropWhileM (fmap (is _Just) . checkUnique . fileRef) candidateTitles
case validTitles of
fTitle : _ -> doUpdate . Entity fRefKey $ fileRef fTitle
_other -> error "Could not make validTitle"
_other -> return ()
migrateFromFile _ _ _ = return ()
whenM (tableExists "submission_file") $ do
[executeQQ|
ALTER TABLE "submission_file" ADD COLUMN "title" VARCHAR;
ALTER TABLE "submission_file" ADD COLUMN "content" BYTEA NULL;
ALTER TABLE "submission_file" ADD COLUMN "modified" TIMESTAMP WITH TIME ZONE;
ALTER TABLE "submission_file" DROP CONSTRAINT "unique_submission_file";
ALTER TABLE "submission_file" ADD CONSTRAINT "unique_submission_file" UNIQUE("submission", "title", "is_update");
|]
let getSubmissionFiles = [queryQQ|SELECT "file", "submission_file"."id", "submission", "is_update", "is_deletion" FROM "submission_file" LEFT OUTER JOIN "file" ON "submission_file"."file" = "file".id ORDER BY "file"."modified" DESC;|]
toResidual [ fromPersistValue -> Right sfId
, fromPersistValue -> Right submissionFileResidualSubmission
, fromPersistValue -> Right submissionFileResidualIsUpdate
, fromPersistValue -> Right submissionFileResidualIsDeletion
]
= (sfId, SubmissionFileResidual{..})
toResidual _ = error "Could not convert SubmissionFile to residual"
runConduit $ getSubmissionFiles .| C.mapM_ (migrateFromFile @SubmissionFile toResidual replaceEntity)
[executeQQ|
ALTER TABLE "submission_file" DROP COLUMN "file";
|]
whenM (tableExists "sheet_file") $ do
[executeQQ|
ALTER TABLE "sheet_file" ADD COLUMN "title" VARCHAR;
ALTER TABLE "sheet_file" ADD COLUMN "content" BYTEA NULL;
ALTER TABLE "sheet_file" ADD COLUMN "modified" TIMESTAMP WITH TIME ZONE;
ALTER TABLE "sheet_file" DROP CONSTRAINT "unique_sheet_file";
ALTER TABLE "sheet_file" ADD CONSTRAINT "unique_sheet_file" UNIQUE("sheet", "type", "title");
|]
let getSheetFiles = [queryQQ|SELECT "file", "sheet_file"."id", "sheet", "type" FROM "sheet_file" LEFT OUTER JOIN "file" ON "sheet_file"."file" = "file".id ORDER BY "file"."modified" DESC;|]
toResidual [ fromPersistValue -> Right shfId
, fromPersistValue -> Right sheetFileResidualSheet
, fromPersistValue -> Right sheetFileResidualType
]
= (shfId, SheetFileResidual{..})
toResidual _ = error "Could not convert SheetFile to residual"
runConduit $ getSheetFiles .| C.mapM_ (migrateFromFile @SheetFile toResidual replaceEntity)
[executeQQ|
ALTER TABLE "sheet_file" DROP COLUMN "file";
|]
whenM (tableExists "course_app_instruction_file") $ do
[executeQQ|
ALTER TABLE "course_app_instruction_file" ADD COLUMN "title" VARCHAR;
ALTER TABLE "course_app_instruction_file" ADD COLUMN "content" BYTEA NULL;
ALTER TABLE "course_app_instruction_file" ADD COLUMN "modified" TIMESTAMP WITH TIME ZONE;
ALTER TABLE "course_app_instruction_file" DROP CONSTRAINT "unique_course_app_instruction_file";
ALTER TABLE "course_app_instruction_file" ADD CONSTRAINT "unique_course_app_instruction_file" UNIQUE("course", "title");
|]
let getCourseAppInstructionFiles = [queryQQ|SELECT "file", "course_app_instruction_file"."id", "course" FROM "course_app_instruction_file" LEFT OUTER JOIN "file" ON "course_app_instruction_file"."file" = "file".id ORDER BY "file"."modified" DESC;|]
toResidual [ fromPersistValue -> Right caifId
, fromPersistValue -> Right courseAppInstructionFileResidualCourse
]
= (caifId, CourseAppInstructionFileResidual{..})
toResidual _ = error "Could not convert CourseAppInstructionFile to residual"
runConduit $ getCourseAppInstructionFiles .| C.mapM_ (migrateFromFile @CourseAppInstructionFile toResidual replaceEntity)
[executeQQ|
ALTER TABLE "course_app_instruction_file" DROP COLUMN "file";
|]
whenM (tableExists "course_news_file") $ do
[executeQQ|
ALTER TABLE "course_news_file" ADD COLUMN "title" VARCHAR;
ALTER TABLE "course_news_file" ADD COLUMN "content" BYTEA NULL;
ALTER TABLE "course_news_file" ADD COLUMN "modified" TIMESTAMP WITH TIME ZONE;
ALTER TABLE "course_news_file" DROP CONSTRAINT "unique_course_news_file";
ALTER TABLE "course_news_file" ADD CONSTRAINT "unique_course_news_file" UNIQUE("news", "title");
|]
let getCourseNewsFiles = [queryQQ|SELECT "file", "course_news_file"."id", "news" FROM "course_news_file" LEFT OUTER JOIN "file" ON "course_news_file"."file" = "file".id ORDER BY "file"."modified" DESC;|]
toResidual [ fromPersistValue -> Right cnfId
, fromPersistValue -> Right courseNewsFileResidualNews
]
= (cnfId, CourseNewsFileResidual{..})
toResidual _ = error "Could not convert CourseNewsFile to residual"
runConduit $ getCourseNewsFiles .| C.mapM_ (migrateFromFile @CourseNewsFile toResidual replaceEntity)
[executeQQ|
ALTER TABLE "course_news_file" DROP COLUMN "file";
|]
whenM (tableExists "material_file") $ do
[executeQQ|
ALTER TABLE "material_file" ADD COLUMN "title" VARCHAR;
ALTER TABLE "material_file" ADD COLUMN "content" BYTEA NULL;
ALTER TABLE "material_file" ADD COLUMN "modified" TIMESTAMP WITH TIME ZONE;
ALTER TABLE "material_file" DROP CONSTRAINT "unique_material_file";
ALTER TABLE "material_file" ADD CONSTRAINT "unique_material_file" UNIQUE("material", "title");
|]
let getMaterialFiles = [queryQQ|SELECT "file", "material_file"."id", "material" FROM "material_file" LEFT OUTER JOIN "file" ON "material_file"."file" = "file".id ORDER BY "file"."modified" DESC;|]
toResidual [ fromPersistValue -> Right shfId
, fromPersistValue -> Right materialFileResidualMaterial
]
= (shfId, MaterialFileResidual{..})
toResidual _ = error "Could not convert MaterialFile to residual"
runConduit $ getMaterialFiles .| C.mapM_ (migrateFromFile @MaterialFile toResidual replaceEntity)
[executeQQ|
ALTER TABLE "material_file" DROP COLUMN "file";
|]
whenM (tableExists "course_application_file") $ do
[executeQQ|
ALTER TABLE "course_application_file" ADD COLUMN "title" VARCHAR;
ALTER TABLE "course_application_file" ADD COLUMN "content" BYTEA NULL;
ALTER TABLE "course_application_file" ADD COLUMN "modified" TIMESTAMP WITH TIME ZONE;
ALTER TABLE "course_application_file" DROP CONSTRAINT "unique_application_file";
ALTER TABLE "course_application_file" ADD CONSTRAINT "unique_course_application_file" UNIQUE("application", "title");
|]
let getCourseApplicationFiles = [queryQQ|SELECT "file", "course_application_file"."id", "application" FROM "course_application_file" LEFT OUTER JOIN "file" ON "course_application_file"."file" = "file".id ORDER BY "file"."modified" DESC;|]
toResidual [ fromPersistValue -> Right cnfId
, fromPersistValue -> Right courseApplicationFileResidualApplication
]
= (cnfId, CourseApplicationFileResidual{..})
toResidual _ = error "Could not convert CourseApplicationFile to residual"
runConduit $ getCourseApplicationFiles .| C.mapM_ (migrateFromFile @CourseApplicationFile toResidual replaceEntity)
[executeQQ|
ALTER TABLE "course_application_file" DROP COLUMN "file";
|]
whenM (tableExists "allocation_matching") $ do
[executeQQ|
ALTER TABLE "allocation_matching" ADD COLUMN "log_ref" BYTEA;
|]
let getAllocationMatchingFiles = [queryQQ|SELECT "log", "allocation_matching"."id" FROM "allocation_matching";|]
moveMatchingFile [ fromPersistValue -> Right (fId :: Int64), fromPersistValue -> Right (amId :: AllocationMatchingId) ] = do
insertedContent <- [sqlQQ|
INSERT INTO "file_content" (SELECT digest("content", 'sha3-512') as "hash", "content" FROM "file" WHERE id = #{fId} AND NOT ("content" IS NULL)) ON CONFLICT("hash") DO UPDATE SET "hash" = EXCLUDED."hash" RETURNING "hash";
|]
let refContent :: Maybe FileContentReference
refContent = listToMaybe $ mapMaybe (preview _Right . fromPersistValue . unSingle) insertedContent
case refContent of
Nothing -> error "AllocationMatching had no fileContent"
Just h -> [executeQQ|UPDATE "allocation_matching" SET "log_ref" = #{h} WHERE "id" = #{amId};|]
moveMatchingFile _ = return ()
runConduit $ getAllocationMatchingFiles .| C.mapM_ moveMatchingFile
[executeQQ|
ALTER TABLE "allocation_matching" DROP COLUMN "log";
ALTER TABLE "allocation_matching" RENAME COLUMN "log_ref" TO "log";
|]
whenM (tableExists "session_file") $
[executeQQ|
TRUNCATE TABLE "session_file";
ALTER TABLE "session_file" DROP COLUMN "file";
ALTER TABLE "session_file" ADD COLUMN "content" BYTEA NULL;
|]
)
] ]

View File

@ -15,3 +15,4 @@ import Model.Types.Misc as Types
import Model.Types.School as Types import Model.Types.School as Types
import Model.Types.Allocation as Types import Model.Types.Allocation as Types
import Model.Types.Languages as Types import Model.Types.Languages as Types
import Model.Types.File as Types

View File

@ -55,4 +55,4 @@ type ClusterId = UUID
type TokenId = UUID type TokenId = UUID
type TermCandidateIncidence = UUID type TermCandidateIncidence = UUID
type SessionFileReference = Digest SHA3_256 type FileContentReference = Digest SHA3_512

37
src/Model/Types/File.hs Normal file
View File

@ -0,0 +1,37 @@
module Model.Types.File
( File(..), _fileTitle, _fileContent, _fileModified
, FileReference(..), _fileReferenceTitle, _fileReferenceContent, _fileReferenceModified
, HasFileReference(..)
) where
import Import.NoModel
import Model.Types.Common (FileContentReference)
import Utils.Lens.TH
data File = File
{ fileTitle :: FilePath
, fileContent :: Maybe ByteString
, fileModified :: UTCTime
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeLenses_ ''File
data FileReference = FileReference
{ fileReferenceTitle :: FilePath
, fileReferenceContent :: Maybe FileContentReference
, fileReferenceModified :: UTCTime
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeLenses_ ''FileReference
class PersistEntity record => HasFileReference record where
data FileReferenceResidual record :: *
_FileReference :: Iso' record (FileReference, FileReferenceResidual record)
fileReferenceTitleField :: EntityField record FilePath
fileReferenceContentField :: EntityField record (Maybe FileContentReference)
fileReferenceModifiedField :: EntityField record UTCTime

View File

@ -43,10 +43,10 @@ import qualified Data.List as List
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import qualified Data.Vector as V import qualified Data.Vector as V
-- import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C (mapMaybe)
import qualified Data.Conduit.Combinators as C import qualified Data.Conduit.Combinators as C
import Control.Lens import Control.Lens hiding (uncons)
import Control.Lens as Utils (none) import Control.Lens as Utils (none)
import Control.Lens.Extras (is) import Control.Lens.Extras (is)
import Data.Set.Lens import Data.Set.Lens
@ -434,6 +434,12 @@ nonEmpty' = maybe empty pure . nonEmpty
nubOn :: Eq b => (a -> b) -> [a] -> [a] nubOn :: Eq b => (a -> b) -> [a] -> [a]
nubOn = List.nubBy . ((==) `on`) nubOn = List.nubBy . ((==) `on`)
dropWhileM :: (IsSequence seq, Monad m) => (Element seq -> m Bool) -> seq -> m seq
dropWhileM p xs'
| Just (x, xs) <- uncons xs'
= bool (return xs') (dropWhileM p xs) =<< p x
| otherwise = return xs'
---------- ----------
-- Sets -- -- Sets --
---------- ----------
@ -831,6 +837,9 @@ allMC f = C.mapM f .| andC
yieldMMany :: forall mono m a. (Monad m, MonoFoldable mono) => m mono -> ConduitT a (Element mono) m () yieldMMany :: forall mono m a. (Monad m, MonoFoldable mono) => m mono -> ConduitT a (Element mono) m ()
yieldMMany = C.yieldMany <=< lift yieldMMany = C.yieldMany <=< lift
eitherC :: Monad m => ConduitT l o m () -> ConduitT r o m () -> ConduitT (Either l r) o m ()
eitherC lC rC = void $ sequenceConduits [C.mapMaybe (preview _Left) .| lC, C.mapMaybe (preview _Right) .| rC]
----------------- -----------------
-- Alternative -- -- Alternative --
----------------- -----------------

View File

@ -107,6 +107,13 @@ myReplaceUnique key datumNew = getJust key >>= replaceOriginal
changedKeys = uniqueKeysNew List.\\ uniqueKeysOriginal changedKeys = uniqueKeysNew List.\\ uniqueKeysOriginal
uniqueKeysOriginal = persistUniqueKeys original uniqueKeysOriginal = persistUniqueKeys original
replaceEntity :: ( MonadIO m
, PersistRecordBackend record backend
, PersistStoreWrite backend
)
=> Entity record -> ReaderT backend m ()
replaceEntity Entity{..} = replace entityKey entityVal
checkUniqueKeys :: ( MonadIO m checkUniqueKeys :: ( MonadIO m
, PersistUniqueRead backend , PersistUniqueRead backend
, PersistRecordBackend record backend , PersistRecordBackend record backend

36
src/Utils/Files.hs Normal file
View File

@ -0,0 +1,36 @@
module Utils.Files
( sinkFile, sinkFiles
, sinkFile', sinkFiles'
) where
import Import.NoFoundation
import qualified Crypto.Hash as Crypto (hash)
import qualified Data.Conduit.Combinators as C
sinkFiles :: MonadIO m => ConduitT File FileReference (SqlPersistT m) ()
sinkFiles = C.mapM sinkFile
sinkFile :: MonadIO m => File -> SqlPersistT m FileReference
sinkFile File{..} = do
fileReferenceContent <- for fileContent $ \fileContentContent -> do
let fileContentHash = Crypto.hash fileContentContent
unlessM (exists [ FileContentHash ==. fileContentHash ]) $
repsert (FileContentKey fileContentHash) FileContent{..}
return fileContentHash
return FileReference
{ fileReferenceContent
, fileReferenceTitle = fileTitle
, fileReferenceModified = fileModified
}
sinkFiles' :: (MonadIO m, HasFileReference record) => ConduitT (File, FileReferenceResidual record) record (SqlPersistT m) ()
sinkFiles' = C.mapM $ uncurry sinkFile'
sinkFile' :: (MonadIO m, HasFileReference record) => File -> FileReferenceResidual record -> SqlPersistT m record
sinkFile' file residual = do
reference <- sinkFile file
return $ _FileReference # (reference, residual)

View File

@ -206,8 +206,6 @@ makeLenses_ ''CourseApplication
makeLenses_ ''Allocation makeLenses_ ''Allocation
makeLenses_ ''File
makeLenses_ ''Submission makeLenses_ ''Submission
makeLenses_ ''SubmissionUser makeLenses_ ''SubmissionUser

View File

@ -46,15 +46,14 @@ sheetOldUnassigned tid ssh csh = do
_ -> error "SQL Query with limit 1 returned more than one result" _ -> error "SQL Query with limit 1 returned more than one result"
-- | Return a specfic file from a `Sheet` -- | Return a specfic file from a `Sheet`
sheetFileQuery :: MonadResource m => TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> FilePath -> ConduitT () (Entity File) (SqlPersistT m) () sheetFileQuery :: MonadResource m => TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> FilePath -> ConduitT () (Entity SheetFile) (SqlPersistT m) ()
sheetFileQuery tid ssh csh shn sft title = E.selectSource $ E.from $ sheetFileQuery tid ssh csh shn sft title = E.selectSource $ E.from $
\(course `E.InnerJoin` sheet `E.InnerJoin` sFile `E.InnerJoin` file) -> do \(course `E.InnerJoin` sheet `E.InnerJoin` sFile) -> do
-- Restrict to consistent rows that correspond to each other -- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. sFile E.^. SheetFileFile)
E.on (sFile E.^. SheetFileSheet E.==. sheet E.^. SheetId) E.on (sFile E.^. SheetFileSheet E.==. sheet E.^. SheetId)
E.on (sheet E.^. SheetCourse E.==. course E.^. CourseId) E.on (sheet E.^. SheetCourse E.==. course E.^. CourseId)
-- filter to requested file -- filter to requested file
E.where_ ((file E.^. FileTitle E.==. E.val title) E.where_ ((sFile E.^. SheetFileTitle E.==. E.val title)
E.&&. (sFile E.^. SheetFileType E.==. E.val sft ) E.&&. (sFile E.^. SheetFileType E.==. E.val sft )
E.&&. (sheet E.^. SheetName E.==. E.val shn ) E.&&. (sheet E.^. SheetName E.==. E.val shn )
E.&&. (course E.^. CourseShorthand E.==. E.val csh ) E.&&. (course E.^. CourseShorthand E.==. E.val csh )
@ -62,14 +61,13 @@ sheetFileQuery tid ssh csh shn sft title = E.selectSource $ E.from $
E.&&. (course E.^. CourseTerm E.==. E.val tid ) E.&&. (course E.^. CourseTerm E.==. E.val tid )
) )
-- return file entity -- return file entity
return file return sFile
-- | Return all files of a certain `SheetFileType` for a `Sheet` -- | Return all files of a certain `SheetFileType` for a `Sheet`
sheetFilesAllQuery :: MonadResource m => TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> ConduitT () (Entity File) (SqlPersistT m) () sheetFilesAllQuery :: MonadResource m => TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> ConduitT () (Entity SheetFile) (SqlPersistT m) ()
sheetFilesAllQuery tid ssh csh shn sft = E.selectSource $ E.from $ sheetFilesAllQuery tid ssh csh shn sft = E.selectSource $ E.from $
\(course `E.InnerJoin` sheet `E.InnerJoin` sFile `E.InnerJoin` file) -> do \(course `E.InnerJoin` sheet `E.InnerJoin` sFile) -> do
-- Restrict to consistent rows that correspond to each other -- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. sFile E.^. SheetFileFile)
E.on (sFile E.^. SheetFileSheet E.==. sheet E.^. SheetId) E.on (sFile E.^. SheetFileSheet E.==. sheet E.^. SheetId)
E.on (sheet E.^. SheetCourse E.==. course E.^. CourseId) E.on (sheet E.^. SheetCourse E.==. course E.^. CourseId)
-- filter to requested file -- filter to requested file
@ -80,14 +78,13 @@ sheetFilesAllQuery tid ssh csh shn sft = E.selectSource $ E.from $
E.&&. (sFile E.^. SheetFileType E.==. E.val sft ) E.&&. (sFile E.^. SheetFileType E.==. E.val sft )
) )
-- return file entity -- return file entity
return file return sFile
-- | Return all files of certain `SheetFileTypes` for a `Sheet` -- | Return all files of certain `SheetFileTypes` for a `Sheet`
sheetFilesSFTsQuery :: MonadResource m => TermId -> SchoolId -> CourseShorthand -> SheetName -> [SheetFileType] -> ConduitT () (Entity File) (SqlPersistT m) () sheetFilesSFTsQuery :: MonadResource m => TermId -> SchoolId -> CourseShorthand -> SheetName -> [SheetFileType] -> ConduitT () (Entity SheetFile) (SqlPersistT m) ()
sheetFilesSFTsQuery tid ssh csh shn sfts = E.selectSource $ E.from $ sheetFilesSFTsQuery tid ssh csh shn sfts = E.selectSource $ E.from $
\(course `E.InnerJoin` sheet `E.InnerJoin` sFile `E.InnerJoin` file) -> do \(course `E.InnerJoin` sheet `E.InnerJoin` sFile) -> do
-- Restrict to consistent rows that correspond to each other -- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. sFile E.^. SheetFileFile)
E.on (sFile E.^. SheetFileSheet E.==. sheet E.^. SheetId) E.on (sFile E.^. SheetFileSheet E.==. sheet E.^. SheetId)
E.on (sheet E.^. SheetCourse E.==. course E.^. CourseId) E.on (sheet E.^. SheetCourse E.==. course E.^. CourseId)
-- filter to requested file -- filter to requested file
@ -98,7 +95,7 @@ sheetFilesSFTsQuery tid ssh csh shn sfts = E.selectSource $ E.from $
E.&&. (sFile E.^. SheetFileType `E.in_` E.valList sfts ) E.&&. (sFile E.^. SheetFileType `E.in_` E.valList sfts )
) )
-- return file entity -- return file entity
return file return sFile
-- | Check whether a sheet has any files for a given file type -- | Check whether a sheet has any files for a given file type
hasSheetFileQuery :: E.SqlExpr (Entity Sheet) -> SheetFileType -> E.SqlExpr (E.Value Bool) hasSheetFileQuery :: E.SqlExpr (Entity Sheet) -> SheetFileType -> E.SqlExpr (E.Value Bool)

View File

@ -114,5 +114,7 @@ extra-deps:
- normaldistribution-1.1.0.3 - normaldistribution-1.1.0.3
- unordered-containers-0.2.11.0
resolver: lts-15.12 resolver: lts-15.12
allow-newer: true allow-newer: true

View File

@ -318,6 +318,13 @@ packages:
sha256: 856818862d12df8b030fa9cfef2c4ffa604d06f0eb057498db245dfffcd60e3c sha256: 856818862d12df8b030fa9cfef2c4ffa604d06f0eb057498db245dfffcd60e3c
original: original:
hackage: normaldistribution-1.1.0.3 hackage: normaldistribution-1.1.0.3
- completed:
hackage: unordered-containers-0.2.11.0@sha256:ba70b8a9d7eebc2034bf92e5690b2dd71200e76aa9f3f93e0b6be3f27f244d18,4998
pantry-tree:
size: 1416
sha256: d9b83f62373f509a441223f22f12e22e39b38ef3275dfca7c190a4795bebfed5
original:
hackage: unordered-containers-0.2.11.0
snapshots: snapshots:
- completed: - completed:
size: 494635 size: 494635

View File

@ -165,6 +165,21 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
_{MsgCourseApplicationTemplateApplication} _{MsgCourseApplicationTemplateApplication}
$else $else
_{MsgCourseApplicationTemplateRegistration} _{MsgCourseApplicationTemplateRegistration}
$nothing
$if hasApplicationTemplate
<dt .deflist__dt>
$if courseApplicationsRequired course
_{MsgCourseApplicationInstructionsApplication}
$else
_{MsgCourseApplicationInstructionsRegistration}
<dd .deflist__dd>
<a href=@{CourseR tid ssh csh CRegisterTemplateR}>
#{iconRegisterTemplate} #
$if courseApplicationsRequired course
_{MsgCourseApplicationTemplateApplication}
$else
_{MsgCourseApplicationTemplateRegistration}
$maybe mGroup <- submissionGroup $maybe mGroup <- submissionGroup
<dt .deflist__dt> <dt .deflist__dt>
_{MsgSubmissionGroup} _{MsgSubmissionGroup}

View File

@ -9,7 +9,7 @@ $if not (null fileInfos)
$forall FileUploadInfo{..} <- fileInfos $forall FileUploadInfo{..} <- fileInfos
<li> <li>
<div .file-container> <div .file-container>
<input type=checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} :fuiForced:readonly> <input type=checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{review _FileTitle fuiTitle} :fuiForced:readonly>
$if fuiSession $if fuiSession
^{messageTooltip uploadOnlySessionMessage} ^{messageTooltip uploadOnlySessionMessage}
<label for=#{fuiHtmlId}> <label for=#{fuiHtmlId}>

View File

@ -35,11 +35,11 @@ testdataDir :: FilePath
testdataDir = "testdata" testdataDir = "testdata"
insertFile :: FilePath -> DB FileId insertFile :: ( HasFileReference fRef, PersistRecordBackend fRef SqlBackend ) => FileReferenceResidual fRef -> FilePath -> DB (Key fRef)
insertFile fileTitle = do insertFile residual fileTitle = do
fileContent <- liftIO . fmap Just . BS.readFile $ testdataDir </> fileTitle fileContent <- liftIO . fmap Just . BS.readFile $ testdataDir </> fileTitle
fileModified <- liftIO getCurrentTime fileModified <- liftIO getCurrentTime
insert File{..} sinkFile' File{..} residual >>= insert
fillDb :: DB () fillDb :: DB ()
fillDb = do fillDb = do
@ -832,12 +832,9 @@ fillDb = do
void . insert $ SheetCorrector jost shId (Load (Just True) 0) CorrectorNormal void . insert $ SheetCorrector jost shId (Load (Just True) 0) CorrectorNormal
void . insert $ SheetCorrector gkleen shId (Load (Just True) 1) CorrectorNormal void . insert $ SheetCorrector gkleen shId (Load (Just True) 1) CorrectorNormal
void . insert $ SheetCorrector svaupel shId (Load (Just True) 1) CorrectorNormal void . insert $ SheetCorrector svaupel shId (Load (Just True) 1) CorrectorNormal
h102 <- insertFile "H10-2.hs" void $ insertFile (SheetFileResidual shId SheetHint) "H10-2.hs"
h103 <- insertFile "H10-3.hs" void $ insertFile (SheetFileResidual shId SheetSolution) "H10-3.hs"
pdf10 <- insertFile "ProMo_Uebung10.pdf" void $ insertFile (SheetFileResidual shId SheetExercise) "ProMo_Uebung10.pdf"
void . insert $ SheetFile shId h102 SheetHint
void . insert $ SheetFile shId h103 SheetSolution
void . insert $ SheetFile shId pdf10 SheetExercise
forM_ [fhamann, maxMuster, tinaTester] $ \uid -> do forM_ [fhamann, maxMuster, tinaTester] $ \uid -> do
subId <- insert $ Submission subId <- insert $ Submission
@ -850,8 +847,7 @@ fillDb = do
} }
void . insert $ SubmissionEdit (Just uid) now subId void . insert $ SubmissionEdit (Just uid) now subId
void . insert $ SubmissionUser uid subId void . insert $ SubmissionUser uid subId
fId <- insertFile "AbgabeH10-1.hs" void $ insertFile (SubmissionFileResidual subId False False) "AbgabeH10-1.hs"
void . insert $ SubmissionFile subId fId False False
tut1 <- insert Tutorial tut1 <- insert Tutorial
{ tutorialName = "Di08" { tutorialName = "Di08"
, tutorialCourse = pmo , tutorialCourse = pmo
@ -1017,7 +1013,7 @@ fillDb = do
, (jost, CourseParticipantActive) , (jost, CourseParticipantActive)
] ]
void $ insertFile "H10-2.hs" -- unreferenced -- void $ insertFile "H10-2.hs" -- unreferenced
-- -- betriebssysteme -- -- betriebssysteme
bs <- insert' Course bs <- insert' Course