feat(file-field): cumulative size limit
This commit is contained in:
parent
5bd9ea85e8
commit
b749039636
@ -290,3 +290,5 @@ bot-mitigations:
|
||||
- only-logged-in-table-sorting
|
||||
|
||||
volatile-cluster-settings-cache-time: 10
|
||||
|
||||
communication-attachments-max-size: 20971520 # 20MiB
|
||||
|
||||
@ -186,6 +186,7 @@ UploadModeExtensionRestrictionTip: Komma-separiert. Wenn keine Dateiendungen ang
|
||||
UploadModeExtensionRestrictionMultipleTip: Einschränkung von Dateiendungen erfolgt für alle hochgeladenen Dateien, auch innerhalb von ZIP-Archiven.
|
||||
FileUploadMaxSize maxSize@Text: Datei darf maximal #{maxSize} groß sein
|
||||
FileUploadMaxSizeMultiple maxSize@Text: Dateien dürfen jeweils maximal #{maxSize} groß sein
|
||||
FileUploadCumulativeMaxSize maxSize@Text: Dateien dürfen insgesamt maximal #{maxSize} groß sein
|
||||
|
||||
InvalidPseudonym pseudonym@Text: Invalides Pseudonym "#{pseudonym}"
|
||||
InvalidPseudonymSubmissionIgnored oPseudonyms@Text iPseudonym@Text: Abgabe mit Pseudonymen „#{oPseudonyms}“ wurde ignoriert, da „#{iPseudonym}“ nicht automatisiert zu einem validen Pseudonym korrigiert werden konnte.
|
||||
|
||||
@ -186,6 +186,8 @@ UploadModeExtensionRestrictionTip: Comma-separated. If no file extensions are sp
|
||||
UploadModeExtensionRestrictionMultipleTip: Checks for valid file extension are performed for all uploaded files, including those packed within zip-archives.
|
||||
FileUploadMaxSize maxSize: File may be up to #{maxSize} in size
|
||||
FileUploadMaxSizeMultiple maxSize: Files may each be up to #{maxSize} in size
|
||||
FileUploadCumulativeMaxSize maxSize: Files may be no larger than #{maxSize} in total
|
||||
|
||||
InvalidPseudonym pseudonym: Invalid pseudonym “#{pseudonym}”
|
||||
InvalidPseudonymSubmissionIgnored oPseudonyms iPseudonym: The submission with pseudonyms “#{oPseudonyms}” has been ignored since “#{iPseudonym}” could not be automatically corrected to be a valid pseudonym.
|
||||
PseudonymAutocorrections: Suggestions:
|
||||
|
||||
@ -55,6 +55,7 @@ UploadSpecificFileMaxSizeNegative: Maximale Dateigröße darf nicht negativ sein
|
||||
UploadSpecificFileEmptyOk: Leere Uploads erlauben
|
||||
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
|
||||
GenericFileFieldFileTooLarge file@FilePath: „#{file}“ ist zu groß
|
||||
GenericFileFieldCumulativeTooLarge: Hochgeladene Dateien sind zu groß
|
||||
GenericFileFieldInvalidExtension file@FilePath: „#{file}” hat keine zulässige Dateiendung
|
||||
OnlyUploadOneFile: Bitte nur eine Datei hochladen.
|
||||
UploadAtLeastOneNonemptyFile: Bitte mindestens eine nichtleere Datei hochladen.
|
||||
|
||||
@ -55,6 +55,7 @@ UploadSpecificFileMaxSizeNegative: Maximum filesize may not be negative
|
||||
UploadSpecificFileEmptyOk: Allow empty uploads
|
||||
UnknownPseudonymWord pseudonymWord: Invalid pseudonym-word “#{pseudonymWord}”
|
||||
GenericFileFieldFileTooLarge file: “#{file}” is too large
|
||||
GenericFileFieldCumulativeTooLarge: Uploaded files are too large
|
||||
GenericFileFieldInvalidExtension file: “#{file}” does not have an acceptable file extension
|
||||
OnlyUploadOneFile: Please only upload one file
|
||||
UploadAtLeastOneNonemptyFile: Please upload at least one nonempty file.
|
||||
|
||||
@ -27,10 +27,10 @@ addStaticContent ext _mime content = do
|
||||
for ((,) <$> appWidgetMemcached <*> appWidgetMemcachedConf appSettings') $ \(mConn, WidgetMemcachedConf{ widgetMemcachedConf = MemcachedConf { memcachedExpiry }, widgetMemcachedBaseUrl }) -> do
|
||||
let expiry = maybe 0 ceiling memcachedExpiry
|
||||
touch = liftIO $ Memcached.touch expiry (encodeUtf8 $ pack fileName) mConn
|
||||
add = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn
|
||||
addItem = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn
|
||||
absoluteLink = unpack widgetMemcachedBaseUrl </> fileName
|
||||
catchIf Memcached.isKeyNotFound touch . const $
|
||||
handleIf Memcached.isKeyExists (const $ return ()) add
|
||||
handleIf Memcached.isKeyExists (const $ return ()) addItem
|
||||
return . Left $ pack absoluteLink
|
||||
where
|
||||
-- Generate a unique filename based on the content itself, this is used
|
||||
|
||||
@ -206,13 +206,23 @@ commR CommunicationRoute{..} = do
|
||||
|
||||
recipientsListMsg <- messageI Info MsgCommRecipientsList
|
||||
|
||||
attachmentsMaxSize <- getsYesod $ view _appCommunicationAttachmentsMaxSize
|
||||
let attachmentField = genericFileField $ return FileField
|
||||
{ fieldIdent = Nothing
|
||||
, fieldUnpackZips = FileFieldUserOption True False
|
||||
, fieldMultiple = True
|
||||
, fieldRestrictExtensions = Nothing
|
||||
, fieldAdditionalFiles = _FileReferenceFileReferenceTitleMap # Map.empty
|
||||
, fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = attachmentsMaxSize
|
||||
, fieldAllEmptyOk = True
|
||||
}
|
||||
((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . withButtonForm' universeF . renderAForm FormStandard $ Communication
|
||||
<$> recipientAForm
|
||||
<* aformMessage recipientsListMsg
|
||||
<*> ( CommunicationContent
|
||||
<$> aopt textField (fslI MsgCommSubject & addAttr "uw-enter-as-tab" "") Nothing
|
||||
<*> (markupOutput <$> areq htmlField (fslI MsgCommBody) Nothing)
|
||||
<*> fmap fold (aopt (convertFieldM (runConduit . (.| C.foldMap Set.singleton)) yieldMany fileFieldMultiple) (fslI MsgCommAttachments & setTooltip MsgCommAttachmentsTip) Nothing)
|
||||
<*> fmap fold (aopt (convertFieldM (runConduit . (.| C.foldMap Set.singleton)) yieldMany attachmentField) (fslI MsgCommAttachments & setTooltip MsgCommAttachmentsTip) Nothing)
|
||||
)
|
||||
formResult commRes $ \case
|
||||
(comm, BtnCommunicationSend) -> do
|
||||
|
||||
@ -998,15 +998,20 @@ genericFileField mkOpts = Field{..}
|
||||
= not (permittedExtension opts fName)
|
||||
&& (not doUnpack || ((/=) `on` simpleContentType) (mimeLookup fName) typeZip)
|
||||
|
||||
whenIsJust fieldMaxFileSize $ \maxSize -> forM_ files $ \fInfo -> do
|
||||
fLength <- runConduit $ fileSource fInfo .| C.takeE (fromIntegral $ succ maxSize) .| C.lengthE
|
||||
when (fLength > maxSize) $ do
|
||||
when (is _Just mIdent) $
|
||||
liftHandler . runDB . runConduit $
|
||||
mapM_ (transPipe lift . handleFile) files
|
||||
.| handleUpload opts mIdent
|
||||
.| C.sinkNull
|
||||
throwE . SomeMessage . MsgGenericFileFieldFileTooLarge . unpack $ fileName fInfo
|
||||
whenIsJust (ignoreNothing min fieldMaxFileSize fieldMaxCumulativeSize) $ \takeSize ->
|
||||
flip evalAccumT mempty . forM_ files $ \fInfo -> do
|
||||
fLength <- lift . runConduit $ fileSource fInfo .| C.takeE (fromIntegral $ succ takeSize) .| C.lengthE
|
||||
add $ Sum fLength
|
||||
Sum cummSize <- look
|
||||
when (NTop (Just cummSize) > NTop fieldMaxCumulativeSize || NTop (Just fLength) > NTop fieldMaxFileSize) $ do
|
||||
when (is _Just mIdent) $
|
||||
lift . liftHandler . runDB . runConduit $
|
||||
mapM_ (transPipe lift . handleFile) files
|
||||
.| handleUpload opts mIdent
|
||||
.| C.sinkNull
|
||||
when (NTop (Just fLength) > NTop fieldMaxFileSize) $ do
|
||||
lift . throwE . SomeMessage . MsgGenericFileFieldFileTooLarge . unpack $ fileName fInfo
|
||||
lift . throwE $ SomeMessage MsgGenericFileFieldCumulativeTooLarge
|
||||
|
||||
if | invExt : _ <- filter invalidUploadExtension uploadedFilenames
|
||||
-> do
|
||||
@ -1125,7 +1130,7 @@ fileFieldMultiple = genericFileField $ return FileField
|
||||
, fieldMultiple = True
|
||||
, fieldRestrictExtensions = Nothing
|
||||
, fieldAdditionalFiles = _FileReferenceFileReferenceTitleMap # Map.empty
|
||||
, fieldMaxFileSize = Nothing
|
||||
, fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing
|
||||
, fieldAllEmptyOk = True
|
||||
}
|
||||
|
||||
@ -1145,7 +1150,7 @@ singleFileField prev = genericFileField $ do
|
||||
[ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True))
|
||||
| FileReference{..} <- Set.toList permitted
|
||||
]
|
||||
, fieldMaxFileSize = Nothing
|
||||
, fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing
|
||||
, fieldAllEmptyOk = True
|
||||
}
|
||||
|
||||
@ -1161,7 +1166,7 @@ specificFileField UploadSpecificFile{..} mPrev = convertField (.| fixupFileTitle
|
||||
[ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True))
|
||||
| FileReference{..} <- Set.toList previous
|
||||
]
|
||||
, fieldMaxFileSize = specificFileMaxSize
|
||||
, fieldMaxFileSize = specificFileMaxSize, fieldMaxCumulativeSize = Nothing
|
||||
, fieldAllEmptyOk = specificFileEmptyOk
|
||||
}
|
||||
where
|
||||
@ -1189,7 +1194,7 @@ zipFileField' doUnpack permittedExtensions emptyOk mPrev = genericFileField $ do
|
||||
[ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True))
|
||||
| FileReference{..} <- Set.toList previous
|
||||
]
|
||||
, fieldMaxFileSize = Nothing
|
||||
, fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing
|
||||
, fieldAllEmptyOk = emptyOk
|
||||
}
|
||||
|
||||
@ -1232,7 +1237,7 @@ multiFileField mkPermitted = genericFileField $ mkField <$> mkPermitted
|
||||
[ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True))
|
||||
| FileReference{..} <- Set.toList permitted
|
||||
]
|
||||
, fieldMaxFileSize = Nothing
|
||||
, fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing
|
||||
, fieldAllEmptyOk = True
|
||||
}
|
||||
|
||||
|
||||
@ -70,6 +70,7 @@ instance ToJSON (FileField FileIdent) where
|
||||
, pure $ "multiple" JSON..= fieldMultiple
|
||||
, pure $ "restrict-extensions" JSON..= fieldRestrictExtensions
|
||||
, pure $ "max-file-size" JSON..= fieldMaxFileSize
|
||||
, pure $ "max-cumulative-size" JSON..= fieldMaxCumulativeSize
|
||||
, pure $ "additional-files" JSON..= addFiles'
|
||||
]
|
||||
where addFiles' = unFileIdentFileReferenceTitleMap fieldAdditionalFiles <&> \FileIdentFileReferenceTitleMapElem{..} -> JSON.object
|
||||
@ -83,6 +84,7 @@ instance FromJSON (FileField FileIdent) where
|
||||
fieldMultiple <- o JSON..: "multiple"
|
||||
fieldRestrictExtensions <- o JSON..:? "restrict-extensions"
|
||||
fieldMaxFileSize <- o JSON..:? "max-file-size"
|
||||
fieldMaxCumulativeSize <- o JSON..:? "max-cumulative-size"
|
||||
fieldAllEmptyOk <- o JSON..:? "all-empty-ok" JSON..!= True
|
||||
addFiles' <- o JSON..:? "additional-files" JSON..!= mempty
|
||||
fieldAdditionalFiles <- fmap FileIdentFileReferenceTitleMap . for addFiles' $ JSON.withObject "FileIdentFileReferenceTitleMapElem" $ \o' -> do
|
||||
|
||||
@ -118,6 +118,11 @@ import Control.Monad.Trans.State as Import
|
||||
( State, runState, mapState, withState
|
||||
, StateT(..), mapStateT, withStateT
|
||||
)
|
||||
import Control.Monad.Trans.Accum as Import
|
||||
( Accum, runAccum, mapAccum
|
||||
, AccumT, runAccumT, execAccumT, evalAccumT, mapAccumT
|
||||
, look, looks, add
|
||||
)
|
||||
import Control.Monad.State.Class as Import (MonadState(state))
|
||||
import Control.Monad.Trans.Writer.Lazy as Import
|
||||
( Writer, runWriter, mapWriter, execWriter
|
||||
|
||||
@ -309,7 +309,7 @@ data FileField fileid = FileField
|
||||
, fieldUnpackZips :: FileFieldUserOption Bool
|
||||
, fieldMultiple :: Bool
|
||||
, fieldRestrictExtensions :: Maybe (NonNull (Set Extension))
|
||||
, fieldMaxFileSize :: Maybe Natural
|
||||
, fieldMaxFileSize, fieldMaxCumulativeSize :: Maybe Natural
|
||||
, fieldAdditionalFiles :: FileReferenceTitleMap fileid (FileFieldUserOption Bool)
|
||||
, fieldAllEmptyOk :: Bool
|
||||
}
|
||||
@ -327,6 +327,7 @@ instance ToJSON (FileField FileReference) where
|
||||
, pure $ "multiple" JSON..= fieldMultiple
|
||||
, pure $ "restrict-extensions" JSON..= fieldRestrictExtensions
|
||||
, pure $ "max-file-size" JSON..= fieldMaxFileSize
|
||||
, pure $ "max-cumulative-size" JSON..= fieldMaxCumulativeSize
|
||||
, pure $ "additional-files" JSON..= addFiles'
|
||||
, pure $ "all-empty-ok" JSON..= fieldAllEmptyOk
|
||||
]
|
||||
@ -342,6 +343,7 @@ instance FromJSON (FileField FileReference) where
|
||||
fieldMultiple <- o JSON..: "multiple"
|
||||
fieldRestrictExtensions <- o JSON..:? "restrict-extensions"
|
||||
fieldMaxFileSize <- o JSON..:? "max-file-size"
|
||||
fieldMaxCumulativeSize <- o JSON..:? "max-cumulative-size"
|
||||
fieldAllEmptyOk <- o JSON..:? "all-empty-ok" JSON..!= True
|
||||
addFiles' <- o JSON..:? "additional-files" JSON..!= mempty
|
||||
fieldAdditionalFiles <- fmap FileReferenceFileReferenceTitleMap . for addFiles' $ JSON.withObject "FileReferenceFileReferenceTitleMapElem" $ \o' -> do
|
||||
|
||||
@ -226,6 +226,8 @@ data AppSettings = AppSettings
|
||||
, appVolatileClusterSettingsCacheTime :: DiffTime
|
||||
|
||||
, appJobMaxFlush :: Maybe Natural
|
||||
|
||||
, appCommunicationAttachmentsMaxSize :: Maybe Natural
|
||||
} deriving Show
|
||||
|
||||
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
|
||||
@ -693,6 +695,8 @@ instance FromJSON AppSettings where
|
||||
|
||||
appJobMaxFlush <- o .:? "job-max-flush"
|
||||
|
||||
appCommunicationAttachmentsMaxSize <- o .:? "communication-attachments-max-size"
|
||||
|
||||
return AppSettings{..}
|
||||
where isValidARCConf ARCConf{..} = arccMaximumWeight > 0
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ $if not (null fileInfos)
|
||||
<div .file-uploads-label>_{MsgUtilAddMoreFiles}
|
||||
|
||||
$# new files
|
||||
<input type="file" uw-file-input name=#{fieldName} id=#{fieldId} :fieldMultiple:multiple :acceptRestricted:accept=#{accept} :req && null fileInfos:required :is _Just fieldMaxFileSize:data-max-size=#{maybe "-1" tshow fieldMaxFileSize}>
|
||||
<input type="file" uw-file-input name=#{fieldName} id=#{fieldId} :fieldMultiple:multiple :acceptRestricted:accept=#{accept} :req && null fileInfos:required :is _Just fieldMaxFileSize || is _Just fieldMaxCumulativeSize:data-max-size=#{maybe "-1" tshow (ignoreNothing min fieldMaxFileSize fieldMaxCumulativeSize)}>
|
||||
|
||||
$if fieldMultiple
|
||||
<div .file-input__info>
|
||||
@ -57,6 +57,10 @@ $maybe maxSize <- fieldMaxFileSize
|
||||
$else
|
||||
_{MsgFileUploadMaxSize (textBytes maxSize)}
|
||||
|
||||
$maybe maxSize <- fieldMaxCumulativeSize
|
||||
<div .file-input__info>
|
||||
_{MsgFileUploadCumulativeMaxSize (textBytes maxSize)}
|
||||
|
||||
$if not (fieldOptionForce fieldUnpackZips)
|
||||
<div .file-input__unpack>
|
||||
^{iconTooltip (i18n MsgAutoUnzipInfo) Nothing False}
|
||||
|
||||
Reference in New Issue
Block a user