finishes initial refactor

This commit is contained in:
Keagan McClelland
2022-06-09 13:36:45 -06:00
parent 8b0e856392
commit c21686a46f
27 changed files with 931 additions and 1147 deletions

View File

@@ -14,9 +14,6 @@
/package/#ApiVersion/instructions/#PkgId InstructionsR GET -- get instructions - can specify version with ?spec=<emver> /package/#ApiVersion/instructions/#PkgId InstructionsR GET -- get instructions - can specify version with ?spec=<emver>
/package/#ApiVersion/version/#PkgId PkgVersionR GET -- get most recent appId version /package/#ApiVersion/version/#PkgId PkgVersionR GET -- get most recent appId version
-- SUPPORT API V0
/support/v0/error-logs ErrorLogsR POST
-- ADMIN API V0 -- ADMIN API V0
/admin/v0/upload PkgUploadR POST !admin /admin/v0/upload PkgUploadR POST !admin
/admin/v0/index PkgIndexR POST !admin /admin/v0/index PkgIndexR POST !admin

View File

@@ -1,204 +1,227 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Application
( appMain module Application (
, develMain appMain,
, makeFoundation develMain,
, makeLogWare makeFoundation,
, shutdownApp makeLogWare,
, shutdownAll shutdownApp,
, shutdownWeb shutdownAll,
, startApp shutdownWeb,
, startWeb startApp,
startWeb,
-- * for DevelMain -- * for DevelMain
, getApplicationRepl getApplicationRepl,
, getAppSettings getAppSettings,
-- * for GHCI -- * for GHCI
, handler handler,
, db db,
) where ) where
import Startlude ( ($) import Startlude (
, (++) Applicative (pure),
, (.) Async (asyncThreadId),
, (<$>) Bool (False, True),
, (<||>) Either (Left, Right),
, Applicative(pure) Eq ((==)),
, Async(asyncThreadId) ExitCode (ExitSuccess),
, Bool(False, True) IO,
, Either(Left, Right) Int,
, Eq((==)) Maybe (Just),
, ExitCode(ExitSuccess) Monad (return, (>>=)),
, IO MonadIO (..),
, Int Print (putStr, putStrLn),
, Maybe(Just) ReaderT (runReaderT),
, Monad((>>=), return) Text,
, MonadIO(..) ThreadId,
, Print(putStr, putStrLn) async,
, ReaderT(runReaderT) flip,
, Text for_,
, ThreadId forever,
, async forkIO,
, flip fromIntegral,
, for_ killThread,
, forever newEmptyMVar,
, forkIO newMVar,
, fromIntegral onException,
, killThread panic,
, newEmptyMVar print,
, newMVar putMVar,
, onException show,
, panic stdout,
, print swapMVar,
, putMVar takeMVar,
, show void,
, stdout waitEitherCatchCancel,
, swapMVar when,
, takeMVar ($),
, void (++),
, waitEitherCatchCancel (.),
, when (<$>),
) (<||>),
)
import Control.Monad.Logger ( LoggingT import Control.Monad.Logger (
, liftLoc LoggingT,
, runLoggingT liftLoc,
) runLoggingT,
import Data.Default ( Default(def) ) )
import Database.Persist.Postgresql ( createPostgresqlPool import Data.Default (Default (def))
, pgConnStr import Database.Persist.Postgresql (
, pgPoolSize createPostgresqlPool,
, runMigration pgConnStr,
, runSqlPool pgPoolSize,
) runMigration,
import Language.Haskell.TH.Syntax ( qLocation ) runSqlPool,
import Network.Wai ( Application )
, Middleware import Language.Haskell.TH.Syntax (qLocation)
, Request(requestHeaders) import Network.Wai (
, ResponseReceived Application,
) Middleware,
import Network.Wai.Handler.Warp ( Settings Request (requestHeaders),
, defaultSettings ResponseReceived,
, defaultShouldDisplayException )
, getPort import Network.Wai.Handler.Warp (
, runSettings Settings,
, setHTTP2Disabled defaultSettings,
, setHost defaultShouldDisplayException,
, setOnException getPort,
, setPort runSettings,
, setTimeout setHTTP2Disabled,
) setHost,
import Network.Wai.Handler.WarpTLS ( runTLS setOnException,
, tlsSettings setPort,
) setTimeout,
import Network.Wai.Middleware.AcceptOverride )
( acceptOverride ) import Network.Wai.Handler.WarpTLS (
import Network.Wai.Middleware.Autohead runTLS,
( autohead ) tlsSettings,
import Network.Wai.Middleware.Cors ( CorsResourcePolicy(..) )
, cors import Network.Wai.Middleware.AcceptOverride (
, simpleCorsResourcePolicy acceptOverride,
) )
import Network.Wai.Middleware.MethodOverride import Network.Wai.Middleware.Autohead (
( methodOverride ) autohead,
import Network.Wai.Middleware.RequestLogger )
( Destination(Logger) import Network.Wai.Middleware.Cors (
, OutputFormat(..) CorsResourcePolicy (..),
, destination cors,
, mkRequestLogger simpleCorsResourcePolicy,
, outputFormat )
) import Network.Wai.Middleware.MethodOverride (
import System.IO ( BufferMode(..) methodOverride,
, hSetBuffering )
) import Network.Wai.Middleware.RequestLogger (
import System.Log.FastLogger ( defaultBufSize Destination (Logger),
, newStdoutLoggerSet OutputFormat (..),
, toLogStr destination,
) mkRequestLogger,
import Yesod.Core ( HandlerFor outputFormat,
, LogLevel(LevelError) )
, Yesod(messageLoggerSource) import System.IO (
, logInfo BufferMode (..),
, mkYesodDispatch hSetBuffering,
, toWaiAppPlain )
, typeOctet import System.Log.FastLogger (
) defaultBufSize,
import Yesod.Core.Types ( Logger(loggerSet) ) newStdoutLoggerSet,
import Yesod.Default.Config2 ( configSettingsYml toLogStr,
, develMainHelper )
, getDevSettings import Yesod.Core (
, loadYamlSettings HandlerFor,
, loadYamlSettingsArgs LogLevel (LevelError),
, makeYesodLogger Yesod (messageLoggerSource),
, useEnv logInfo,
) mkYesodDispatch,
toWaiAppPlain,
typeOctet,
)
import Yesod.Core.Types (Logger (loggerSet))
import Yesod.Default.Config2 (
configSettingsYml,
develMainHelper,
getDevSettings,
loadYamlSettings,
loadYamlSettingsArgs,
makeYesodLogger,
useEnv,
)
import Control.Lens (both)
import Data.List (lookup)
import Data.String.Interpolate.IsString (
i,
)
import Database.Persist.Migration qualified
import Database.Persist.Migration.Postgres qualified
import Database.Persist.Sql (SqlBackend)
import Foundation (
Handler,
RegistryCtx (..),
Route (..),
resourcesRegistryCtx,
setWebProcessThreadId,
unsafeHandler,
)
import Handler.Admin (
deleteCategoryR,
deletePkgCategorizeR,
getPkgDeindexR,
postCategoryR,
postPkgCategorizeR,
postPkgDeindexR,
postPkgIndexR,
postPkgUploadR,
)
import Handler.Eos (getEosR, getEosVersionR)
import Handler.Package
import Lib.PkgRepository (watchEosRepoRoot)
import Lib.Ssl (
doesSslNeedRenew,
renewSslCerts,
setupSsl,
)
import Migration (manualMigration)
import Model (migrateAll)
import Network.HTTP.Types.Header (hOrigin)
import Network.Wai.Middleware.Gzip (
GzipFiles (GzipCompress),
GzipSettings (gzipCheckMime, gzipFiles),
defaultCheckMime,
gzip,
)
import Network.Wai.Middleware.RequestLogger.JSON (
formatAsJSONWithHeaders,
)
import Settings (
AppPort,
AppSettings (..),
configSettingsYmlValue,
)
import System.Directory (createDirectoryIfMissing)
import System.Posix.Process (exitImmediately)
import System.Time.Extra (sleep)
import Yesod (YesodPersist (runDB))
import Control.Lens ( both )
import Data.List ( lookup )
import Data.String.Interpolate.IsString
( i )
import qualified Database.Persist.Migration
import qualified Database.Persist.Migration.Postgres
import Database.Persist.Sql ( SqlBackend )
import Foundation ( Handler
, RegistryCtx(..)
, Route(..)
, resourcesRegistryCtx
, setWebProcessThreadId
, unsafeHandler
)
import Handler.Admin ( deleteCategoryR
, deletePkgCategorizeR
, getPkgDeindexR
, postCategoryR
, postPkgCategorizeR
, postPkgDeindexR
, postPkgIndexR
, postPkgUploadR
)
import Handler.ErrorLogs ( postErrorLogsR )
import Handler.Marketplace ( getEosR
, getEosVersionR
)
import Handler.Package
import Lib.PkgRepository ( watchEosRepoRoot )
import Lib.Ssl ( doesSslNeedRenew
, renewSslCerts
, setupSsl
)
import Migration ( manualMigration )
import Model ( migrateAll )
import Network.HTTP.Types.Header ( hOrigin )
import Network.Wai.Middleware.Gzip ( GzipFiles(GzipCompress)
, GzipSettings(gzipCheckMime, gzipFiles)
, defaultCheckMime
, gzip
)
import Network.Wai.Middleware.RequestLogger.JSON
( formatAsJSONWithHeaders )
import Settings ( AppPort
, AppSettings(..)
, configSettingsYmlValue
)
import System.Directory ( createDirectoryIfMissing )
import System.Posix.Process ( exitImmediately )
import System.Time.Extra ( sleep )
import Yesod ( YesodPersist(runDB) )
-- This line actually creates our YesodDispatch instance. It is the second half -- This line actually creates our YesodDispatch instance. It is the second half
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the -- of the call to mkYesodData which occurs in Foundation.hs. Please see the
-- comments there for more details. -- comments there for more details.
mkYesodDispatch "RegistryCtx" resourcesRegistryCtx mkYesodDispatch "RegistryCtx" resourcesRegistryCtx
-- | This function allocates resources (such as a database connection pool), -- | This function allocates resources (such as a database connection pool),
-- performs initialization and returns a foundation datatype value. This is also -- performs initialization and returns a foundation datatype value. This is also
-- the place to put your migrate statements to have automatic database -- the place to put your migrate statements to have automatic database
@@ -207,20 +230,20 @@ makeFoundation :: AppSettings -> IO RegistryCtx
makeFoundation appSettings = do makeFoundation appSettings = do
-- Some basic initializations: HTTP connection manager, logger, and static -- Some basic initializations: HTTP connection manager, logger, and static
-- subsite. -- subsite.
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
appWebServerThreadId <- newEmptyMVar appWebServerThreadId <- newEmptyMVar
appShouldRestartWeb <- newMVar False appShouldRestartWeb <- newMVar False
-- We need a log function to create a connection pool. We need a connection -- We need a log function to create a connection pool. We need a connection
-- pool to create our foundation. And we need our foundation to get a -- pool to create our foundation. And we need our foundation to get a
-- logging function. To get out of this loop, we initially create a -- logging function. To get out of this loop, we initially create a
-- temporary foundation without a real connection pool, get a log function -- temporary foundation without a real connection pool, get a log function
-- from there, and then create the real foundation. -- from there, and then create the real foundation.
let mkFoundation appConnPool appStopFsNotifyEos = RegistryCtx { .. } let mkFoundation appConnPool appStopFsNotifyEos = RegistryCtx{..}
-- The RegistryCtx {..} syntax is an example of record wild cards. For more -- The RegistryCtx {..} syntax is an example of record wild cards. For more
-- information, see: -- information, see:
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
tempFoundation = tempFoundation =
mkFoundation (panic "connPool forced in tempFoundation") (panic "stopFsNotify forced in tempFoundation") mkFoundation (panic "connPool forced in tempFoundation") (panic "stopFsNotify forced in tempFoundation")
logFunc = messageLoggerSource tempFoundation appLogger logFunc = messageLoggerSource tempFoundation appLogger
@@ -228,8 +251,9 @@ makeFoundation appSettings = do
createDirectoryIfMissing True (errorLogRoot appSettings) createDirectoryIfMissing True (errorLogRoot appSettings)
-- Create the database connection pool -- Create the database connection pool
pool <- flip runLoggingT logFunc pool <-
$ createPostgresqlPool (pgConnStr $ appDatabaseConf appSettings) (pgPoolSize . appDatabaseConf $ appSettings) flip runLoggingT logFunc $
createPostgresqlPool (pgConnStr $ appDatabaseConf appSettings) (pgPoolSize . appDatabaseConf $ appSettings)
stopEosWatch <- runLoggingT (runReaderT (watchEosRepoRoot pool) appSettings) logFunc stopEosWatch <- runLoggingT (runReaderT (watchEosRepoRoot pool) appSettings) logFunc
@@ -242,6 +266,7 @@ makeFoundation appSettings = do
-- Return the foundation -- Return the foundation
return $ mkFoundation pool stopEosWatch return $ mkFoundation pool stopEosWatch
-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and -- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and
-- applying some additional middlewares. -- applying some additional middlewares.
makeApplication :: RegistryCtx -> IO Application makeApplication :: RegistryCtx -> IO Application
@@ -254,7 +279,7 @@ makeApplication foundation = do
-- TODO: change this to the cached version when we have better release processes -- TODO: change this to the cached version when we have better release processes
-- since caches aren't invalidated, publishing a new package/eos won't take effect -- since caches aren't invalidated, publishing a new package/eos won't take effect
-- because the cached file will be downloaded. -- because the cached file will be downloaded.
def { gzipFiles = GzipCompress, gzipCheckMime = defaultCheckMime <||> (== typeOctet) } def{gzipFiles = GzipCompress, gzipCheckMime = defaultCheckMime <||> (== typeOctet)}
pure pure
. logWare . logWare
. cors dynamicCorsResourcePolicy . cors dynamicCorsResourcePolicy
@@ -265,78 +290,86 @@ makeApplication foundation = do
. gzip gzipSettings . gzip gzipSettings
$ appPlain $ appPlain
dynamicCorsResourcePolicy :: Request -> Maybe CorsResourcePolicy dynamicCorsResourcePolicy :: Request -> Maybe CorsResourcePolicy
dynamicCorsResourcePolicy req = Just . policy . lookup hOrigin $ requestHeaders req dynamicCorsResourcePolicy req = Just . policy . lookup hOrigin $ requestHeaders req
where where
policy o = simpleCorsResourcePolicy policy o =
{ corsOrigins = (\o' -> ([o'], True)) <$> o simpleCorsResourcePolicy
, corsMethods = ["GET", "POST", "HEAD", "PUT", "DELETE", "TRACE", "CONNECT", "OPTIONS", "PATCH"] { corsOrigins = (\o' -> ([o'], True)) <$> o
, corsRequestHeaders = [ "app-version" , corsMethods = ["GET", "POST", "HEAD", "PUT", "DELETE", "TRACE", "CONNECT", "OPTIONS", "PATCH"]
, "Accept" , corsRequestHeaders =
, "Accept-Charset" [ "app-version"
, "Accept-Encoding" , "Accept"
, "Accept-Language" , "Accept-Charset"
, "Accept-Ranges" , "Accept-Encoding"
, "Age" , "Accept-Language"
, "Allow" , "Accept-Ranges"
, "Authorization" , "Age"
, "Cache-Control" , "Allow"
, "Connection" , "Authorization"
, "Content-Encoding" , "Cache-Control"
, "Content-Language" , "Connection"
, "Content-Length" , "Content-Encoding"
, "Content-Location" , "Content-Language"
, "Content-MD5" , "Content-Length"
, "Content-Range" , "Content-Location"
, "Content-Type" , "Content-MD5"
, "Date" , "Content-Range"
, "ETag" , "Content-Type"
, "Expect" , "Date"
, "Expires" , "ETag"
, "From" , "Expect"
, "Host" , "Expires"
, "If-Match" , "From"
, "If-Modified-Since" , "Host"
, "If-None-Match" , "If-Match"
, "If-Range" , "If-Modified-Since"
, "If-Unmodified-Since" , "If-None-Match"
, "Last-Modified" , "If-Range"
, "Location" , "If-Unmodified-Since"
, "Max-Forwards" , "Last-Modified"
, "Pragma" , "Location"
, "Proxy-Authenticate" , "Max-Forwards"
, "Proxy-Authorization" , "Pragma"
, "Range" , "Proxy-Authenticate"
, "Referer" , "Proxy-Authorization"
, "Retry-After" , "Range"
, "Server" , "Referer"
, "TE" , "Retry-After"
, "Trailer" , "Server"
, "Transfer-Encoding" , "TE"
, "Upgrade" , "Trailer"
, "User-Agent" , "Transfer-Encoding"
, "Vary" , "Upgrade"
, "Via" , "User-Agent"
, "WWW-Authenticate" , "Vary"
, "Warning" , "Via"
, "Content-Disposition" , "WWW-Authenticate"
, "MIME-Version" , "Warning"
, "Cookie" , "Content-Disposition"
, "Set-Cookie" , "MIME-Version"
, "Origin" , "Cookie"
, "Prefer" , "Set-Cookie"
, "Preference-Applied" , "Origin"
] , "Prefer"
, corsIgnoreFailures = True , "Preference-Applied"
} ]
, corsIgnoreFailures = True
}
makeLogWare :: RegistryCtx -> IO Middleware makeLogWare :: RegistryCtx -> IO Middleware
makeLogWare foundation = mkRequestLogger def makeLogWare foundation =
{ outputFormat = if appDetailedRequestLogging $ appSettings foundation mkRequestLogger
then Detailed True def
else CustomOutputFormatWithDetailsAndHeaders formatAsJSONWithHeaders { outputFormat =
, destination = Logger $ loggerSet $ appLogger foundation if appDetailedRequestLogging $ appSettings foundation
} then Detailed True
else CustomOutputFormatWithDetailsAndHeaders formatAsJSONWithHeaders
, destination = Logger $ loggerSet $ appLogger foundation
}
makeAuthWare :: RegistryCtx -> Middleware makeAuthWare :: RegistryCtx -> Middleware
makeAuthWare _ app req res = next makeAuthWare _ app req res = next
@@ -344,40 +377,47 @@ makeAuthWare _ app req res = next
next :: IO ResponseReceived next :: IO ResponseReceived
next = app req res next = app req res
-- | Warp settings for the given foundation value. -- | Warp settings for the given foundation value.
warpSettings :: AppPort -> RegistryCtx -> Settings warpSettings :: AppPort -> RegistryCtx -> Settings
warpSettings port foundation = warpSettings port foundation =
setTimeout 60 setTimeout 60 $
$ setPort (fromIntegral port) setPort (fromIntegral port) $
$ setHost (appHost $ appSettings foundation) setHost (appHost $ appSettings foundation) $
$ setOnException (\_req e -> setOnException
when (defaultShouldDisplayException e) $ messageLoggerSource ( \_req e ->
foundation when (defaultShouldDisplayException e) $
(appLogger foundation) messageLoggerSource
$(qLocation >>= liftLoc) foundation
"yesod" (appLogger foundation)
LevelError $(qLocation >>= liftLoc)
(toLogStr $ "Exception from Warp: " ++ show e)) "yesod"
(setHTTP2Disabled defaultSettings) LevelError
(toLogStr $ "Exception from Warp: " ++ show e)
)
(setHTTP2Disabled defaultSettings)
getAppSettings :: IO AppSettings getAppSettings :: IO AppSettings
getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv
-- | The @main@ function for an executable running this site. -- | The @main@ function for an executable running this site.
appMain :: IO () appMain :: IO ()
appMain = do appMain = do
hSetBuffering stdout LineBuffering hSetBuffering stdout LineBuffering
-- Get the settings from all relevant sources -- Get the settings from all relevant sources
settings <- loadYamlSettingsArgs settings <-
-- fall back to compile-time values, set to [] to require values at runtime loadYamlSettingsArgs
[configSettingsYmlValue] -- fall back to compile-time values, set to [] to require values at runtime
[configSettingsYmlValue]
-- allow environment variables to override -- allow environment variables to override
useEnv useEnv
-- Generate the foundation from the settings -- Generate the foundation from the settings
makeFoundation settings >>= startApp makeFoundation settings >>= startApp
startApp :: RegistryCtx -> IO () startApp :: RegistryCtx -> IO ()
startApp foundation = do startApp foundation = do
when (sslAuto . appSettings $ foundation) $ do when (sslAuto . appSettings $ foundation) $ do
@@ -387,33 +427,38 @@ startApp foundation = do
runLog $ $logInfo "SSL Setup Complete" runLog $ $logInfo "SSL Setup Complete"
-- certbot renew loop -- certbot renew loop
void . forkIO $ forever $ flip runReaderT foundation $ do void . forkIO $
shouldRenew <- doesSslNeedRenew forever $
runLog $ $logInfo [i|Checking if SSL Certs should be renewed: #{shouldRenew}|] flip runReaderT foundation $ do
when shouldRenew $ do shouldRenew <- doesSslNeedRenew
runLog $ $logInfo "Renewing SSL Certs." runLog $ $logInfo [i|Checking if SSL Certs should be renewed: #{shouldRenew}|]
renewSslCerts when shouldRenew $ do
liftIO $ restartWeb foundation runLog $ $logInfo "Renewing SSL Certs."
liftIO $ sleep 86_400 renewSslCerts
liftIO $ restartWeb foundation
liftIO $ sleep 86_400
startWeb foundation startWeb foundation
where where
runLog :: MonadIO m => LoggingT m a -> m a runLog :: MonadIO m => LoggingT m a -> m a
runLog a = runLoggingT a (messageLoggerSource foundation (appLogger foundation)) runLog a = runLoggingT a (messageLoggerSource foundation (appLogger foundation))
startWeb :: RegistryCtx -> IO () startWeb :: RegistryCtx -> IO ()
startWeb foundation = do startWeb foundation = do
app <- makeApplication foundation app <- makeApplication foundation
startWeb' app startWeb' app
where where
startWeb' app = (`onException` appStopFsNotifyEos foundation) $ do startWeb' app = (`onException` appStopFsNotifyEos foundation) $ do
let AppSettings {..} = appSettings foundation let AppSettings{..} = appSettings foundation
runLog $ $logInfo [i|Launching Tor Web Server on port #{torPort}|] runLog $ $logInfo [i|Launching Tor Web Server on port #{torPort}|]
torAction <- async $ runSettings (warpSettings torPort foundation) app torAction <- async $ runSettings (warpSettings torPort foundation) app
runLog $ $logInfo [i|Launching Web Server on port #{appPort}|] runLog $ $logInfo [i|Launching Web Server on port #{appPort}|]
action <- async $ if sslAuto action <-
then runTLS (tlsSettings sslCertLocation sslKeyLocation) (warpSettings appPort foundation) app async $
else runSettings (warpSettings appPort foundation) app if sslAuto
then runTLS (tlsSettings sslCertLocation sslKeyLocation) (warpSettings appPort foundation) app
else runSettings (warpSettings appPort foundation) app
setWebProcessThreadId (asyncThreadId action, asyncThreadId torAction) foundation setWebProcessThreadId (asyncThreadId action, asyncThreadId torAction) foundation
res <- waitEitherCatchCancel action torAction res <- waitEitherCatchCancel action torAction
@@ -439,52 +484,60 @@ startWeb foundation = do
startWeb' app startWeb' app
runLog a = runLoggingT a (messageLoggerSource foundation (appLogger foundation)) runLog a = runLoggingT a (messageLoggerSource foundation (appLogger foundation))
restartWeb :: RegistryCtx -> IO () restartWeb :: RegistryCtx -> IO ()
restartWeb foundation = do restartWeb foundation = do
void $ swapMVar (appShouldRestartWeb foundation) True void $ swapMVar (appShouldRestartWeb foundation) True
shutdownWeb foundation shutdownWeb foundation
shutdownAll :: [ThreadId] -> IO () shutdownAll :: [ThreadId] -> IO ()
shutdownAll threadIds = do shutdownAll threadIds = do
for_ threadIds killThread for_ threadIds killThread
exitImmediately ExitSuccess exitImmediately ExitSuccess
-- Careful, you should always spawn this within forkIO so as to avoid accidentally killing the running process -- Careful, you should always spawn this within forkIO so as to avoid accidentally killing the running process
shutdownWeb :: RegistryCtx -> IO () shutdownWeb :: RegistryCtx -> IO ()
shutdownWeb RegistryCtx {..} = do shutdownWeb RegistryCtx{..} = do
threadIds <- takeMVar appWebServerThreadId threadIds <- takeMVar appWebServerThreadId
void $ both killThread threadIds void $ both killThread threadIds
-------------------------------------------------------------- --------------------------------------------------------------
-- Functions for DevelMain.hs (a way to run the RegistryCtx from GHCi) -- Functions for DevelMain.hs (a way to run the RegistryCtx from GHCi)
-------------------------------------------------------------- --------------------------------------------------------------
getApplicationRepl :: IO (Int, RegistryCtx, Application) getApplicationRepl :: IO (Int, RegistryCtx, Application)
getApplicationRepl = do getApplicationRepl = do
settings <- getAppSettings settings <- getAppSettings
foundation <- getAppSettings >>= makeFoundation foundation <- getAppSettings >>= makeFoundation
wsettings <- getDevSettings $ warpSettings (appPort settings) foundation wsettings <- getDevSettings $ warpSettings (appPort settings) foundation
app1 <- makeApplication foundation app1 <- makeApplication foundation
return (getPort wsettings, foundation, app1) return (getPort wsettings, foundation, app1)
shutdownApp :: RegistryCtx -> IO () shutdownApp :: RegistryCtx -> IO ()
shutdownApp _ = return () shutdownApp _ = return ()
-- | For yesod devel, return the Warp settings and WAI Application. -- | For yesod devel, return the Warp settings and WAI Application.
getApplicationDev :: AppPort -> IO (Settings, Application) getApplicationDev :: AppPort -> IO (Settings, Application)
getApplicationDev port = do getApplicationDev port = do
settings <- getAppSettings settings <- getAppSettings
foundation <- makeFoundation settings foundation <- makeFoundation settings
app <- makeApplication foundation app <- makeApplication foundation
wsettings <- getDevSettings $ warpSettings port foundation wsettings <- getDevSettings $ warpSettings port foundation
return (wsettings, app) return (wsettings, app)
-- | main function for use by yesod devel -- | main function for use by yesod devel
develMain :: IO () develMain :: IO ()
develMain = do develMain = do
settings <- getAppSettings settings <- getAppSettings
develMainHelper $ getApplicationDev $ appPort settings develMainHelper $ getApplicationDev $ appPort settings
--------------------------------------------- ---------------------------------------------
-- Functions for use in development with GHCi -- Functions for use in development with GHCi
--------------------------------------------- ---------------------------------------------
@@ -493,6 +546,7 @@ develMain = do
handler :: Handler a -> IO a handler :: Handler a -> IO a
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h
-- | Run DB queries -- | Run DB queries
db :: ReaderT SqlBackend (HandlerFor RegistryCtx) a -> IO a db :: ReaderT SqlBackend (HandlerFor RegistryCtx) a -> IO a
db = handler . runDB db = handler . runDB

View File

@@ -1,9 +1,10 @@
{-# LANGUAGE BlockArguments #-}
{-# HLINT ignore "Fuse on/on" #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Fuse on/on" #-}
module Database.Marketplace where module Database.Marketplace where
import Conduit ( import Conduit (
@@ -82,25 +83,26 @@ import Startlude (
Maybe (..), Maybe (..),
Monad, Monad,
MonadIO, MonadIO,
NonEmpty,
ReaderT, ReaderT,
Show, Show,
Text, Text,
headMay, headMay,
lift,
snd, snd,
sortOn, sortOn,
($), ($),
($>), ($>),
(.), (.),
(<$>), (<$>),
(<<$>>),
) )
data PackageMetadata = PackageMetadata data PackageMetadata = PackageMetadata
{ packageMetadataPkgId :: !PkgId { packageMetadataPkgId :: !PkgId
, packageMetadataPkgVersionRecords :: ![Entity VersionRecord] , packageMetadataPkgVersionRecords :: !(NonEmpty VersionRecord)
, packageMetadataPkgCategories :: ![Entity Category]
, packageMetadataPkgVersion :: !Version , packageMetadataPkgVersion :: !Version
, packageMetadataPkgCategories :: ![Category]
} }
deriving (Eq, Show) deriving (Eq, Show)
data PackageDependencyMetadata = PackageDependencyMetadata data PackageDependencyMetadata = PackageDependencyMetadata
@@ -111,12 +113,12 @@ data PackageDependencyMetadata = PackageDependencyMetadata
deriving (Eq, Show) deriving (Eq, Show)
searchServices :: serviceQuerySource ::
(MonadResource m, MonadIO m) => (MonadResource m, MonadIO m) =>
Maybe Text -> Maybe Text ->
Text -> Text ->
ConduitT () (Entity VersionRecord) (ReaderT SqlBackend m) () ConduitT () (Entity VersionRecord) (ReaderT SqlBackend m) ()
searchServices Nothing query = selectSource $ do serviceQuerySource Nothing query = selectSource $ do
service <- from $ table @VersionRecord service <- from $ table @VersionRecord
where_ where_
( (service ^. VersionRecordDescShort `ilike` (%) ++. val query ++. (%)) ( (service ^. VersionRecordDescShort `ilike` (%) ++. val query ++. (%))
@@ -130,7 +132,7 @@ searchServices Nothing query = selectSource $ do
, desc (service ^. VersionRecordUpdatedAt) , desc (service ^. VersionRecordUpdatedAt)
] ]
pure service pure service
searchServices (Just category) query = selectSource $ do serviceQuerySource (Just category) query = selectSource $ do
services <- services <-
from from
( do ( do
@@ -162,8 +164,8 @@ searchServices (Just category) query = selectSource $ do
pure services pure services
getPkgData :: (MonadResource m, MonadIO m) => [PkgId] -> ConduitT () (Entity VersionRecord) (ReaderT SqlBackend m) () getPkgDataSource :: (MonadResource m, MonadIO m) => [PkgId] -> ConduitT () (Entity VersionRecord) (ReaderT SqlBackend m) ()
getPkgData pkgs = selectSource $ do getPkgDataSource pkgs = selectSource $ do
pkgData <- from $ table @VersionRecord pkgData <- from $ table @VersionRecord
where_ (pkgData ^. VersionRecordPkgId `in_` valList (PkgRecordKey <$> pkgs)) where_ (pkgData ^. VersionRecordPkgId `in_` valList (PkgRecordKey <$> pkgs))
pure pkgData pure pkgData
@@ -188,29 +190,24 @@ getPkgDependencyData pkgId pkgVersion = select $ do
) )
zipCategories :: getCategoriesFor ::
MonadUnliftIO m => MonadUnliftIO m =>
ConduitT PkgId ->
(PkgId, [Entity VersionRecord]) ReaderT SqlBackend m [Category]
(PkgId, [Entity VersionRecord], [Entity Category]) getCategoriesFor pkg =
(ReaderT SqlBackend m) entityVal <<$>> select do
() (sc :& cat) <-
zipCategories = awaitForever $ \(pkg, vers) -> do from $
raw <- lift $ table @PkgCategory
select $ do `innerJoin` table @Category
(sc :& cat) <- `on` (\(sc :& cat) -> sc ^. PkgCategoryCategoryId ==. cat ^. CategoryId)
from $ where_ (sc ^. PkgCategoryPkgId ==. val (PkgRecordKey pkg))
table @PkgCategory pure cat
`innerJoin` table @Category
`on` (\(sc :& cat) -> sc ^. PkgCategoryCategoryId ==. cat ^. CategoryId)
where_ (sc ^. PkgCategoryPkgId ==. val (PkgRecordKey pkg))
pure cat
yield (pkg, vers, raw)
collateVersions :: collateVersions ::
MonadUnliftIO m => MonadUnliftIO m =>
ConduitT (Entity VersionRecord) (PkgId, [Entity VersionRecord]) (ReaderT SqlBackend m) () ConduitT (Entity VersionRecord) (PkgId, [VersionRecord]) (ReaderT SqlBackend m) ()
collateVersions = awaitForever $ \v0 -> do collateVersions = awaitForever $ \v0 -> do
let pkg = unPkgRecordKey . versionRecordPkgId $ entityVal v0 let pkg = unPkgRecordKey . versionRecordPkgId $ entityVal v0
let pull = do let pull = do
@@ -221,7 +218,7 @@ collateVersions = awaitForever $ \v0 -> do
let pkg' = unPkgRecordKey . versionRecordPkgId $ entityVal vn let pkg' = unPkgRecordKey . versionRecordPkgId $ entityVal vn
if pkg == pkg' then pure (Just vn) else leftover vn $> Nothing if pkg == pkg' then pure (Just vn) else leftover vn $> Nothing
ls <- unfoldM pull ls <- unfoldM pull
yield (pkg, v0 : ls) yield (pkg, fmap entityVal $ v0 : ls)
zipDependencyVersions :: zipDependencyVersions ::

View File

@@ -62,7 +62,6 @@ import Database.Persist.Sql (
SqlPersistT, SqlPersistT,
runSqlPool, runSqlPool,
) )
import Lib.Registry (S9PK)
import Yesod.Core ( import Yesod.Core (
AuthResult (Authorized, Unauthorized), AuthResult (Authorized, Unauthorized),
LogLevel (..), LogLevel (..),
@@ -112,7 +111,7 @@ import Lib.PkgRepository (
EosRepo, EosRepo,
PkgRepo, PkgRepo,
) )
import Lib.Types.AppIndex (PkgId) import Lib.Types.AppIndex (PkgId, S9PK)
import Model ( import Model (
Admin (..), Admin (..),
Key (AdminKey), Key (AdminKey),

View File

@@ -1,121 +0,0 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Handler.Apps where
import Startlude (
Applicative (pure),
FilePath,
Maybe (..),
Monad ((>>=)),
Show,
String,
show,
void,
($),
(.),
)
import Control.Monad.Logger (logError)
import Data.Text qualified as T
import GHC.Show qualified (Show (..))
import Network.HTTP.Types (status404)
import System.FilePath (
takeBaseName,
(<.>),
)
import Yesod.Core (
Content (ContentFile),
TypedContent,
addHeader,
notFound,
respond,
respondSource,
sendChunkBS,
sendResponseStatus,
typeJson,
typeOctet,
)
import Yesod.Persist.Core (YesodPersist (runDB))
import Conduit (
awaitForever,
(.|),
)
import Data.String.Interpolate.IsString (
i,
)
import Database.Queries (
createMetric,
fetchApp,
fetchAppVersion,
)
import Foundation (Handler)
import Handler.Util (addPackageHeader, getVersionSpecFromQuery, orThrow, versionPriorityFromQueryIsMin)
import Lib.Error (S9Error (NotFoundE))
import Lib.PkgRepository (
getBestVersion,
getManifest,
getPackage,
)
import Lib.Registry (S9PK)
import Lib.Types.AppIndex (PkgId (PkgId))
import Lib.Types.Emver (Version)
data FileExtension = FileExtension !FilePath !(Maybe String)
instance Show FileExtension where
show (FileExtension f Nothing) = f
show (FileExtension f (Just e)) = f <.> e
getAppManifestR :: PkgId -> Handler TypedContent
getAppManifestR pkg = do
versionSpec <- getVersionSpecFromQuery
preferMin <- versionPriorityFromQueryIsMin
version <-
getBestVersion pkg versionSpec preferMin
`orThrow` sendResponseStatus status404 (NotFoundE [i|#{pkg} satisfying #{versionSpec}|])
addPackageHeader pkg version
(len, src) <- getManifest pkg version
addHeader "Content-Length" (show len)
respondSource typeJson $ src .| awaitForever sendChunkBS
getAppR :: S9PK -> Handler TypedContent
getAppR file = do
let pkg = PkgId . T.pack $ takeBaseName (show file)
versionSpec <- getVersionSpecFromQuery
preferMin <- versionPriorityFromQueryIsMin
version <-
getBestVersion pkg versionSpec preferMin
`orThrow` sendResponseStatus status404 (NotFoundE [i|#{pkg} satisfying #{versionSpec}|])
addPackageHeader pkg version
void $ recordMetrics pkg version
pkgPath <-
getPackage pkg version >>= \case
Nothing -> sendResponseStatus status404 (NotFoundE [i|#{pkg}@#{version}|])
Just a -> pure a
respond typeOctet $ ContentFile pkgPath Nothing
recordMetrics :: PkgId -> Version -> Handler ()
recordMetrics pkg appVersion = do
sa <- runDB $ fetchApp pkg
case sa of
Nothing -> do
$logError [i|#{pkg} not found in database|]
notFound
Just _ -> do
existingVersion <- runDB $ fetchAppVersion pkg appVersion
case existingVersion of
Nothing -> do
$logError [i|#{pkg}@#{appVersion} not found in database|]
notFound
Just _ -> runDB $ createMetric pkg appVersion

5
src/Handler/Eos.hs Normal file
View File

@@ -0,0 +1,5 @@
module Handler.Eos (module X) where
import Handler.Eos.V0.EosImg as X
import Handler.Eos.V0.Latest as X

View File

@@ -1 +1,53 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Handler.Eos.V0.EosImg where module Handler.Eos.V0.EosImg where
import Crypto.Hash (SHA256)
import Crypto.Hash.Conduit (hashFile)
import Data.Attoparsec.Text qualified as Atto
import Data.ByteArray.Encoding (Base (..), convertToBase)
import Data.String.Interpolate.IsString (i)
import Data.Text qualified as T
import Database.Persist (Entity (..), insertUnique)
import Database.Persist.Class (getBy)
import Foundation (Handler, RegistryCtx (..))
import Handler.Util (getVersionSpecFromQuery)
import Lib.Error (S9Error (..))
import Lib.Types.Emver (Version (..), parseVersion, satisfies)
import Model (EosHash (..), Unique (..))
import Network.HTTP.Types (status404)
import Settings (AppSettings (..))
import Startlude (Down (..), FilePath, Maybe (..), Text, decodeUtf8, filter, for_, headMay, partitionEithers, pure, show, sortOn, void, ($), (.), (<$>))
import System.FilePath ((</>))
import UnliftIO.Directory (listDirectory)
import Yesod (Content (..), TypedContent, YesodDB, YesodPersist (runDB), addHeader, getsYesod, respond, sendResponseStatus, typeOctet)
import Yesod.Core (logWarn)
getEosR :: Handler TypedContent
getEosR = do
spec <- getVersionSpecFromQuery
root <- getsYesod $ (</> "eos") . resourcesDir . appSettings
subdirs <- listDirectory root
let (failures, successes) = partitionEithers $ Atto.parseOnly parseVersion . T.pack <$> subdirs
for_ failures $ \f -> $logWarn [i|Emver Parse Failure for EOS: #{f}|]
let mVersion = headMay . sortOn Down . filter (`satisfies` spec) $ successes
case mVersion of
Nothing -> sendResponseStatus status404 (NotFoundE [i|EOS version satisfying #{spec}|])
Just version -> do
let imgPath = root </> show version </> "eos.img"
h <- runDB $ retrieveHash version imgPath
addHeader "x-eos-hash" h
respond typeOctet $ ContentFile imgPath Nothing
where
retrieveHash :: Version -> FilePath -> YesodDB RegistryCtx Text
retrieveHash v fp = do
mHash <- getBy (UniqueVersion v)
case mHash of
Just h -> pure . eosHashHash . entityVal $ h
Nothing -> do
h <- hashFile @_ @SHA256 fp
let t = decodeUtf8 $ convertToBase Base16 h
void $ insertUnique (EosHash v t) -- lazily populate
pure t

View File

@@ -3,11 +3,25 @@
module Handler.Eos.V0.Latest where module Handler.Eos.V0.Latest where
import Data.Aeson (ToJSON (toJSON), object, (.=)) import Data.Aeson (ToJSON (toJSON), object, (.=))
import Handler.Package.V0.ReleaseNotes (ReleaseNotes) import Data.HashMap.Strict qualified as HM
import Lib.Types.Emver (Version) import Database.Esqueleto.Experimental (
Entity (entityVal),
desc,
from,
orderBy,
select,
table,
(^.),
)
import Foundation (Handler)
import Handler.Package.V0.ReleaseNotes (ReleaseNotes (..))
import Handler.Util (queryParamAs)
import Lib.Types.Emver (Version, parseVersion)
import Model (EntityField (..), OsVersion (..))
import Orphans.Emver () import Orphans.Emver ()
import Startlude (Eq, Generic, Show, Text, (.)) import Startlude (Bool (..), Down (..), Eq, Generic, Maybe, Ord ((<)), Show, Text, const, filter, fst, head, maybe, pure, sortOn, ($), (&&&), (.), (<$>), (<&>))
import Yesod (ToContent (toContent), ToTypedContent (..)) import Yesod (ToContent (toContent), ToTypedContent (..), YesodPersist (runDB))
import Yesod.Core.Types (JSONResponse (..))
data EosRes = EosRes data EosRes = EosRes
@@ -22,4 +36,30 @@ instance ToJSON EosRes where
instance ToContent EosRes where instance ToContent EosRes where
toContent = toContent . toJSON toContent = toContent . toJSON
instance ToTypedContent EosRes where instance ToTypedContent EosRes where
toTypedContent = toTypedContent . toJSON toTypedContent = toTypedContent . toJSON
getEosVersionR :: Handler (JSONResponse (Maybe EosRes))
getEosVersionR = do
eosVersion <- queryParamAs "eos-version" parseVersion
allEosVersions <- runDB $
select $ do
vers <- from $ table @OsVersion
orderBy [desc (vers ^. OsVersionCreatedAt)]
pure vers
let osV = entityVal <$> allEosVersions
let mLatest = head osV
let mappedVersions =
ReleaseNotes $
HM.fromList $
sortOn (Down . fst) $
filter (maybe (const True) (<) eosVersion . fst) $
((osVersionNumber &&& osVersionReleaseNotes))
<$> osV
pure . JSONResponse $
mLatest <&> \latest ->
EosRes
{ eosResVersion = osVersionNumber latest
, eosResHeadline = osVersionHeadline latest
, eosResReleaseNotes = mappedVersions
}

View File

@@ -1,66 +0,0 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
module Handler.ErrorLogs where
import Data.Aeson ( (.:)
, FromJSON(parseJSON)
, withObject
)
import Foundation ( Handler )
import Model ( EntityField(ErrorLogRecordIncidents)
, ErrorLogRecord(ErrorLogRecord)
)
import Startlude ( ($)
, Applicative(pure)
, Eq
, MonadIO(liftIO)
, Show
, Text
, Word32
, getCurrentTime
, void
)
import Yesod.Core ( requireCheckJsonBody )
import Yesod.Persist ( (+=.)
, runDB
, upsert
)
data ErrorLog = ErrorLog
{ errorLogEpoch :: !Text
, errorLogCommitHash :: !Text
, errorLogSourceFile :: !Text
, errorLogLine :: !Word32
, errorLogTarget :: !Text
, errorLogLevel :: !Text
, errorLogMessage :: !Text
}
deriving (Eq, Show)
instance FromJSON ErrorLog where
parseJSON = withObject "Error Log" $ \o -> do
errorLogEpoch <- o .: "log-epoch"
errorLogCommitHash <- o .: "commit-hash"
errorLogSourceFile <- o .: "file"
errorLogLine <- o .: "line"
errorLogLevel <- o .: "level"
errorLogTarget <- o .: "target"
errorLogMessage <- o .: "log-message"
pure ErrorLog { .. }
postErrorLogsR :: Handler ()
postErrorLogsR = do
ErrorLog {..} <- requireCheckJsonBody @_ @ErrorLog
void $ runDB $ do
now <- liftIO getCurrentTime
let logRecord = ErrorLogRecord now
errorLogEpoch
errorLogCommitHash
errorLogSourceFile
errorLogLine
errorLogTarget
errorLogLevel
errorLogMessage
1
upsert logRecord [ErrorLogRecordIncidents +=. 1]

View File

@@ -1,18 +0,0 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
module Handler.Icons where
import Data.Aeson (FromJSON, ToJSON)
import Startlude (Eq, Generic, Read, Show)
data IconType = PNG | JPG | JPEG | SVG
deriving (Eq, Show, Generic, Read)
instance ToJSON IconType
instance FromJSON IconType

View File

@@ -1,306 +0,0 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Redundant <$>" #-}
module Handler.Marketplace where
import Startlude (
Applicative (pure, (*>)),
Bool (True),
ByteString,
Down (Down),
Either (Left, Right),
FilePath,
Foldable (foldMap),
Functor (fmap),
Int,
Maybe (..),
Monad ((>>=)),
MonadIO,
MonadReader,
Monoid (mappend),
Num ((*), (-)),
Ord ((<)),
ReaderT (runReaderT),
Text,
Traversable (traverse),
catMaybes,
const,
decodeUtf8,
encodeUtf8,
filter,
flip,
for_,
fromMaybe,
fst,
head,
headMay,
id,
maybe,
partitionEithers,
readMaybe,
show,
snd,
void,
($),
(&&&),
(.),
(<$>),
(<&>),
)
import Conduit (
dropC,
runConduit,
sinkList,
takeC,
(.|),
)
import Control.Monad.Logger (
MonadLogger,
logWarn,
)
import Control.Monad.Reader.Has (
Has,
ask,
)
import Crypto.Hash (SHA256)
import Crypto.Hash.Conduit (hashFile)
import Data.Aeson (
decode,
eitherDecode,
eitherDecodeStrict,
)
import Data.Attoparsec.Text qualified as Atto
import Data.Attoparsec.Text (
Parser,
parseOnly,
)
import Data.ByteArray.Encoding (
Base (..),
convertToBase,
)
import Data.ByteString.Base64 (encodeBase64)
import Data.ByteString.Lazy qualified as LBS
import Data.Conduit.List qualified as CL
import Data.HashMap.Strict qualified as HM
import Data.List (
lookup,
sortOn,
)
import Data.String.Interpolate.IsString (
i,
)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TB
import Database.Esqueleto.Experimental (
Entity (entityKey, entityVal),
SqlBackend,
asc,
desc,
from,
orderBy,
select,
table,
(^.),
)
import Database.Marketplace (
collateVersions,
fetchAllAppVersions,
fetchLatestApp,
getPkgData,
getPkgDependencyData,
searchServices,
zipCategories,
zipDependencyVersions,
)
import Database.Persist (
PersistUniqueRead (getBy),
insertUnique,
)
import Foundation (
Handler,
RegistryCtx (appConnPool, appSettings),
Route (InstructionsR, LicenseR),
)
import Handler.Util (getVersionSpecFromQuery)
import Lib.Error (S9Error (..))
import Lib.PkgRepository (
PkgRepo,
getIcon,
getManifest,
)
import Lib.Types.AppIndex (PkgId)
import Lib.Types.Emver (
Version,
VersionRange,
parseRange,
parseVersion,
satisfies,
)
import Model (
Category (..),
EntityField (..),
EosHash (EosHash, eosHashHash),
Key (PkgRecordKey, unPkgRecordKey),
OsVersion (..),
PkgRecord (..),
Unique (UniqueVersion),
VersionRecord (..),
)
import Network.HTTP.Types (
status400,
status404,
)
import Protolude.Unsafe (unsafeFromJust)
import Settings (AppSettings (marketplaceName, resourcesDir))
import System.FilePath ((</>))
import UnliftIO.Async (mapConcurrently)
import UnliftIO.Directory (listDirectory)
import Util.Shared (
filterDependencyBestVersion,
filterDependencyOsCompatible,
filterLatestVersionFromSpec,
filterPkgOsCompatible,
)
import Yesod.Core (
Content (ContentFile),
MonadHandler,
MonadResource,
RenderRoute (renderRoute),
TypedContent,
YesodRequest (..),
addHeader,
getRequest,
getYesod,
getsYesod,
lookupGetParam,
respond,
sendResponseStatus,
typeOctet,
)
import Yesod.Core.Types (JSONResponse (..))
import Yesod.Persist (YesodDB)
import Yesod.Persist.Core (YesodPersist (runDB))
queryParamAs :: MonadHandler m => Text -> Parser a -> m (Maybe a)
queryParamAs k p =
lookupGetParam k >>= \case
Nothing -> pure Nothing
Just x -> case parseOnly p x of
Left e ->
sendResponseStatus @_ @Text status400 [i|Invalid Request! The query parameter '#{k}' failed to parse: #{e}|]
Right a -> pure (Just a)
getInfoR :: Handler (JSONResponse InfoRes)
getInfoR = do
name <- getsYesod $ marketplaceName . appSettings
allCategories <- runDB $
select $ do
cats <- from $ table @Category
orderBy [asc (cats ^. CategoryPriority)]
pure cats
pure $ JSONResponse $ InfoRes name $ categoryName . entityVal <$> allCategories
getEosVersionR :: Handler (JSONResponse (Maybe EosRes))
getEosVersionR = do
eosVersion <- queryParamAs "eos-version" parseVersion
allEosVersions <- runDB $
select $ do
vers <- from $ table @OsVersion
orderBy [desc (vers ^. OsVersionCreatedAt)]
pure vers
let osV = entityVal <$> allEosVersions
let mLatest = head osV
let mappedVersions =
ReleaseNotes $
HM.fromList $
sortOn (Down . fst) $
filter (maybe (const True) (<) eosVersion . fst) $
(\v -> (osVersionNumber v, osVersionReleaseNotes v))
<$> osV
pure . JSONResponse $
mLatest <&> \latest ->
EosRes
{ eosResVersion = osVersionNumber latest
, eosResHeadline = osVersionHeadline latest
, eosResReleaseNotes = mappedVersions
}
getReleaseNotesR :: PkgId -> Handler ReleaseNotes
getReleaseNotesR pkg = do
appConnPool <- appConnPool <$> getYesod
versionRecords <- runDB $ fetchAllAppVersions appConnPool pkg
pure $ constructReleaseNotesApiRes versionRecords
where
constructReleaseNotesApiRes :: [VersionRecord] -> ReleaseNotes
constructReleaseNotesApiRes vers = do
ReleaseNotes $
HM.fromList $
sortOn (Down . fst) $
(versionRecordNumber &&& versionRecordReleaseNotes)
<$> vers
getEosR :: Handler TypedContent
getEosR = do
spec <- getVersionSpecFromQuery
root <- getsYesod $ (</> "eos") . resourcesDir . appSettings
subdirs <- listDirectory root
let (failures, successes) = partitionEithers $ Atto.parseOnly parseVersion . T.pack <$> subdirs
for_ failures $ \f -> $logWarn [i|Emver Parse Failure for EOS: #{f}|]
let mVersion = headMay . sortOn Down . filter (`satisfies` spec) $ successes
case mVersion of
Nothing -> sendResponseStatus status404 (NotFoundE [i|EOS version satisfying #{spec}|])
Just version -> do
let imgPath = root </> show version </> "eos.img"
h <- runDB $ retrieveHash version imgPath
addHeader "x-eos-hash" h
respond typeOctet $ ContentFile imgPath Nothing
where
retrieveHash :: Version -> FilePath -> YesodDB RegistryCtx Text
retrieveHash v fp = do
mHash <- getBy (UniqueVersion v)
case mHash of
Just h -> pure . eosHashHash . entityVal $ h
Nothing -> do
h <- hashFile @_ @SHA256 fp
let t = decodeUtf8 $ convertToBase Base16 h
void $ insertUnique (EosHash v t) -- lazily populate
pure t
-- TODO refactor with conduit
getVersionLatestR :: Handler VersionLatestRes
getVersionLatestR = do
getParameters <- reqGetParams <$> getRequest
case lookup "ids" getParameters of
Nothing -> sendResponseStatus status400 (InvalidParamsE "get:ids" "<MISSING>")
Just packages -> case eitherDecode $ LBS.fromStrict $ encodeUtf8 packages of
Left _ -> sendResponseStatus status400 (InvalidParamsE "get:ids" packages)
Right p -> do
let packageList = (,Nothing) <$> p
found <- runDB $ traverse fetchLatestApp $ fst <$> packageList
pure $
VersionLatestRes $
HM.union
( HM.fromList $
( \v ->
(unPkgRecordKey . entityKey $ fst v, Just $ versionRecordNumber $ entityVal $ snd v)
)
<$> catMaybes found
)
$ HM.fromList packageList

View File

@@ -1,14 +1,18 @@
module Handler.Package where module Handler.Package where
import Foundation (Handler) import Foundation (Handler)
import Handler.Package.V0.Index (PackageListRes) import Handler.Package.V0.Icon qualified
import Handler.Package.V0.Info (InfoRes) import Handler.Package.V0.Index (PackageListRes, getPackageIndexR)
import Handler.Package.V0.Latest (VersionLatestRes) import Handler.Package.V0.Info (InfoRes, getInfoR)
import Handler.Package.V0.ReleaseNotes (ReleaseNotes) import Handler.Package.V0.Instructions qualified
import Handler.Types.Api (ApiVersion) import Handler.Package.V0.Latest (VersionLatestRes, getVersionLatestR)
import Handler.Types.Status (AppVersionRes) import Handler.Package.V0.License qualified
import Lib.Registry (S9PK) import Handler.Package.V0.Manifest qualified
import Lib.Types.AppIndex (PkgId) import Handler.Package.V0.ReleaseNotes (ReleaseNotes, getReleaseNotesR)
import Handler.Package.V0.S9PK qualified
import Handler.Package.V0.Version (AppVersionRes, getPkgVersionR)
import Handler.Types.Api (ApiVersion (..))
import Lib.Types.AppIndex (PkgId, S9PK)
import Yesod.Core.Types ( import Yesod.Core.Types (
JSONResponse, JSONResponse,
TypedContent, TypedContent,
@@ -16,40 +20,40 @@ import Yesod.Core.Types (
getInfoR :: ApiVersion -> Handler (JSONResponse InfoRes) getInfoR :: ApiVersion -> Handler (JSONResponse InfoRes)
getInfoR = _ getInfoR _ = Handler.Package.V0.Info.getInfoR
getPackageListR :: ApiVersion -> Handler PackageListRes getPackageIndexR :: ApiVersion -> Handler PackageListRes
getPackageListR = _ getPackageIndexR _ = Handler.Package.V0.Index.getPackageIndexR
getVersionLatestR :: ApiVersion -> Handler VersionLatestRes getVersionLatestR :: ApiVersion -> Handler VersionLatestRes
getVersionLatestR = _ getVersionLatestR _ = Handler.Package.V0.Latest.getVersionLatestR
getAppR :: ApiVersion -> S9PK -> Handler TypedContent getAppR :: ApiVersion -> S9PK -> Handler TypedContent
getAppR = _ getAppR _ = Handler.Package.V0.S9PK.getAppR
getAppManifestR :: ApiVersion -> PkgId -> Handler TypedContent getAppManifestR :: ApiVersion -> PkgId -> Handler TypedContent
getAppManifestR = _ getAppManifestR _ = Handler.Package.V0.Manifest.getAppManifestR
getReleaseNotesR :: ApiVersion -> PkgId -> Handler ReleaseNotes getReleaseNotesR :: ApiVersion -> PkgId -> Handler ReleaseNotes
getReleaseNotesR = _ getReleaseNotesR _ = Handler.Package.V0.ReleaseNotes.getReleaseNotesR
getIconsR :: ApiVersion -> PkgId -> Handler TypedContent getIconsR :: ApiVersion -> PkgId -> Handler TypedContent
getIconsR = _ getIconsR _ = Handler.Package.V0.Icon.getIconsR
getLicenseR :: ApiVersion -> PkgId -> Handler TypedContent getLicenseR :: ApiVersion -> PkgId -> Handler TypedContent
getLicenseR = _ getLicenseR _ = Handler.Package.V0.License.getLicenseR
getInstructionsR :: ApiVersion -> PkgId -> Handler TypedContent getInstructionsR :: ApiVersion -> PkgId -> Handler TypedContent
getInstructionsR = _ getInstructionsR _ = Handler.Package.V0.Instructions.getInstructionsR
getPkgVersionR :: ApiVersion -> PkgId -> Handler AppVersionRes getPkgVersionR :: ApiVersion -> PkgId -> Handler AppVersionRes
getPkgVersionR = _ getPkgVersionR _ = Handler.Package.V0.Version.getPkgVersionR

View File

@@ -3,28 +3,36 @@
module Handler.Package.V0.Index where module Handler.Package.V0.Index where
import Conduit (runConduit, (.|)) import Conduit (concatMapC, dropC, mapC, mapMC, runConduit, sinkList, takeC, (.|))
import Control.Monad.Reader.Has (Functor (fmap), Has, Monad ((>>=)), MonadReader, ReaderT (runReaderT), ask) import Control.Monad.Reader.Has (Functor (fmap), Has, Monad ((>>=)), MonadReader, ReaderT (runReaderT), ask)
import Data.Aeson (FromJSON (..), ToJSON (..), Value, decode, object, withObject, (.:), (.=)) import Data.Aeson (FromJSON (..), ToJSON (..), Value, decode, eitherDecodeStrict, object, withObject, (.:), (.=))
import Data.Attoparsec.Text qualified as Atto import Data.Attoparsec.Text qualified as Atto
import Data.ByteString.Base64 (encodeBase64) import Data.ByteString.Base64 (encodeBase64)
import Data.ByteString.Lazy qualified as LBS import Data.ByteString.Lazy qualified as LBS
import Data.Conduit.List qualified as CL import Data.Conduit.List qualified as CL
import Data.HashMap.Internal.Strict (HashMap) import Data.HashMap.Internal.Strict (HashMap)
import Data.HashMap.Strict qualified as HM import Data.HashMap.Strict qualified as HM
import Data.List (lookup)
import Data.List.NonEmpty qualified as NE
import Data.Text qualified as T import Data.Text qualified as T
import Database.Marketplace (PackageMetadata (..), collateVersions, getPkgDependencyData, searchServices, zipDependencyVersions) import Database.Marketplace (PackageMetadata (..), collateVersions, getCategoriesFor, getPkgDataSource, getPkgDependencyData, serviceQuerySource, zipDependencyVersions)
import Database.Persist (Entity (..), Key) import Database.Persist (Key)
import Database.Persist.Sql (SqlBackend) import Database.Persist.Sql (SqlBackend)
import Foundation (Handler, Route (InstructionsR, LicenseR)) import Foundation (Handler, Route (InstructionsR, LicenseR))
import Handler.Types.Api (ApiVersion (..))
import Handler.Util (basicRender)
import Lib.Conduit (filterDependencyBestVersion, filterDependencyOsCompatible, selectLatestVersionFromSpec)
import Lib.Error (S9Error (..)) import Lib.Error (S9Error (..))
import Lib.PkgRepository (PkgRepo, getIcon, getManifest) import Lib.PkgRepository (PkgRepo, getIcon, getManifest)
import Lib.Types.AppIndex (PkgId) import Lib.Types.AppIndex (PkgId)
import Lib.Types.Emver (Version, VersionRange, parseRange, satisfies) import Lib.Types.Emver (Version, VersionRange (..), parseRange, satisfies)
import Model (Category (..), Key (..), PkgRecord (..), VersionRecord (..)) import Model (Category (..), Key (..), PkgRecord (..), VersionRecord (..))
import Network.HTTP.Types (status400)
import Protolude.Unsafe (unsafeFromJust)
import Settings (AppSettings) import Settings (AppSettings)
import Startlude (Bool (..), ByteString, Either (..), Eq, Generic, Int, Maybe (..), MonadIO, Read, Show, Text, Traversable (traverse), catMaybes, const, flip, fromMaybe, id, pure, snd, ($), (.), (<$>), (<&>)) import Startlude (Applicative ((*>)), Bifunctor (second), Bool (..), ByteString, Either (..), Eq, Generic, Int, Maybe (..), MonadIO, Num ((*), (-)), Read, Show, Text, Traversable (traverse), catMaybes, const, encodeUtf8, filter, flip, fromMaybe, id, nonEmpty, pure, readMaybe, show, snd, ($), (&&&), (.), (<$>), (<&>))
import Yesod (MonadLogger, MonadResource, ToContent (..), ToTypedContent (..), YesodPersist (runDB), lookupGetParam) import UnliftIO (mapConcurrently)
import Yesod (MonadLogger, MonadResource, ToContent (..), ToTypedContent (..), YesodPersist (runDB), lookupGetParam, sendResponseStatus)
import Yesod.Core (logWarn) import Yesod.Core (logWarn)
@@ -108,8 +116,8 @@ data OrderArrangement = ASC | DESC
deriving (Eq, Show, Read) deriving (Eq, Show, Read)
getPackageListR :: Handler PackageListRes getPackageIndexR :: Handler PackageListRes
getPackageListR = do getPackageIndexR = do
osPredicate <- osPredicate <-
getOsVersionQuery <&> \case getOsVersionQuery <&> \case
Nothing -> const True Nothing -> const True
@@ -124,27 +132,39 @@ getPackageListR = do
query <- T.strip . fromMaybe (packageListQuery defaults) <$> lookupGetParam "query" query <- T.strip . fromMaybe (packageListQuery defaults) <$> lookupGetParam "query"
runDB $ runDB $
runConduit $ runConduit $
searchServices category query serviceQuerySource category query
-- group conduit pipeline by pkg id
.| collateVersions .| collateVersions
.| zipCategories -- filter out versions of apps that are incompatible with the OS predicate
-- empty list since there are no requested packages in this case .| mapC (second (filter (osPredicate . versionRecordOsVersion)))
.| filterLatestVersionFromSpec [] -- prune empty version sets
.| filterPkgOsCompatible osPredicate .| concatMapC (\(pkgId, vs) -> (pkgId,) <$> nonEmpty vs)
-- grab the latest matching version if it exists
.| concatMapC (\(a, b) -> (a,b,) <$> (selectLatestVersionFromSpec (const Any) b))
-- construct
.| mapMC (\(a, b, c) -> PackageMetadata a b (versionRecordNumber c) <$> getCategoriesFor a)
-- pages start at 1 for some reason. TODO: make pages start at 0 -- pages start at 1 for some reason. TODO: make pages start at 0
.| (dropC (limit' * (page - 1)) *> takeC limit') .| (dropC (limit' * (page - 1)) *> takeC limit')
.| sinkList .| sinkList
Just packages' -> do Just packages' -> do
-- for each item in list get best available from version range -- for each item in list get best available from version range
let vMap = (packageReqId &&& packageReqVersion) <$> packages' let packageRanges = fromMaybe None . (flip lookup $ (packageReqId &&& packageReqVersion) <$> packages')
runDB runDB
-- TODO could probably be better with sequenceConduits -- TODO could probably be better with sequenceConduits
. runConduit . runConduit
$ getPkgData (packageReqId <$> packages') $ getPkgDataSource (packageReqId <$> packages')
-- group conduit pipeline by pkg id
.| collateVersions .| collateVersions
.| zipCategories -- filter out versions of apps that are incompatible with the OS predicate
.| filterLatestVersionFromSpec vMap .| mapC (second (filter (osPredicate . versionRecordOsVersion)))
.| filterPkgOsCompatible osPredicate -- prune empty version sets
.| concatMapC (\(pkgId, vs) -> (pkgId,) <$> nonEmpty vs)
-- grab the latest matching version if it exists
.| concatMapC (\(a, b) -> (a,b,) <$> (selectLatestVersionFromSpec packageRanges b))
-- construct
.| mapMC (\(a, b, c) -> PackageMetadata a b (versionRecordNumber c) <$> getCategoriesFor a)
.| sinkList .| sinkList
-- NOTE: if a package's dependencies do not meet the system requirements, it is currently omitted from the list -- NOTE: if a package's dependencies do not meet the system requirements, it is currently omitted from the list
pkgsWithDependencies <- runDB $ mapConcurrently (getPackageDependencies osPredicate) filteredPackages pkgsWithDependencies <- runDB $ mapConcurrently (getPackageDependencies osPredicate) filteredPackages
PackageListRes <$> mapConcurrently constructPackageListApiRes pkgsWithDependencies PackageListRes <$> mapConcurrently constructPackageListApiRes pkgsWithDependencies
@@ -165,7 +185,8 @@ getPackageListR = do
Left _ -> Left _ ->
do do
let e = InvalidParamsE "get:ids" ids let e = InvalidParamsE "get:ids" ids
$logWarn (show e) sendResponseStatus status400 e $logWarn (show e)
sendResponseStatus status400 e
Right a -> pure a Right a -> pure a
getCategoryQuery :: Handler (Maybe Text) getCategoryQuery :: Handler (Maybe Text)
getCategoryQuery = getCategoryQuery =
@@ -175,7 +196,8 @@ getPackageListR = do
Nothing -> Nothing ->
do do
let e = InvalidParamsE "get:category" c let e = InvalidParamsE "get:category" c
$logWarn (show e) sendResponseStatus status400 e $logWarn (show e)
sendResponseStatus status400 e
Just t -> pure $ Just t Just t -> pure $ Just t
getPageQuery :: Handler Int getPageQuery :: Handler Int
getPageQuery = getPageQuery =
@@ -185,7 +207,8 @@ getPackageListR = do
Nothing -> Nothing ->
do do
let e = InvalidParamsE "get:page" p let e = InvalidParamsE "get:page" p
$logWarn (show e) sendResponseStatus status400 e $logWarn (show e)
sendResponseStatus status400 e
Just t -> pure $ case t of Just t -> pure $ case t of
0 -> 1 -- disallow page 0 so offset is not negative 0 -> 1 -- disallow page 0 so offset is not negative
_ -> t _ -> t
@@ -197,7 +220,8 @@ getPackageListR = do
Nothing -> Nothing ->
do do
let e = InvalidParamsE "get:per-page" pp let e = InvalidParamsE "get:per-page" pp
$logWarn (show e) sendResponseStatus status400 e $logWarn (show e)
sendResponseStatus status400 e
Just l -> pure l Just l -> pure l
getOsVersionQuery :: Handler (Maybe VersionRange) getOsVersionQuery :: Handler (Maybe VersionRange)
getOsVersionQuery = getOsVersionQuery =
@@ -207,7 +231,8 @@ getPackageListR = do
Left _ -> Left _ ->
do do
let e = InvalidParamsE "get:eos-version-compat" osv let e = InvalidParamsE "get:eos-version-compat" osv
$logWarn (show e) sendResponseStatus status400 e $logWarn (show e)
sendResponseStatus status400 e
Right v -> pure $ Just v Right v -> pure $ Just v
getPackageDependencies :: getPackageDependencies ::
(MonadIO m, MonadLogger m) => (MonadIO m, MonadLogger m) =>
@@ -225,13 +250,13 @@ getPackageListR = do
getPackageDependencies osPredicate PackageMetadata{packageMetadataPkgId = pkg, packageMetadataPkgVersionRecords = pkgVersions, packageMetadataPkgCategories = pkgCategories, packageMetadataPkgVersion = pkgVersion} = getPackageDependencies osPredicate PackageMetadata{packageMetadataPkgId = pkg, packageMetadataPkgVersionRecords = pkgVersions, packageMetadataPkgCategories = pkgCategories, packageMetadataPkgVersion = pkgVersion} =
do do
let pkgId = PkgRecordKey pkg let pkgId = PkgRecordKey pkg
let pkgVersions' = versionRecordNumber . entityVal <$> pkgVersions let pkgVersions' = versionRecordNumber <$> pkgVersions
let pkgCategories' = entityVal <$> pkgCategories let pkgCategories' = pkgCategories
pkgDepInfo <- getPkgDependencyData pkgId pkgVersion pkgDepInfo <- getPkgDependencyData pkgId pkgVersion
pkgDepInfoWithVersions <- traverse zipDependencyVersions pkgDepInfo pkgDepInfoWithVersions <- traverse zipDependencyVersions pkgDepInfo
let compatiblePkgDepInfo = fmap (filterDependencyOsCompatible osPredicate) pkgDepInfoWithVersions let compatiblePkgDepInfo = fmap (filterDependencyOsCompatible osPredicate) pkgDepInfoWithVersions
res <- catMaybes <$> traverse filterDependencyBestVersion compatiblePkgDepInfo res <- catMaybes <$> traverse filterDependencyBestVersion compatiblePkgDepInfo
pure (pkgId, pkgCategories', pkgVersions', pkgVersion, res) pure (pkgId, pkgCategories', NE.toList pkgVersions', pkgVersion, res)
constructPackageListApiRes :: constructPackageListApiRes ::
(MonadResource m, MonadReader r m, Has AppSettings r, Has PkgRepo r) => (MonadResource m, MonadReader r m, Has AppSettings r, Has PkgRepo r) =>
( Key PkgRecord ( Key PkgRecord
@@ -255,8 +280,8 @@ getPackageListR = do
{ packageResIcon = encodeBase64 icon -- pass through raw JSON Value, we have checked its correct parsing above { packageResIcon = encodeBase64 icon -- pass through raw JSON Value, we have checked its correct parsing above
, packageResManifest = unsafeFromJust . decode $ manifest , packageResManifest = unsafeFromJust . decode $ manifest
, packageResCategories = categoryName <$> pkgCategories , packageResCategories = categoryName <$> pkgCategories
, packageResInstructions = basicRender $ InstructionsR _ pkgId , packageResInstructions = basicRender $ InstructionsR V0 pkgId
, packageResLicense = basicRender $ LicenseR _ pkgId , packageResLicense = basicRender $ LicenseR V0 pkgId
, packageResVersions = pkgVersions , packageResVersions = pkgVersions
, packageResDependencies = HM.fromList deps , packageResDependencies = HM.fromList deps
} }

View File

@@ -1,8 +1,13 @@
module Handler.Package.V0.Info where module Handler.Package.V0.Info where
import Data.Aeson (ToJSON (..)) import Data.Aeson (ToJSON (..))
import Startlude (Generic, Show, Text, (.)) import Database.Esqueleto.Experimental (Entity (..), asc, from, orderBy, select, table, (^.))
import Yesod (ToContent (..), ToTypedContent (..)) import Foundation (Handler, RegistryCtx (..))
import Model (Category (..), EntityField (..))
import Settings (AppSettings (..))
import Startlude (Generic, Show, Text, pure, ($), (.), (<$>))
import Yesod (ToContent (..), ToTypedContent (..), YesodPersist (runDB), getsYesod)
import Yesod.Core.Types (JSONResponse (..))
data InfoRes = InfoRes data InfoRes = InfoRes
@@ -15,3 +20,14 @@ instance ToContent InfoRes where
toContent = toContent . toJSON toContent = toContent . toJSON
instance ToTypedContent InfoRes where instance ToTypedContent InfoRes where
toTypedContent = toTypedContent . toJSON toTypedContent = toTypedContent . toJSON
getInfoR :: Handler (JSONResponse InfoRes)
getInfoR = do
name <- getsYesod $ marketplaceName . appSettings
allCategories <- runDB $
select $ do
cats <- from $ table @Category
orderBy [asc (cats ^. CategoryPriority)]
pure cats
pure $ JSONResponse $ InfoRes name $ categoryName . entityVal <$> allCategories

View File

@@ -1,11 +1,19 @@
module Handler.Package.V0.Latest where module Handler.Package.V0.Latest where
import Data.Aeson (ToJSON (..)) import Data.Aeson (ToJSON (..), eitherDecode)
import Data.ByteString.Lazy qualified as LBS
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HM
import Data.List (lookup)
import Database.Marketplace (fetchLatestApp)
import Foundation (Handler)
import Lib.Error (S9Error (..))
import Lib.Types.AppIndex (PkgId) import Lib.Types.AppIndex (PkgId)
import Lib.Types.Emver (Version) import Lib.Types.Emver (Version)
import Startlude (Generic, Maybe, Show, (.)) import Model (Key (..), VersionRecord (..))
import Yesod (ToContent (..), ToTypedContent (..)) import Network.HTTP.Types (status400)
import Startlude (Either (..), Generic, Maybe (..), Show, catMaybes, encodeUtf8, fst, pure, snd, traverse, ($), (.), (<$>))
import Yesod (Entity (..), ToContent (..), ToTypedContent (..), YesodPersist (runDB), YesodRequest (reqGetParams), getRequest, sendResponseStatus)
newtype VersionLatestRes = VersionLatestRes (HashMap PkgId (Maybe Version)) newtype VersionLatestRes = VersionLatestRes (HashMap PkgId (Maybe Version))
@@ -15,3 +23,26 @@ instance ToContent VersionLatestRes where
toContent = toContent . toJSON toContent = toContent . toJSON
instance ToTypedContent VersionLatestRes where instance ToTypedContent VersionLatestRes where
toTypedContent = toTypedContent . toJSON toTypedContent = toTypedContent . toJSON
-- TODO refactor with conduit
getVersionLatestR :: Handler VersionLatestRes
getVersionLatestR = do
getParameters <- reqGetParams <$> getRequest
case lookup "ids" getParameters of
Nothing -> sendResponseStatus status400 (InvalidParamsE "get:ids" "<MISSING>")
Just packages -> case eitherDecode $ LBS.fromStrict $ encodeUtf8 packages of
Left _ -> sendResponseStatus status400 (InvalidParamsE "get:ids" packages)
Right p -> do
let packageList = (,Nothing) <$> p
found <- runDB $ traverse fetchLatestApp $ fst <$> packageList
pure $
VersionLatestRes $
HM.union
( HM.fromList $
( \v ->
(unPkgRecordKey . entityKey $ fst v, Just $ versionRecordNumber $ entityVal $ snd v)
)
<$> catMaybes found
)
$ HM.fromList packageList

View File

@@ -2,11 +2,16 @@
module Handler.Package.V0.License where module Handler.Package.V0.License where
import Conduit (awaitForever, (.|))
import Data.String.Interpolate.IsString (i) import Data.String.Interpolate.IsString (i)
import Foundation (Handler) import Foundation (Handler)
import Handler.Util (getVersionSpecFromQuery) import Handler.Util (getVersionSpecFromQuery, orThrow, versionPriorityFromQueryIsMin)
import Lib.Error (S9Error (..))
import Lib.PkgRepository (getBestVersion, getLicense)
import Lib.Types.AppIndex (PkgId) import Lib.Types.AppIndex (PkgId)
import Yesod (TypedContent) import Network.HTTP.Types (status400)
import Startlude (show, ($))
import Yesod (TypedContent, addHeader, respondSource, sendChunkBS, sendResponseStatus, typePlain)
getLicenseR :: PkgId -> Handler TypedContent getLicenseR :: PkgId -> Handler TypedContent

View File

@@ -1 +1,27 @@
{-# LANGUAGE QuasiQuotes #-}
module Handler.Package.V0.Manifest where module Handler.Package.V0.Manifest where
import Conduit (awaitForever, (.|))
import Data.String.Interpolate.IsString (i)
import Foundation (Handler)
import Handler.Util (addPackageHeader, getVersionSpecFromQuery, orThrow, versionPriorityFromQueryIsMin)
import Lib.Error (S9Error (..))
import Lib.PkgRepository (getBestVersion, getManifest)
import Lib.Types.AppIndex (PkgId)
import Network.HTTP.Types (status404)
import Startlude (show, ($))
import Yesod (TypedContent, addHeader, respondSource, sendChunkBS, sendResponseStatus, typeJson)
getAppManifestR :: PkgId -> Handler TypedContent
getAppManifestR pkg = do
versionSpec <- getVersionSpecFromQuery
preferMin <- versionPriorityFromQueryIsMin
version <-
getBestVersion pkg versionSpec preferMin
`orThrow` sendResponseStatus status404 (NotFoundE [i|#{pkg} satisfying #{versionSpec}|])
addPackageHeader pkg version
(len, src) <- getManifest pkg version
addHeader "Content-Length" (show len)
respondSource typeJson $ src .| awaitForever sendChunkBS

View File

@@ -2,12 +2,16 @@
module Handler.Package.V0.ReleaseNotes where module Handler.Package.V0.ReleaseNotes where
import Data.Aeson (ToJSON (..), Value (..), object, (.=)) import Data.Aeson (ToJSON (..))
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HM import Data.HashMap.Strict qualified as HM
import Database.Marketplace (fetchAllAppVersions)
import Foundation (Handler, RegistryCtx (..))
import Lib.Types.AppIndex (PkgId)
import Lib.Types.Emver (Version) import Lib.Types.Emver (Version)
import Startlude (Eq, Show, Text, (.)) import Model (VersionRecord (..))
import Yesod (ToContent (..), ToTypedContent (..)) import Startlude (Down (..), Eq, Show, Text, fst, pure, sortOn, ($), (&&&), (.), (<$>))
import Yesod (ToContent (..), ToTypedContent (..), YesodPersist (runDB), getYesod)
newtype ReleaseNotes = ReleaseNotes {unReleaseNotes :: HashMap Version Text} newtype ReleaseNotes = ReleaseNotes {unReleaseNotes :: HashMap Version Text}
@@ -18,3 +22,18 @@ instance ToContent ReleaseNotes where
toContent = toContent . toJSON toContent = toContent . toJSON
instance ToTypedContent ReleaseNotes where instance ToTypedContent ReleaseNotes where
toTypedContent = toTypedContent . toJSON toTypedContent = toTypedContent . toJSON
getReleaseNotesR :: PkgId -> Handler ReleaseNotes
getReleaseNotesR pkg = do
appConnPool <- appConnPool <$> getYesod
versionRecords <- runDB $ fetchAllAppVersions appConnPool pkg
pure $ constructReleaseNotesApiRes versionRecords
where
constructReleaseNotesApiRes :: [VersionRecord] -> ReleaseNotes
constructReleaseNotesApiRes vers = do
ReleaseNotes $
HM.fromList $
sortOn (Down . fst) $
(versionRecordNumber &&& versionRecordReleaseNotes)
<$> vers

View File

@@ -1 +1,56 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Handler.Package.V0.S9PK where module Handler.Package.V0.S9PK where
import Data.String.Interpolate.IsString (i)
import Data.Text qualified as T
import Database.Queries (createMetric, fetchApp, fetchAppVersion)
import Foundation (Handler)
import GHC.Show (show)
import Handler.Util (addPackageHeader, getVersionSpecFromQuery, orThrow, versionPriorityFromQueryIsMin)
import Lib.Error (S9Error (..))
import Lib.PkgRepository (getBestVersion, getPackage)
import Lib.Types.AppIndex (PkgId (..), S9PK)
import Lib.Types.Emver (Version (..))
import Network.HTTP.Types (status404)
import Startlude (Maybe (..), pure, void, ($), (.), (>>=))
import System.FilePath (takeBaseName)
import Yesod (Content (..), TypedContent, YesodPersist (runDB), notFound, respond, sendResponseStatus, typeOctet)
import Yesod.Core (logError)
getAppR :: S9PK -> Handler TypedContent
getAppR file = do
let pkg = PkgId . T.pack $ takeBaseName (show file)
versionSpec <- getVersionSpecFromQuery
preferMin <- versionPriorityFromQueryIsMin
version <-
getBestVersion pkg versionSpec preferMin
`orThrow` sendResponseStatus status404 (NotFoundE [i|#{pkg} satisfying #{versionSpec}|])
addPackageHeader pkg version
void $ recordMetrics pkg version
pkgPath <-
getPackage pkg version >>= \case
Nothing -> sendResponseStatus status404 (NotFoundE [i|#{pkg}@#{version}|])
Just a -> pure a
respond typeOctet $ ContentFile pkgPath Nothing
recordMetrics :: PkgId -> Version -> Handler ()
recordMetrics pkg appVersion = do
sa <- runDB $ fetchApp pkg
case sa of
Nothing ->
do
$logError [i|#{pkg} not found in database|]
notFound
Just _ -> do
existingVersion <- runDB $ fetchAppVersion pkg appVersion
case existingVersion of
Nothing ->
do
$logError [i|#{pkg}@#{appVersion} not found in database|]
notFound
Just _ -> runDB $ createMetric pkg appVersion

View File

@@ -1 +1,46 @@
{-# LANGUAGE QuasiQuotes #-}
module Handler.Package.V0.Version where module Handler.Package.V0.Version where
import Data.Aeson (ToJSON, object, (.=))
import Data.String.Interpolate.IsString (i)
import Foundation (Handler)
import Handler.Util (
getVersionSpecFromQuery,
orThrow,
versionPriorityFromQueryIsMin,
)
import Lib.Error (S9Error (..))
import Lib.PkgRepository (getBestVersion)
import Lib.Types.AppIndex (PkgId)
import Lib.Types.Emver (Version (..))
import Network.HTTP.Types (status404)
import Startlude (Eq, Maybe, Show, (.), (<$>))
import Yesod (ToContent (..), ToTypedContent, sendResponseStatus)
import Yesod.Core (ToJSON (..), ToTypedContent (..))
newtype AppVersionRes = AppVersionRes
{ appVersionVersion :: Version
}
deriving (Eq, Show)
instance ToJSON AppVersionRes where
toJSON AppVersionRes{appVersionVersion} = object ["version" .= appVersionVersion]
instance ToContent AppVersionRes where
toContent = toContent . toJSON
instance ToTypedContent AppVersionRes where
toTypedContent = toTypedContent . toJSON
instance ToContent (Maybe AppVersionRes) where
toContent = toContent . toJSON
instance ToTypedContent (Maybe AppVersionRes) where
toTypedContent = toTypedContent . toJSON
getPkgVersionR :: PkgId -> Handler AppVersionRes
getPkgVersionR pkg = do
spec <- getVersionSpecFromQuery
preferMin <- versionPriorityFromQueryIsMin
AppVersionRes <$> getBestVersion pkg spec preferMin
`orThrow` sendResponseStatus
status404
(NotFoundE [i|Version for #{pkg} satisfying #{spec}|])

View File

@@ -1,24 +1,31 @@
module Handler.Types.Api where module Handler.Types.Api where
import GHC.Read ( Read(..) ) import GHC.Read (Read (..))
import GHC.Show ( show ) import GHC.Show (show)
import Startlude ( Eq import Startlude (
, Maybe(..) Eq,
, Ord Maybe (..),
, Read Ord,
, Show Show,
) )
import Yesod ( PathPiece(..) ) import Yesod (PathPiece (..))
data ApiVersion data ApiVersion
= V0 = V0
| V1 deriving (Eq, Ord) | V1
deriving (Eq, Ord)
instance Show ApiVersion where instance Show ApiVersion where
show V0 = "v0" show V0 = "v0"
show V1 = "v1" show V1 = "v1"
instance Read ApiVersion where instance Read ApiVersion where
readsPrec = _ readsPrec _ "v0" = [(V0, "")]
readsPrec _ "v1" = [(V1, "")]
readsPrec _ _ = []
instance PathPiece ApiVersion where instance PathPiece ApiVersion where
@@ -26,4 +33,4 @@ instance PathPiece ApiVersion where
toPathPiece V1 = "v1" toPathPiece V1 = "v1"
fromPathPiece "v0" = Just V0 fromPathPiece "v0" = Just V0
fromPathPiece "v1" = Just V1 fromPathPiece "v1" = Just V1
fromPathPiece _ = Nothing fromPathPiece _ = Nothing

View File

@@ -1,37 +0,0 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Use newtype instead of data" #-}
module Handler.Types.Status where
import Startlude ( (.)
, Eq
, Maybe
, Show
)
import Data.Aeson ( KeyValue((.=))
, ToJSON(toJSON)
, object
)
import Yesod.Core.Content ( ToContent(..)
, ToTypedContent(..)
)
import Lib.Types.Emver ( Version )
import Orphans.Emver ( )
newtype AppVersionRes = AppVersionRes
{ appVersionVersion :: Version
}
deriving (Eq, Show)
instance ToJSON AppVersionRes where
toJSON AppVersionRes { appVersionVersion } = object ["version" .= appVersionVersion]
instance ToContent AppVersionRes where
toContent = toContent . toJSON
instance ToTypedContent AppVersionRes where
toTypedContent = toTypedContent . toJSON
instance ToContent (Maybe AppVersionRes) where
toContent = toContent . toJSON
instance ToTypedContent (Maybe AppVersionRes) where
toTypedContent = toTypedContent . toJSON

View File

@@ -1,16 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Handler.Util where module Handler.Util where
import Control.Monad.Reader.Has ( import Control.Monad.Reader.Has (
Has, Has,
MonadReader, MonadReader,
) )
import Data.Attoparsec.Text (Parser, parseOnly)
import Data.String.Interpolate.IsString (i)
import Data.Text qualified as T import Data.Text qualified as T
import Data.Text.Lazy qualified as TL import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TB import Data.Text.Lazy.Builder qualified as TB
import Lib.PkgRepository (PkgRepo, getHash) import Lib.PkgRepository (PkgRepo, getHash)
import Lib.Types.AppIndex (PkgId) import Lib.Types.AppIndex (PkgId)
import Lib.Types.Emver ( import Lib.Types.Emver (
Version (Version), Version,
VersionRange, VersionRange,
) )
import Network.HTTP.Types ( import Network.HTTP.Types (
@@ -19,29 +23,35 @@ import Network.HTTP.Types (
) )
import Startlude ( import Startlude (
Bool (..), Bool (..),
Either (..),
Foldable (foldMap), Foldable (foldMap),
Maybe (..), Maybe (..),
Monoid (..),
Semigroup ((<>)), Semigroup ((<>)),
Text, Text,
decodeUtf8,
fromMaybe, fromMaybe,
fst,
isSpace, isSpace,
not, not,
pure, pure,
readMaybe, readMaybe,
($),
(.), (.),
(<$>), (<$>),
(>>=),, ($) (>>=),
) )
import UnliftIO (MonadUnliftIO) import UnliftIO (MonadUnliftIO)
import Yesod ( import Yesod (
MonadHandler, MonadHandler,
RenderRoute (Route), RenderRoute (..),
TypedContent (..), TypedContent (..),
lookupGetParam, lookupGetParam,
sendResponseStatus, sendResponseStatus,
toContent, toContent,
typePlain, typePlain,
) )
import Yesod.Core (addHeader)
orThrow :: MonadHandler m => m (Maybe a) -> m a -> m a orThrow :: MonadHandler m => m (Maybe a) -> m a -> m a
@@ -80,4 +90,14 @@ addPackageHeader pkg version = do
basicRender :: RenderRoute a => Route a -> Text basicRender :: RenderRoute a => Route a -> Text
basicRender = TL.toStrict . TB.toLazyText . foldMap (mappend (TB.singleton '/') . TB.fromText) . fst . renderRoute basicRender = TL.toStrict . TB.toLazyText . foldMap (mappend (TB.singleton '/') . TB.fromText) . fst . renderRoute
queryParamAs :: MonadHandler m => Text -> Parser a -> m (Maybe a)
queryParamAs k p =
lookupGetParam k >>= \case
Nothing -> pure Nothing
Just x -> case parseOnly p x of
Left e ->
sendResponseText status400 [i|Invalid Request! The query parameter '#{k}' failed to parse: #{e}|]
Right a -> pure (Just a)

View File

@@ -1,32 +0,0 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
module Handler.Version where
import Startlude ( (<$>) )
import Yesod.Core ( sendResponseStatus )
import Data.String.Interpolate.IsString
( i )
import Foundation ( Handler )
import Handler.Types.Status ( AppVersionRes(AppVersionRes) )
import Handler.Util ( orThrow )
import Lib.Error ( S9Error(NotFoundE) )
import Lib.PkgRepository ( getBestVersion )
import Lib.Types.AppIndex ( PkgId )
import Network.HTTP.Types.Status ( status404 )
import Util.Shared ( getVersionSpecFromQuery
, versionPriorityFromQueryIsMin
)
getPkgVersionR :: PkgId -> Handler AppVersionRes
getPkgVersionR pkg = do
spec <- getVersionSpecFromQuery
preferMin <- versionPriorityFromQueryIsMin
AppVersionRes <$> getBestVersion pkg spec preferMin `orThrow` sendResponseStatus
status404
(NotFoundE [i|Version for #{pkg} satisfying #{spec}|])

View File

@@ -3,52 +3,27 @@
module Lib.Conduit where module Lib.Conduit where
import Conduit (ConduitT, awaitForever, yield)
import Control.Monad.Logger (logInfo) import Control.Monad.Logger (logInfo)
import Control.Monad.Logger.CallStack (MonadLogger) import Control.Monad.Logger.CallStack (MonadLogger)
import Data.List (lookup, null) import Data.List.NonEmpty qualified as NE
import Data.String.Interpolate.IsString (i) import Data.String.Interpolate.IsString (i)
import Database.Marketplace (PackageDependencyMetadata (..), PackageMetadata (..)) import Database.Marketplace (PackageDependencyMetadata (..))
import Database.Persist (Entity (..)) import Database.Persist (Entity (..))
import Lib.Ord (maximumOn)
import Lib.Types.AppIndex (PkgId) import Lib.Types.AppIndex (PkgId)
import Lib.Types.Emver (Version, VersionRange (..), satisfies) import Lib.Types.Emver (Version, VersionRange (..), satisfies, (<||))
import Model (Category, Key, PkgDependency (..), PkgRecord (PkgRecord), VersionRecord (..)) import Model (Key (..), PkgDependency (..), PkgRecord (..), VersionRecord (..))
import Startlude (Bool, Down (..), Maybe (..), Monad, Text, filter, fmap, fromMaybe, headMay, sortOn, unless, ($), (.)) import Startlude (Bool, Down (..), Maybe (..), NonEmpty, Text, filter, headMay, pure, sortOn, ($), (.), (<$>))
filterPkgOsCompatible :: Monad m => (Version -> Bool) -> ConduitT PackageMetadata PackageMetadata m () selectLatestVersionFromSpec ::
filterPkgOsCompatible p = (PkgId -> VersionRange) ->
awaitForever $ NonEmpty VersionRecord ->
\PackageMetadata{packageMetadataPkgId = pkg, packageMetadataPkgVersionRecords = versions, packageMetadataPkgCategories = cats, packageMetadataPkgVersion = requestedVersion} -> Maybe VersionRecord
do selectLatestVersionFromSpec pkgRanges vs =
let compatible = filter (p . versionRecordOsVersion . entityVal) versions let pkgId = NE.head $ versionRecordPkgId <$> vs
unless (null compatible) $ spec = pkgRanges (unPkgRecordKey pkgId)
yield in headMay . sortOn (Down . versionRecordNumber) $ NE.filter ((`satisfies` spec) . versionRecordNumber) vs
PackageMetadata
{ packageMetadataPkgId = pkg
, packageMetadataPkgVersionRecords = compatible
, packageMetadataPkgCategories = cats
, packageMetadataPkgVersion = requestedVersion
}
filterLatestVersionFromSpec ::
(Monad m, MonadLogger m) =>
[(PkgId, VersionRange)] ->
ConduitT (PkgId, [Entity VersionRecord], [Entity Category]) PackageMetadata m ()
filterLatestVersionFromSpec versionMap = awaitForever $ \(pkgId, vs, cats) -> do
-- if no packages are specified, the VersionRange is implicitly `*`
let spec = fromMaybe Any $ lookup pkgId versionMap
case headMay . sortOn Down $ filter (`satisfies` spec) $ fmap (versionRecordNumber . entityVal) vs of
Nothing -> $logInfo [i|No version for #{pkgId} satisfying #{spec}|]
Just v ->
yield $
PackageMetadata
{ packageMetadataPkgId = pkgId
, packageMetadataPkgVersionRecords = vs
, packageMetadataPkgCategories = cats
, packageMetadataPkgVersion = v
}
filterDependencyOsCompatible :: (Version -> Bool) -> PackageDependencyMetadata -> PackageDependencyMetadata filterDependencyOsCompatible :: (Version -> Bool) -> PackageDependencyMetadata -> PackageDependencyMetadata

View File

@@ -1,47 +0,0 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Lib.Registry where
import Startlude ( ($)
, (.)
, ConvertText(toS)
, Eq((==))
, KnownSymbol
, Proxy(Proxy)
, Read
, Show
, String
, Symbol
, readMaybe
, show
, symbolVal
)
import qualified GHC.Read ( Read(..) )
import qualified GHC.Show ( Show(..) )
import System.FilePath ( (<.>)
, splitExtension
)
import Yesod.Core ( PathPiece(..) )
newtype Extension (a :: Symbol) = Extension String deriving (Eq)
type S9PK = Extension "s9pk"
extension :: KnownSymbol a => Extension a -> String
extension = symbolVal
instance KnownSymbol a => Show (Extension a) where
show e@(Extension file) = file <.> extension e
instance KnownSymbol a => Read (Extension a) where
readsPrec _ s = case symbolVal $ Proxy @a of
"" -> [(Extension s, "")]
other -> [ (Extension file, "") | ext' == "" <.> other ]
where (file, ext') = splitExtension s
instance KnownSymbol a => PathPiece (Extension a) where
fromPathPiece = readMaybe . toS
toPathPiece = show

View File

@@ -1,46 +1,57 @@
{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-} {-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
module Lib.Types.AppIndex where module Lib.Types.AppIndex where
import Startlude import Startlude
-- NOTE: leave eitherDecode for inline test evaluation below -- NOTE: leave eitherDecode for inline test evaluation below
import Control.Monad ( fail ) import Control.Monad (fail)
import Data.Aeson ( (.:) import Data.Aeson (
, (.:?) FromJSON (..),
, FromJSON(..) FromJSONKey (..),
, FromJSONKey(..) ToJSON (..),
, ToJSON(..) ToJSONKey (..),
, ToJSONKey(..) withObject,
, withObject (.:),
) (.:?),
import qualified Data.ByteString.Lazy as BS )
import Data.Functor.Contravariant ( contramap ) import Data.ByteString.Lazy qualified as BS
import qualified Data.HashMap.Strict as HM import Data.Functor.Contravariant (contramap)
import Data.String.Interpolate.IsString import Data.HashMap.Strict qualified as HM
( i ) import Data.String.Interpolate.IsString (
import qualified Data.Text as T i,
import Database.Persist ( PersistField(..) )
, PersistValue(PersistText) import Data.Text qualified as T
, SqlType(..) import Database.Persist (
) PersistField (..),
import Database.Persist.Sql ( PersistFieldSql(sqlType) ) PersistValue (PersistText),
import GHC.Read ( Read(readsPrec) ) SqlType (..),
import Lib.Types.Emver ( Version )
, VersionRange import Database.Persist.Sql (PersistFieldSql (sqlType))
) import GHC.Read (Read (readsPrec))
import Orphans.Emver ( ) import Lib.Types.Emver (
import qualified Protolude.Base as P Version,
( Show(..) ) VersionRange,
import Web.HttpApiData ( FromHttpApiData )
, ToHttpApiData import Orphans.Emver ()
) import Protolude.Base qualified as P (
import Yesod ( PathPiece(..) ) Show (..),
newtype PkgId = PkgId { unPkgId :: Text } )
import System.FilePath (splitExtension, (<.>))
import Web.HttpApiData (
FromHttpApiData,
ToHttpApiData,
)
import Yesod (PathPiece (..))
newtype PkgId = PkgId {unPkgId :: Text}
deriving stock (Eq, Ord) deriving stock (Eq, Ord)
deriving newtype (FromHttpApiData, ToHttpApiData) deriving newtype (FromHttpApiData, ToHttpApiData)
instance IsString PkgId where instance IsString PkgId where
@@ -62,72 +73,100 @@ instance ToJSONKey PkgId where
instance PersistField PkgId where instance PersistField PkgId where
toPersistValue = PersistText . show toPersistValue = PersistText . show
fromPersistValue (PersistText t) = Right . PkgId $ toS t fromPersistValue (PersistText t) = Right . PkgId $ toS t
fromPersistValue other = Left [i|Invalid AppId: #{other}|] fromPersistValue other = Left [i|Invalid AppId: #{other}|]
instance PersistFieldSql PkgId where instance PersistFieldSql PkgId where
sqlType _ = SqlString sqlType _ = SqlString
instance PathPiece PkgId where instance PathPiece PkgId where
fromPathPiece = fmap PkgId . fromPathPiece fromPathPiece = fmap PkgId . fromPathPiece
toPathPiece = unPkgId toPathPiece = unPkgId
data VersionInfo = VersionInfo data VersionInfo = VersionInfo
{ versionInfoVersion :: !Version { versionInfoVersion :: !Version
, versionInfoReleaseNotes :: !Text , versionInfoReleaseNotes :: !Text
, versionInfoDependencies :: !(HM.HashMap PkgId VersionRange) , versionInfoDependencies :: !(HM.HashMap PkgId VersionRange)
, versionInfoOsVersion :: !Version , versionInfoOsVersion :: !Version
, versionInfoInstallAlert :: !(Maybe Text) , versionInfoInstallAlert :: !(Maybe Text)
} }
deriving (Eq, Show) deriving (Eq, Show)
data PackageDependency = PackageDependency data PackageDependency = PackageDependency
{ packageDependencyOptional :: !(Maybe Text) { packageDependencyOptional :: !(Maybe Text)
, packageDependencyVersion :: !VersionRange , packageDependencyVersion :: !VersionRange
, packageDependencyDescription :: !(Maybe Text) , packageDependencyDescription :: !(Maybe Text)
} }
deriving Show deriving (Show)
instance FromJSON PackageDependency where instance FromJSON PackageDependency where
parseJSON = withObject "service dependency info" $ \o -> do parseJSON = withObject "service dependency info" $ \o -> do
packageDependencyOptional <- o .:? "optional" packageDependencyOptional <- o .:? "optional"
packageDependencyVersion <- o .: "version" packageDependencyVersion <- o .: "version"
packageDependencyDescription <- o .:? "description" packageDependencyDescription <- o .:? "description"
pure PackageDependency { .. } pure PackageDependency{..}
data ServiceAlert = INSTALL | UNINSTALL | RESTORE | START | STOP data ServiceAlert = INSTALL | UNINSTALL | RESTORE | START | STOP
deriving (Show, Eq, Generic, Hashable, Read) deriving (Show, Eq, Generic, Hashable, Read)
data PackageManifest = PackageManifest data PackageManifest = PackageManifest
{ packageManifestId :: !PkgId { packageManifestId :: !PkgId
, packageManifestTitle :: !Text , packageManifestTitle :: !Text
, packageManifestVersion :: !Version , packageManifestVersion :: !Version
, packageManifestDescriptionLong :: !Text , packageManifestDescriptionLong :: !Text
, packageManifestDescriptionShort :: !Text , packageManifestDescriptionShort :: !Text
, packageManifestReleaseNotes :: !Text , packageManifestReleaseNotes :: !Text
, packageManifestIcon :: !(Maybe Text) , packageManifestIcon :: !(Maybe Text)
, packageManifestAlerts :: !(HM.HashMap ServiceAlert (Maybe Text)) , packageManifestAlerts :: !(HM.HashMap ServiceAlert (Maybe Text))
, packageManifestDependencies :: !(HM.HashMap PkgId PackageDependency) , packageManifestDependencies :: !(HM.HashMap PkgId PackageDependency)
, packageManifestEosVersion :: !Version , packageManifestEosVersion :: !Version
} }
deriving Show deriving (Show)
instance FromJSON PackageManifest where instance FromJSON PackageManifest where
parseJSON = withObject "service manifest" $ \o -> do parseJSON = withObject "service manifest" $ \o -> do
packageManifestId <- o .: "id" packageManifestId <- o .: "id"
packageManifestTitle <- o .: "title" packageManifestTitle <- o .: "title"
packageManifestVersion <- o .: "version" packageManifestVersion <- o .: "version"
packageManifestDescriptionLong <- o .: "description" >>= (.: "long") packageManifestDescriptionLong <- o .: "description" >>= (.: "long")
packageManifestDescriptionShort <- o .: "description" >>= (.: "short") packageManifestDescriptionShort <- o .: "description" >>= (.: "short")
packageManifestIcon <- o .: "assets" >>= (.: "icon") packageManifestIcon <- o .: "assets" >>= (.: "icon")
packageManifestReleaseNotes <- o .: "release-notes" packageManifestReleaseNotes <- o .: "release-notes"
alerts <- o .: "alerts" alerts <- o .: "alerts"
a <- for (HM.toList alerts) $ \(key, value) -> do a <- for (HM.toList alerts) $ \(key, value) -> do
alertType <- case readMaybe $ T.toUpper key of alertType <- case readMaybe $ T.toUpper key of
Nothing -> fail "could not parse alert key as ServiceAlert" Nothing -> fail "could not parse alert key as ServiceAlert"
Just t -> pure t Just t -> pure t
alertDesc <- parseJSON value alertDesc <- parseJSON value
pure (alertType, alertDesc) pure (alertType, alertDesc)
let packageManifestAlerts = HM.fromList a let packageManifestAlerts = HM.fromList a
packageManifestDependencies <- o .: "dependencies" packageManifestDependencies <- o .: "dependencies"
packageManifestEosVersion <- o .: "eos-version" packageManifestEosVersion <- o .: "eos-version"
pure PackageManifest { .. } pure PackageManifest{..}
newtype Extension (a :: Symbol) = Extension String deriving (Eq)
type S9PK = Extension "s9pk"
extension :: KnownSymbol a => Extension a -> String
extension = symbolVal
instance KnownSymbol a => Show (Extension a) where
show e@(Extension file) = file <.> extension e
instance KnownSymbol a => Read (Extension a) where
readsPrec _ s = case symbolVal $ Proxy @a of
"" -> [(Extension s, "")]
other -> [(Extension file, "") | ext' == "" <.> other]
where
(file, ext') = splitExtension s
instance KnownSymbol a => PathPiece (Extension a) where
fromPathPiece = readMaybe . toS
toPathPiece = show
-- >>> eitherDecode testManifest :: Either String PackageManifest -- >>> eitherDecode testManifest :: Either String PackageManifest
testManifest :: BS.ByteString testManifest :: BS.ByteString
testManifest = [i|{ testManifest =
[i|{
"id": "embassy-pages", "id": "embassy-pages",
"title": "Embassy Pages", "title": "Embassy Pages",
"version": "0.1.3", "version": "0.1.3",