Download of submission files (single & as archive)

This commit is contained in:
Gregor Kleen 2017-10-11 18:02:04 +02:00
parent 68b3e578b4
commit b552e2a0e3
11 changed files with 169 additions and 15 deletions

2
models
View File

@ -104,6 +104,8 @@ SubmissionFile
fileId FileId fileId FileId
isUpdate Bool isUpdate Bool
UniqueSubmissionFile fileId submissionId isUpdate UniqueSubmissionFile fileId submissionId isUpdate
UniqueFile fileId
deriving Show
SubmissionUser SubmissionUser
userId UserId userId UserId
submissionId SubmissionId submissionId SubmissionId

View File

@ -66,6 +66,8 @@ dependencies:
- binary - binary
- mtl - mtl
- sandi - sandi
- esqueleto
- mime-types
# The library contains all of our application code. The executable # The library contains all of our application code. The executable
# defined below is just a thin wrapper. # defined below is just a thin wrapper.

2
routes
View File

@ -20,6 +20,8 @@
/submission SubmissionListR GET /submission SubmissionListR GET
/submission/#CryptoUUIDSubmission SubmissionR GET POST /submission/#CryptoUUIDSubmission SubmissionR GET POST
!/submission/archive/#FilePath SubmissionDownloadArchiveR GET
!/submission/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET
-- For demonstration -- For demonstration
/course/#CryptoUUIDCourse/edit CourseEditExistIDR GET /course/#CryptoUUIDCourse/edit CourseEditExistIDR GET

View File

@ -8,6 +8,7 @@ module CryptoID
( module CryptoID ( module CryptoID
, module Data.UUID.Cryptographic , module Data.UUID.Cryptographic
, module Data.CryptoID.Poly , module Data.CryptoID.Poly
, module CryptoID.Base32
) where ) where
import CryptoID.TH import CryptoID.TH
@ -16,7 +17,8 @@ import ClassyPrelude hiding (fromString)
import Model import Model
import Data.CryptoID import Data.CryptoID
import Data.CryptoID.Poly hiding (decrypt, encrypt) import Data.CryptoID.Poly hiding (encrypt, decrypt)
import CryptoID.Base32 hiding (encrypt, decrypt)
import Data.UUID.Cryptographic import Data.UUID.Cryptographic
import Data.UUID.Types import Data.UUID.Types

45
src/CryptoID/Base32.hs Normal file
View File

@ -0,0 +1,45 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
module CryptoID.Base32
( encrypt
, decrypt
, Base32
) where
import ClassyPrelude
import qualified Codec.Binary.Base32 as Base32
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.CryptoID.Poly as Poly (encrypt, decrypt)
import Data.CryptoID.Poly hiding (encrypt, decrypt)
import GHC.TypeLits
import Data.Binary (Binary)
type Base32 = CI Text
type CryptoFileName namespace = CryptoID namespace Base32
encrypt :: ( KnownSymbol namespace
, MonadThrow m
, Binary a
) => CryptoIDKey -> a -> m (CryptoFileName namespace)
encrypt = Poly.encrypt Nothing base32
where
base32 = return . CI.mk . Text.dropWhileEnd (== '=') . Text.decodeUtf8 . Base32.encode
decrypt :: ( KnownSymbol namespace
, MonadThrow m
, Binary a
) => CryptoIDKey -> CryptoFileName namespace -> m a
decrypt = Poly.decrypt unbase32
where
unbase32 = (\bs -> either (const . throwM $ CiphertextConversionFailed bs) return $ Base32.decode bs) . Text.encodeUtf8 . Text.toUpper . CI.original . (<> "===")

View File

@ -11,6 +11,7 @@ import Language.Haskell.TH
import Data.CryptoID (CryptoID) import Data.CryptoID (CryptoID)
import Data.UUID.Types (UUID) import Data.UUID.Types (UUID)
import Data.Binary (Binary(..)) import Data.Binary (Binary(..))
import CryptoID.Base32 (Base32)
import Database.Persist.Sql (toSqlKey, fromSqlKey) import Database.Persist.Sql (toSqlKey, fromSqlKey)
@ -22,10 +23,12 @@ decTypeAliases = return . concatMap decTypeAliases'
decTypeAliases' n decTypeAliases' n
= [ TySynD cryptoIDn [] $ ConT ''CryptoID `AppT` LitT (StrTyLit n) = [ TySynD cryptoIDn [] $ ConT ''CryptoID `AppT` LitT (StrTyLit n)
, TySynD cryptoUUIDn [] $ ConT cryptoIDn `AppT` ConT ''UUID , TySynD cryptoUUIDn [] $ ConT cryptoIDn `AppT` ConT ''UUID
, TySynD cryptoBase32n [] $ ConT cryptoIDn `AppT` ConT ''Base32
] ]
where where
cryptoIDn = mkName $ "CryptoID" ++ n cryptoIDn = mkName $ "CryptoID" ++ n
cryptoUUIDn = mkName $ "CryptoUUID" ++ n cryptoUUIDn = mkName $ "CryptoUUID" ++ n
cryptoBase32n = mkName $ "CryptoFileName" ++ n
decKeysBinary :: [Name] -> DecsQ decKeysBinary :: [Name] -> DecsQ
decKeysBinary = fmap concat . mapM decKeyBinary decKeysBinary = fmap concat . mapM decKeyBinary

View File

@ -175,6 +175,8 @@ instance Yesod UniWorX where
isAuthorized (CourseListTermR _) _ = return Authorized isAuthorized (CourseListTermR _) _ = return Authorized
isAuthorized SubmissionListR _ = return Authorized isAuthorized SubmissionListR _ = return Authorized
isAuthorized (SubmissionR _) _ = return Authorized isAuthorized (SubmissionR _) _ = return Authorized
isAuthorized (SubmissionDownloadSingleR _ _) _ = return Authorized
isAuthorized (SubmissionDownloadArchiveR _) _ = return Authorized
-- TODO: change to Assistants -- TODO: change to Assistants
isAuthorized TermEditR _ = return Authorized isAuthorized TermEditR _ = return Authorized
isAuthorized (TermEditExistR _) _ = return Authorized isAuthorized (TermEditExistR _) _ = return Authorized

View File

@ -14,7 +14,20 @@ import Import
import Handler.Utils import Handler.Utils
import Network.Mime
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.CaseInsensitive as CI
import qualified Data.UUID.Cryptographic as UUID import qualified Data.UUID.Cryptographic as UUID
import qualified CryptoID.Base32 as Base32
import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit
import System.FilePath
getSubmissionListR :: Handler Html getSubmissionListR :: Handler Html
@ -27,13 +40,62 @@ getSubmissionListR = do
submissionList <- mapM cryptEntity entityList submissionList <- mapM cryptEntity entityList
defaultLayout $(widgetFile "submission-list") defaultLayout $(widgetFile "submission-list")
getSubmissionDownloadSingleR :: CryptoUUIDSubmission -> FilePath -> Handler TypedContent
getSubmissionDownloadSingleR cID path = do
cIDKey <- getsYesod appCryptoIDKey
submissionID <- UUID.decrypt cIDKey cID
results <- runDB . E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOn [E.don $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId)
E.where_ (sf E.^. SubmissionFileSubmissionId E.==. E.val submissionID)
E.where_ (f E.^. FileTitle E.==. E.val path)
E.where_ . E.not_ . E.isNothing $ f E.^. FileContent
return f
let fileName = Text.pack $ takeFileName path
case results of
[Entity _ File{ fileContent = Just c }] -> return $ TypedContent (defaultMimeLookup fileName) (toContent c)
_ -> notFound
getSubmissionDownloadArchiveR :: FilePath -> Handler TypedContent
getSubmissionDownloadArchiveR path = do
let (baseName, ext) = splitExtension path
cID :: CryptoFileNameSubmission
cID = CryptoID . CI.mk $ Text.pack baseName
unless (ext == ".zip") notFound
cIDKey <- getsYesod appCryptoIDKey
submissionID <- Base32.decrypt cIDKey cID
cUUID <- UUID.encrypt cIDKey submissionID
runDB $ do
exists <- count [SubmissionId ==. submissionID]
unless (exists == 1) notFound
sqlBackend <- ask
let fileEntitySource = E.selectSource . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId)
return f
fileEntitySource' :: Source (ResourceT IO) File
fileEntitySource' = runReaderC sqlBackend fileEntitySource =$= Conduit.map entityVal
info = ZipInfo { zipComment = Text.encodeUtf8 . tshow $ ciphertext (cUUID :: CryptoUUIDSubmission) }
return . TypedContent "application/zip" . toContent $ fileEntitySource' =$= produceZip info
getSubmissionR, postSubmissionR :: CryptoUUIDSubmission -> Handler Html getSubmissionR, postSubmissionR :: CryptoUUIDSubmission -> Handler Html
getSubmissionR = postSubmissionR getSubmissionR = postSubmissionR
postSubmissionR cID = do postSubmissionR cID = do
cIDKey <- getsYesod appCryptoIDKey cIDKey <- getsYesod appCryptoIDKey
submissionID <- UUID.decrypt cIDKey cID submissionID <- UUID.decrypt cIDKey cID
submission <- runDB $ get404 (submissionID :: Key Submission) (submission, files) <- runDB $ do
submission <- get404 (submissionID :: Key Submission)
files <- E.select . E.distinct . E.from $ \(sf `E.InnerJoin` f) -> do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId)
return (f, sf)
return $ (submission, files)
defaultLayout $ do let
[whamlet|<pre>#{tshow submission}|] rating = (,) <$> submissionRatingPoints submission <*> submissionRatingComment submission
cID' <- Base32.encrypt cIDKey submissionID
let
archiveBaseName = Text.unpack . CI.foldedCase $ ciphertext (cID' :: CryptoFileNameSubmission)
archiveName = archiveBaseName <.> "zip"
defaultLayout $(widgetFile "submission")

View File

@ -23,7 +23,6 @@ module Handler.Utils.Zip.Rating
import Import hiding ((</>)) import Import hiding ((</>))
import qualified Codec.Binary.Base32 as Base32
import Text.PrettyPrint.Leijen.Text hiding ((<$>)) import Text.PrettyPrint.Leijen.Text hiding ((<$>))
@ -36,6 +35,7 @@ import Data.Text.Encoding.Error (UnicodeException(..))
import qualified Data.Text.Lazy.Encoding as Lazy.Text import qualified Data.Text.Lazy.Encoding as Lazy.Text
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.ByteString.Lazy as Lazy (ByteString) import qualified Data.ByteString.Lazy as Lazy (ByteString)
@ -45,7 +45,7 @@ import Text.Read (readEither)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import qualified Data.CryptoID.Poly as Poly import CryptoID.Base32 as Base32
import System.FilePath import System.FilePath
@ -53,11 +53,14 @@ import System.FilePath
instance HasResolution prec => Pretty (Fixed prec) where instance HasResolution prec => Pretty (Fixed prec) where
pretty = pretty . show pretty = pretty . show
instance Pretty x => Pretty (CI x) where
pretty = pretty . CI.original
data Rating = Rating data Rating = Rating
{ ratingCourseName :: Text { ratingCourseName :: Text
, ratingSheetName :: Text , ratingSheetName :: Text
, ratingSubmissionId :: CryptoIDSubmission String -- ^ 'SubmissionId' , ratingSubmissionId :: CryptoFileNameSubmission
, ratingComment :: Maybe Text , ratingComment :: Maybe Text
, ratingPoints :: Maybe Points , ratingPoints :: Maybe Points
} deriving (Read, Show, Eq, Generic, Typeable) } deriving (Read, Show, Eq, Generic, Typeable)
@ -81,10 +84,8 @@ getRating submissionId = runMaybeT $ do
Sheet{ sheetCourseId, sheetName = ratingSheetName } <- MaybeT $ get submissionSheetId Sheet{ sheetCourseId, sheetName = ratingSheetName } <- MaybeT $ get submissionSheetId
Course{ courseName = ratingCourseName } <- MaybeT $ get sheetCourseId Course{ courseName = ratingCourseName } <- MaybeT $ get sheetCourseId
cIDKey <- getsYesod appCryptoIDKey cIDKey <- getsYesod appCryptoIDKey
ratingSubmissionId <- Poly.encrypt Nothing base32 cIDKey submissionId ratingSubmissionId <- Base32.encrypt cIDKey submissionId
return Rating{..} return Rating{..}
where
base32 = return . CI.foldCase . Text.unpack . Text.dropWhileEnd (== '=') . Text.decodeUtf8 . Base32.encode
formatRating :: Rating -> Lazy.ByteString formatRating :: Rating -> Lazy.ByteString
formatRating Rating{..} = let formatRating Rating{..} = let
@ -153,20 +154,19 @@ isRatingFile :: ( MonadHandler m
isRatingFile fName isRatingFile fName
| Just cID <- isRatingFile' fName = do | Just cID <- isRatingFile' fName = do
cIDKey <- getsYesod appCryptoIDKey cIDKey <- getsYesod appCryptoIDKey
(Just <$> Poly.decrypt unbase32 cIDKey cID) `catch` decryptErrors (Just <$> Base32.decrypt cIDKey cID) `catch` decryptErrors
| otherwise = return Nothing | otherwise = return Nothing
where where
unbase32 = (\bs -> either (const . throwM $ CiphertextConversionFailed bs) return $ Base32.decode bs) . Text.encodeUtf8 . Text.pack . toUpper . (<> "===")
decryptErrors (CiphertextConversionFailed _) = return Nothing decryptErrors (CiphertextConversionFailed _) = return Nothing
decryptErrors InvalidNamespaceDetected = return Nothing decryptErrors InvalidNamespaceDetected = return Nothing
decryptErrors DeserializationError = return Nothing decryptErrors DeserializationError = return Nothing
decryptErrors err = throwM err decryptErrors err = throwM err
isRatingFile' :: FilePath -> Maybe (CryptoIDSubmission String) isRatingFile' :: FilePath -> Maybe CryptoFileNameSubmission
isRatingFile' (takeFileName -> fName) isRatingFile' (takeFileName -> fName)
| (bName, ".txt") <- splitExtension fName | (bName, ".txt") <- splitExtension fName
, Just ciphertext <- stripPrefix "bewertung_" bName , Just (CI.mk . Text.pack -> ciphertext) <- stripPrefix "bewertung_" bName
= Just CryptoID{..} = Just CryptoID{..}
| otherwise | otherwise
= Nothing = Nothing

View File

@ -1,4 +1,4 @@
<ul> <ul>
$forall (cID, val) <- submissionList $forall (cID, _) <- submissionList
<li> <li>
<a href=@{SubmissionR cID}>#{tshow $ ciphertext cID} <a href=@{SubmissionR cID}>#{tshow $ ciphertext cID}

View File

@ -0,0 +1,34 @@
<pre style="display:none">
#{tshow submission}
<table .table .table-striped>
$maybe (points, text) <- rating
<tr>
<td>Punkte
<td>#{tshow points}
<tr>
<td>Kommentar
<td>#{text}
$nothing
<tr>
<td colspan="2">Noch nicht bewertet
<a href=@{SubmissionDownloadArchiveR archiveName} download>Submission archive
<ul>
$forall (Entity _ file, Entity _ sFile) <- files
<li>
<pre style="display:none">
#{tshow file}
<pre style="display:none">
#{tshow sFile}
<b>#{fileTitle file}
$if submissionFileIsUpdate sFile
(Korrektur)
<br>
$maybe content <- fileContent file
<pre>
#{decodeUtf8 content}
$nothing
This is a directory