module Utils.Files ( sinkFile, sinkFiles , sinkFile', sinkFiles' , FileUploads , replaceFileReferences, replaceFileReferences' ) where import Import.NoFoundation import Foundation.Type import Handler.Utils.Minio import qualified Network.Minio as Minio import qualified Crypto.Hash as Crypto (hash) import qualified Data.Conduit.Combinators as C import qualified Data.ByteString.Base64.URL as Base64 import qualified Data.ByteArray as ByteArray import qualified Data.Map.Lazy as Map import qualified Data.Set as Set import Control.Monad.State.Class (modify) import Database.Persist.Sql (deleteWhereCount) import Control.Monad.Trans.Resource (allocate) sinkFiles :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX) => ConduitT File FileReference (SqlPersistT m) () sinkFiles = C.mapM sinkFile sinkFile :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX) => File -> SqlPersistT m FileReference sinkFile File{ fileContent = Nothing, .. } = return FileReference { fileReferenceContent = Nothing , fileReferenceTitle = fileTitle , fileReferenceModified = fileModified } sinkFile File{ fileContent = Just fileContentContent, .. } = do void . withUnliftIO $ \UnliftIO{..} -> let takeLock = do fileLockTime <- liftIO getCurrentTime fileLockInstance <- getsYesod appInstanceID insert FileLock{ fileLockContent = fileContentHash, .. } releaseLock lId = liftHandler . runDB $ (withReaderT projectBackend $ setSerializable (delete lId :: SqlPersistT (HandlerFor UniWorX) ()) :: YesodDB UniWorX ()) in unliftIO $ allocate (unliftIO takeLock) (unliftIO . releaseLock) inDB <- exists [ FileContentHash ==. fileContentHash ] let sinkFileDB = unless inDB $ repsert (FileContentKey fileContentHash) FileContent{ fileContentUnreferencedSince = Nothing, .. } maybeT sinkFileDB $ do let uploadName = decodeUtf8 . Base64.encodeUnpadded $ ByteArray.convert fileContentHash uploadBucket <- getsYesod $ views appSettings appUploadCacheBucket unless inDB . runAppMinio $ do uploadExists <- handleIf minioIsDoesNotExist (const $ return False) $ True <$ Minio.statObject uploadBucket uploadName Minio.defaultGetObjectOptions unless uploadExists $ do let pooOptions = Minio.defaultPutObjectOptions { Minio.pooCacheControl = Just "immutable" } Minio.putObject uploadBucket uploadName (C.sourceLazy $ fromStrict fileContentContent) (Just . fromIntegral $ olength fileContentContent) pooOptions -- Note that MinIO does not accept length zero uploads without an explicit length specification (not `Nothing` in the line above for the api we use) return FileReference { fileReferenceContent = Just fileContentHash , fileReferenceTitle = fileTitle , fileReferenceModified = fileModified } where fileContentHash = Crypto.hash fileContentContent sinkFiles' :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX, HasFileReference record) => ConduitT (File, FileReferenceResidual record) record (SqlPersistT m) () sinkFiles' = C.mapM $ uncurry sinkFile' sinkFile' :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX, HasFileReference record) => File -> FileReferenceResidual record -> SqlPersistT m record sinkFile' file residual = do reference <- sinkFile file return $ _FileReference # (reference, residual) type FileUploads = ConduitT () FileReference (HandlerFor UniWorX) () replaceFileReferences' :: ( MonadIO m, MonadThrow m , IsFileReference record , PersistEntityBackend record ~ SqlBackend ) => (FileReferenceResidual record -> [Filter record]) -> FileReferenceResidual record -> ConduitT FileReference Void (SqlPersistT m) (Set (Key record), Set (Key record)) -- ^ @(oldFiles, changes)@ replaceFileReferences' mkFilter residual = do let resFilter = mkFilter residual oldFiles <- lift $ Map.fromListWith Set.union . map (\(Entity k v) -> (v ^. _FileReference . _1, Set.singleton k)) <$> selectList resFilter [] let oldFiles' = setOf (folded . folded) oldFiles let finsert fRef | Just sfIds <- fRef `Map.lookup` oldFiles = modify $ Map.mapMaybe (assertM' (not . Set.null) . (\\ sfIds)) | otherwise = do let fRef' = _FileReference # (fRef, residual) forM_ (persistUniqueKeys fRef') $ \u -> maybeT (return ()) $ do Entity cKey cVal <- MaybeT . lift $ getBy u deleted <- lift . lift . deleteWhereCount $ resFilter <> [ persistIdField ==. cKey ] unless (deleted == 1) $ throwM . userError $ "replaceFileReferences tried to delete outside of filter/database inconsistency: deleted=" <> show deleted lift . modify $ Map.alter (Just . maybe (Set.singleton cKey) (Set.insert cKey)) (cVal ^. _FileReference . _1) fId <- lift $ insert fRef' modify $ Map.alter (Just . maybe (Set.singleton fId) (Set.insert fId)) fRef changes <- fmap (setOf $ folded . folded) . execStateC oldFiles $ C.mapM_ finsert lift . deleteWhere $ resFilter <> [ persistIdField <-. Set.toList (oldFiles' `Set.intersection` changes) ] return (oldFiles', changes) replaceFileReferences :: ( MonadHandler m, MonadThrow m , HandlerSite m ~ UniWorX , IsFileReference record , PersistEntityBackend record ~ SqlBackend ) => (FileReferenceResidual record -> [Filter record]) -> FileReferenceResidual record -> FileUploads -> SqlPersistT m (Set (Key record), Set (Key record)) -- ^ @(oldFiles, changes)@ replaceFileReferences mkFilter residual fs = runConduit $ transPipe liftHandler fs .| replaceFileReferences' mkFilter residual