module Snap.Internal.Util.FileUploads
  ( 
    handleFormUploads
  , foldMultipart
  , PartFold
  , FormParam
  , FormFile (..)
  , storeAsLazyByteString
  , withTemporaryStore
    
  , handleFileUploads
  , handleMultipart
  , PartProcessor
    
  , PartInfo(..)
  , PartDisposition(..)
  , toPartDisposition
    
    
  , UploadPolicy(..)
  , defaultUploadPolicy
  , doProcessFormInputs
  , setProcessFormInputs
  , getMaximumFormInputSize
  , setMaximumFormInputSize
  , getMaximumNumberOfFormInputs
  , setMaximumNumberOfFormInputs
  , getMinimumUploadRate
  , setMinimumUploadRate
  , getMinimumUploadSeconds
  , setMinimumUploadSeconds
  , getUploadTimeout
  , setUploadTimeout
    
  , FileUploadPolicy(..)
  , defaultFileUploadPolicy
  , setMaximumFileSize
  , setMaximumNumberOfFiles
  , setSkipFilesWithoutNames
  , setMaximumSkippedFileSize
    
  , PartUploadPolicy(..)
  , disallow
  , allowWithMaximumSize
    
  , FileUploadException(..)
  , fileUploadExceptionReason
  , BadPartException(..)
  , PolicyViolationException(..)
  ) where
import           Control.Applicative              (Alternative ((<|>)), Applicative (pure, (*>), (<*)))
import           Control.Arrow                    (Arrow (first))
import           Control.Exception.Lifted         (Exception, SomeException (..), bracket, catch, finally, fromException, mask, throwIO, toException)
import qualified Control.Exception.Lifted         as E (try)
import           Control.Monad                    (Functor (fmap), Monad (return, (>>=)), MonadPlus (mzero), forM_, guard, liftM, sequence, unless, void, when, (>=>))
import           Control.Monad.IO.Class           (liftIO)
import           Data.Attoparsec.ByteString.Char8 (Parser, isEndOfLine, string, takeWhile)
import qualified Data.Attoparsec.ByteString.Char8 as Atto (try)
import           Data.ByteString.Char8            (ByteString)
import qualified Data.ByteString.Char8            as S
import           Data.ByteString.Internal         (c2w)
import qualified Data.ByteString.Lazy.Internal    as LB (ByteString (Empty), chunk)
import qualified Data.CaseInsensitive             as CI (mk)
import           Data.Int                         (Int, Int64)
import qualified Data.IORef                       as IORef
import           Data.List                        (find, map, (++))
import qualified Data.Map                         as Map (insertWith)
import           Data.Maybe                       (Maybe (..), fromMaybe, isJust, maybe)
import           Data.Text                        (Text)
import qualified Data.Text                        as T (concat, pack, unpack)
import qualified Data.Text.Encoding               as TE (decodeUtf8)
import           Data.Typeable                    (Typeable, cast)
import           Prelude                          (Bool (..), Double, Either (..), Eq (..), FilePath, IO, Ord (..), Show (..), String, const, either, foldr, fst, id, max, not, otherwise, seq, snd, succ, ($), ($!), (.), (^), (||))
import           Snap.Core                        (HasHeaders (headers), Headers, MonadSnap, Request (rqParams, rqPostParams), getHeader, getRequest, getTimeoutModifier, putRequest, runRequestBody)
import           Snap.Internal.Parsing            (crlf, fullyParse, pContentTypeWithParameters, pHeaders, pValueWithParameters')
import qualified Snap.Types.Headers               as H (fromList)
import           System.Directory                 (removeFile)
import           System.FilePath                  ((</>))
import           System.IO                        (BufferMode (NoBuffering), Handle, hClose, hSetBuffering, openBinaryTempFile)
import           System.IO.Error                  (isDoesNotExistError)
import           System.IO.Streams                (InputStream, MatchInfo (..), TooManyBytesReadException, search)
import qualified System.IO.Streams                as Streams
import           System.IO.Streams.Attoparsec     (parseFromStream)
import           System.PosixCompat.Temp          (mkstemp)
handleFileUploads ::
       (MonadSnap m) =>
       FilePath                       
    -> UploadPolicy                   
    -> (PartInfo -> PartUploadPolicy) 
    -> (PartInfo -> Either PolicyViolationException FilePath -> IO a)
                                      
                                      
    -> m [a]
handleFileUploads tmpdir uploadPolicy partPolicy partHandler =
    handleMultipart uploadPolicy go
  where
    go partInfo stream = maybe disallowed takeIt mbFs
      where
        ctText = partContentType partInfo
        fnText = fromMaybe "" $ partFileName partInfo
        ct = TE.decodeUtf8 ctText
        fn = TE.decodeUtf8 fnText
        (PartUploadPolicy mbFs) = partPolicy partInfo
        takeIt maxSize = do
            str' <- Streams.throwIfProducesMoreThan maxSize stream
            fileReader tmpdir partHandler partInfo str' `catch` tooMany maxSize
        tooMany maxSize (_ :: TooManyBytesReadException) =
            partHandler partInfo
                        (Left $
                         PolicyViolationException $
                         T.concat [ "File \""
                                  , fn
                                  , "\" exceeded maximum allowable size "
                                  , T.pack $ show maxSize ])
        disallowed =
            partHandler partInfo
                        (Left $
                         PolicyViolationException $
                         T.concat [ "Policy disallowed upload of file \""
                                  , fn
                                  , "\" with content-type \""
                                  , ct
                                  , "\"" ] )
data FormFile a = FormFile
    { formFileName  :: !ByteString
         
    , formFileValue :: a
         
    } deriving (Eq, Ord, Show)
data UploadState a = UploadState
     { numUploadedFiles :: !Int
     , uploadedFiles :: !([FormFile a] -> [FormFile a])
     }
handleFormUploads ::
       (MonadSnap m) =>
       UploadPolicy                   
    -> FileUploadPolicy               
    -> (PartInfo -> InputStream ByteString -> IO a)
                                      
    -> m ([FormParam], [FormFile a])
handleFormUploads uploadPolicy filePolicy partHandler = do
    (params, !st) <- foldMultipart uploadPolicy go (UploadState 0 id)
    return (params, uploadedFiles st [])
  where
    go !partInfo stream !st = do
        when (numUploads >= maxFiles) throwTooManyFiles
        case partFileName partInfo of
          Nothing -> onEmptyName
          Just _ -> takeIt
      where
        numUploads = numUploadedFiles st
        files = uploadedFiles st
        maxFiles = maxNumberOfFiles filePolicy
        maxFileSize = maxFileUploadSize filePolicy
        fnText = fromMaybe "" $ partFileName partInfo
        fn = TE.decodeUtf8 fnText
        takeIt = do
            str' <- Streams.throwIfProducesMoreThan maxFileSize stream
            r <- partHandler partInfo str' `catch` tooMany maxFileSize
            let f = FormFile (partFieldName partInfo) r
            return $! UploadState (succ numUploads) (files . ([f] ++) )
        skipIt maxSize = do
            str' <- Streams.throwIfProducesMoreThan maxSize stream
            !_ <- Streams.skipToEof str' `catch` tooMany maxSize
            return $! UploadState (succ numUploads) files
        onEmptyName = if skipEmptyFileName filePolicy
                      then skipIt (maxEmptyFileNameSize filePolicy)
                      else takeIt
        throwTooManyFiles = throwIO . PolicyViolationException $ T.concat
                            ["number of files exceeded the maximum of "
                            ,T.pack (show maxFiles) ]
        tooMany maxSize (_ :: TooManyBytesReadException) =
            throwIO . PolicyViolationException $
                    T.concat [ "File \""
                             , fn
                             , "\" exceeded maximum allowable size "
                             , T.pack $ show maxSize ]
type PartFold a = PartInfo -> InputStream ByteString -> a -> IO a
foldMultipart ::
       (MonadSnap m) =>
       UploadPolicy        
    -> PartFold a          
    -> a                   
    -> m ([FormParam], a)
foldMultipart uploadPolicy origPartHandler zero = do
    hdrs <- liftM headers getRequest
    let (ct, mbBoundary) = getContentType hdrs
    tickleTimeout <- liftM (. max) getTimeoutModifier
    let bumpTimeout = tickleTimeout $ uploadTimeout uploadPolicy
    let partHandler = if doProcessFormInputs uploadPolicy
                        then captureVariableOrReadFile
                                 (getMaximumFormInputSize uploadPolicy)
                                 origPartHandler
                        else \x y acc -> liftM File $ origPartHandler x y acc
    
    guard (ct == "multipart/form-data")
    boundary <- maybe (throwIO $ BadPartException
                       "got multipart/form-data without boundary")
                      return
                      mbBoundary
    
    
    runRequestBody (proc bumpTimeout boundary partHandler)
  where
    
    uploadRate  = minimumUploadRate uploadPolicy
    uploadSecs  = minimumUploadSeconds uploadPolicy
    maxFormVars = maximumNumberOfFormInputs uploadPolicy
    
    proc bumpTimeout boundary partHandler =
        Streams.throwIfTooSlow bumpTimeout uploadRate uploadSecs >=>
        internalFoldMultipart maxFormVars boundary partHandler zero
type PartProcessor a = PartInfo -> InputStream ByteString -> IO a
handleMultipart ::
       (MonadSnap m) =>
       UploadPolicy        
    -> PartProcessor a     
    -> m [a]
handleMultipart uploadPolicy origPartHandler = do
    (captures, files) <- foldMultipart uploadPolicy partFold id
    procCaptures captures
    return $! files []
  where
    partFold info input acc = do
      x <- origPartHandler info input
      return $ acc . ([x]++)
    
    procCaptures []          = pure ()
    procCaptures params = do
        rq <- getRequest
        putRequest $ modifyParams (\m -> foldr ins m params) rq
    
    ins (!k, !v) = Map.insertWith (\_ ex -> (v:ex)) k [v]
         
    
    modifyParams f r = r { rqPostParams = f $ rqPostParams r
                         , rqParams     = f $ rqParams r
                         }
data PartDisposition =
    DispositionAttachment       
  | DispositionFile             
  | DispositionFormData         
  | DispositionOther ByteString 
  deriving (Eq, Show)
data PartInfo =
  PartInfo
  { partFieldName   :: !ByteString
    
    
  , partFileName    :: !(Maybe ByteString)
    
  , partContentType :: !ByteString
    
  , partDisposition :: !PartDisposition
    
  , partHeaders     :: !Headers
    
  }
  deriving (Show)
toPartDisposition :: ByteString -> PartDisposition
toPartDisposition s | s == "attachment" = DispositionAttachment
                    | s == "file"       = DispositionFile
                    | s == "form-data"  = DispositionFormData
                    | otherwise         = DispositionOther s
data FileUploadException = forall e . (ExceptionWithReason e, Show e) =>
                           WrappedFileUploadException e
  deriving (Typeable)
class Exception e => ExceptionWithReason e where
    exceptionReason :: e -> Text
instance Show FileUploadException where
    show (WrappedFileUploadException e) = show e
instance Exception FileUploadException
fileUploadExceptionReason :: FileUploadException -> Text
fileUploadExceptionReason (WrappedFileUploadException e) = exceptionReason e
uploadExceptionToException :: ExceptionWithReason e => e -> SomeException
uploadExceptionToException = toException . WrappedFileUploadException
uploadExceptionFromException :: ExceptionWithReason e => SomeException -> Maybe e
uploadExceptionFromException x = do
    WrappedFileUploadException e <- fromException x
    cast e
data BadPartException = BadPartException {
  
  badPartExceptionReason :: Text
  }
  deriving (Typeable)
instance Exception BadPartException where
    toException = uploadExceptionToException
    fromException = uploadExceptionFromException
instance ExceptionWithReason BadPartException where
    exceptionReason (BadPartException e) = T.concat ["Bad part: ", e]
instance Show BadPartException where
  show = T.unpack . exceptionReason
data PolicyViolationException = PolicyViolationException {
      
      
      policyViolationExceptionReason :: Text
    } deriving (Typeable)
instance Exception PolicyViolationException where
    toException e@(PolicyViolationException _) =
        uploadExceptionToException e
    fromException = uploadExceptionFromException
instance ExceptionWithReason PolicyViolationException where
    exceptionReason (PolicyViolationException r) =
        T.concat ["File upload policy violation: ", r]
instance Show PolicyViolationException where
  show (PolicyViolationException s) = "File upload policy violation: "
                                            ++ T.unpack s
data UploadPolicy = UploadPolicy {
      processFormInputs         :: Bool
    , maximumFormInputSize      :: Int64
    , maximumNumberOfFormInputs :: Int
    , minimumUploadRate         :: Double
    , minimumUploadSeconds      :: Int
    , uploadTimeout             :: Int
}
defaultUploadPolicy :: UploadPolicy
defaultUploadPolicy = UploadPolicy True maxSize maxNum minRate minSeconds tout
  where
    maxSize    = 2^(17::Int)
    maxNum     = 10
    minRate    = 1000
    minSeconds = 10
    tout       = 20
doProcessFormInputs :: UploadPolicy -> Bool
doProcessFormInputs = processFormInputs
setProcessFormInputs :: Bool -> UploadPolicy -> UploadPolicy
setProcessFormInputs b u = u { processFormInputs = b }
getMaximumFormInputSize :: UploadPolicy -> Int64
getMaximumFormInputSize = maximumFormInputSize
setMaximumFormInputSize :: Int64 -> UploadPolicy -> UploadPolicy
setMaximumFormInputSize s u = u { maximumFormInputSize = s }
getMaximumNumberOfFormInputs :: UploadPolicy -> Int
getMaximumNumberOfFormInputs = maximumNumberOfFormInputs
setMaximumNumberOfFormInputs :: Int -> UploadPolicy -> UploadPolicy
setMaximumNumberOfFormInputs s u = u { maximumNumberOfFormInputs = s }
getMinimumUploadRate :: UploadPolicy -> Double
getMinimumUploadRate = minimumUploadRate
setMinimumUploadRate :: Double -> UploadPolicy -> UploadPolicy
setMinimumUploadRate s u = u { minimumUploadRate = s }
getMinimumUploadSeconds :: UploadPolicy -> Int
getMinimumUploadSeconds = minimumUploadSeconds
setMinimumUploadSeconds :: Int -> UploadPolicy -> UploadPolicy
setMinimumUploadSeconds s u = u { minimumUploadSeconds = s }
getUploadTimeout :: UploadPolicy -> Int
getUploadTimeout = uploadTimeout
setUploadTimeout :: Int -> UploadPolicy -> UploadPolicy
setUploadTimeout s u = u { uploadTimeout = s }
data FileUploadPolicy = FileUploadPolicy
    { maxFileUploadSize    :: !Int64
    , maxNumberOfFiles     :: !Int
    , skipEmptyFileName    :: !Bool
    , maxEmptyFileNameSize :: !Int64
    }
defaultFileUploadPolicy :: FileUploadPolicy
defaultFileUploadPolicy = FileUploadPolicy maxFileSize maxFiles
                                           skipEmptyName maxEmptySize
  where
    maxFileSize = 1048576 
    maxFiles    = 10
    skipEmptyName = True
    maxEmptySize = 0
setMaximumFileSize :: Int64 -> FileUploadPolicy -> FileUploadPolicy
setMaximumFileSize maxSize s =
    s { maxFileUploadSize = maxSize }
setMaximumNumberOfFiles :: Int -> FileUploadPolicy -> FileUploadPolicy
setMaximumNumberOfFiles maxFiles s =
    s { maxNumberOfFiles = maxFiles }
setSkipFilesWithoutNames :: Bool -> FileUploadPolicy -> FileUploadPolicy
setSkipFilesWithoutNames shouldSkip s =
    s { skipEmptyFileName = shouldSkip }
setMaximumSkippedFileSize :: Int64 -> FileUploadPolicy -> FileUploadPolicy
setMaximumSkippedFileSize maxSize s =
    s { maxEmptyFileNameSize = maxSize }
data PartUploadPolicy = PartUploadPolicy (Maybe Int64)
disallow :: PartUploadPolicy
disallow = PartUploadPolicy Nothing
allowWithMaximumSize :: Int64 -> PartUploadPolicy
allowWithMaximumSize = PartUploadPolicy . Just
storeAsLazyByteString :: InputStream ByteString -> IO LB.ByteString
storeAsLazyByteString !str = do
   f <- Streams.fold (\f c -> f . LB.chunk c) id str
   return $! f LB.Empty
withTemporaryStore ::
    MonadSnap m
    => FilePath 
    -> String   
    -> ((InputStream ByteString -> IO FilePath) -> m a)
      
    -> m a
withTemporaryStore tempdir pat act = do
    ioref <- liftIO $ IORef.newIORef []
    let
      modifyIORef' ref f = do 
          x <- IORef.readIORef ref
          let x' = f x
          x' `seq` IORef.writeIORef ref x'
      go input = do
          (fn, h) <- openBinaryTempFile tempdir pat
          modifyIORef' ioref (fn:)
          hSetBuffering h NoBuffering
          output <- Streams.handleToOutputStream h
          Streams.connect input output
          hClose h
          pure fn
      cleanup = liftIO $ do
          files <- IORef.readIORef ioref
          forM_ files $ \fn ->
             removeFile fn `catch` handleExists
      handleExists e = unless (isDoesNotExistError e) $ throwIO e
    act go `finally` cleanup
captureVariableOrReadFile ::
       Int64                                   
    -> PartFold a                              
    -> PartInfo -> InputStream ByteString
    -> a
    -> IO (Capture a)
captureVariableOrReadFile maxSize fileHandler partInfo stream acc =
    if isFile
      then liftM File $ fileHandler partInfo stream acc
      else variable `catch` handler
  where
    isFile = isJust (partFileName partInfo) ||
             partDisposition partInfo == DispositionFile
    variable = do
        !x <- liftM S.concat $
             Streams.throwIfProducesMoreThan maxSize stream >>= Streams.toList
        return $! Capture fieldName x
    fieldName = partFieldName partInfo
    handler (_ :: TooManyBytesReadException) =
        throwIO $ PolicyViolationException $
                T.concat [ "form input '"
                         , TE.decodeUtf8 fieldName
                         , "' exceeded maximum permissible size ("
                         , T.pack $ show maxSize
                         , " bytes)" ]
data Capture a = Capture !ByteString !ByteString
               | File a
fileReader :: FilePath
           -> (PartInfo -> Either PolicyViolationException FilePath -> IO a)
           -> PartProcessor a
fileReader tmpdir partProc partInfo input =
    withTempFile tmpdir "snap-upload-" $ \(fn, h) -> do
        hSetBuffering h NoBuffering
        output <- Streams.handleToOutputStream h
        Streams.connect input output
        hClose h
        partProc partInfo $ Right fn
data MultipartState a = MultipartState
  { numFormVars       ::  !Int
  , numFormFiles      ::  !Int
  , capturedFields    :: !([FormParam] -> [FormParam])
  , accumulator       :: !a
  }
type FormParam = (ByteString, ByteString)
addCapture :: ByteString -> ByteString -> MultipartState a -> MultipartState a
addCapture !k !v !ms =
  let !kv = (k,v)
      f = capturedFields ms . ([kv]++)
      !ms' = ms { capturedFields = f
                , numFormVars = succ (numFormVars ms) }
  in ms'
internalFoldMultipart ::
       Int           
    -> ByteString                                     
    -> (PartInfo -> InputStream ByteString -> a -> IO (Capture a))  
    -> a
    -> InputStream ByteString
    -> IO ([FormParam], a)
internalFoldMultipart !maxFormVars !boundary clientHandler !zeroAcc !stream = go
  where
    
    initialState = MultipartState 0 0 id zeroAcc
    
    go = do
        
        _        <- parseFromStream (parseFirstBoundary boundary) stream
        bmstream <- search (fullBoundary boundary) stream
        ms <- foldParts goPart bmstream initialState
        return $ (capturedFields ms [], accumulator ms)
    
    pBoundary !b = Atto.try $ do
      _ <- string "--"
      string b
    
    fullBoundary !b       = S.concat ["\r\n", "--", b]
    pLine                 = takeWhile (not . isEndOfLine . c2w) <* eol
    parseFirstBoundary !b = pBoundary b <|> (pLine *> parseFirstBoundary b)
    
    takeHeaders !str = hdrs `catch` handler
      where
        hdrs = do
            str' <- Streams.throwIfProducesMoreThan mAX_HDRS_SIZE str
            liftM toHeaders $ parseFromStream pHeadersWithSeparator str'
        handler (_ :: TooManyBytesReadException) =
            throwIO $ BadPartException "headers exceeded maximum size"
    
    goPart !str !state = do
        hdrs <- takeHeaders str
        
        let (contentType, mboundary) = getContentType hdrs
        let (fieldName, fileName, disposition) = getFieldHeaderInfo hdrs
        if contentType == "multipart/mixed"
          then maybe (throwIO $ BadPartException $
                      "got multipart/mixed without boundary")
                     (processMixed fieldName str state)
                     mboundary
          else do
              let info = PartInfo fieldName fileName contentType disposition hdrs
              handlePart info str state
    
    handlePart !info !str !ms = do
      r <- clientHandler info str (accumulator ms)
      case r of
        Capture !k !v -> do
           when (maxFormVars <= numFormVars ms) throwTooMuchVars
           return $! addCapture k v ms
        File !newAcc -> return $! ms { accumulator = newAcc
                                     , numFormFiles = succ (numFormFiles ms)
                                     }
    throwTooMuchVars =
        throwIO . PolicyViolationException
        $ T.concat [ "number of form inputs exceeded maximum of "
                   , T.pack $ show maxFormVars ]
    
    processMixed !fieldName !str !state !mixedBoundary = do
        
        _  <- parseFromStream (parseFirstBoundary mixedBoundary) str
        bm <- search (fullBoundary mixedBoundary) str
        foldParts (mixedStream fieldName) bm state
    
    mixedStream !fieldName !str !acc = do
        hdrs <- takeHeaders str
        let (contentType, _)           = getContentType hdrs
        let (_, fileName, disposition) = getFieldHeaderInfo hdrs
        let info = PartInfo fieldName fileName contentType disposition hdrs
        handlePart info str acc
getContentType :: Headers
               -> (ByteString, Maybe ByteString)
getContentType hdrs = (contentType, boundary)
  where
    contentTypeValue = fromMaybe "text/plain" $
                       getHeader "content-type" hdrs
    eCT = fullyParse contentTypeValue pContentTypeWithParameters
    (!contentType, !params) = either (const ("text/plain", [])) id eCT
    boundary = findParam "boundary" params
getFieldHeaderInfo :: Headers -> (ByteString, Maybe ByteString, PartDisposition)
getFieldHeaderInfo hdrs = (fieldName, fileName, disposition)
  where
    contentDispositionValue = fromMaybe "unknown" $
                              getHeader "content-disposition" hdrs
    eDisposition = fullyParse contentDispositionValue $ pValueWithParameters' (const True)
    (!dispositionType, dispositionParameters) =
        either (const ("unknown", [])) id eDisposition
    disposition = toPartDisposition dispositionType
    fieldName = fromMaybe "" $ findParam "name" dispositionParameters
    fileName = findParam "filename" dispositionParameters
findParam :: (Eq a) => a -> [(a, b)] -> Maybe b
findParam p = fmap snd . find ((== p) . fst)
partStream :: InputStream MatchInfo -> IO (InputStream ByteString)
partStream st = Streams.makeInputStream go
  where
    go = do
        s <- Streams.read st
        return $! s >>= f
    f (NoMatch s) = return s
    f _           = mzero
foldParts :: (InputStream ByteString -> MultipartState a -> IO (MultipartState a))
             -> InputStream MatchInfo
             -> (MultipartState a)
             -> IO (MultipartState a)
foldParts partFunc stream = go
  where
    part acc pStream = do
        isLast <- parseFromStream pBoundaryEnd pStream
        if isLast
          then return Nothing
          else do
              !x <- partFunc pStream acc
              Streams.skipToEof pStream
              return $! Just x
    go !acc = do
      cap <- partStream stream >>= part acc
      maybe (return acc) go cap
    pBoundaryEnd = (eol *> pure False) <|> (string "--" *> pure True)
eol :: Parser ByteString
eol = (string "\n") <|> (string "\r\n")
pHeadersWithSeparator :: Parser [(ByteString,ByteString)]
pHeadersWithSeparator = pHeaders <* crlf
toHeaders :: [(ByteString,ByteString)] -> Headers
toHeaders kvps = H.fromList kvps'
  where
    kvps'     = map (first CI.mk) kvps
mAX_HDRS_SIZE :: Int64
mAX_HDRS_SIZE = 32768
withTempFile :: FilePath
             -> String
             -> ((FilePath, Handle) -> IO a)
             -> IO a
withTempFile tmpl temp handler =
    mask $ \restore -> bracket make cleanup (restore . handler)
  where
    make           = mkstemp $ tmpl </> (temp ++ "XXXXXXX")
    cleanup (fp,h) = sequence $ map gobble [hClose h, removeFile fp]
    t :: IO z -> IO (Either SomeException z)
    t = E.try
    gobble = void . t