Feature/encode icons (#91)

* base64 encode icon in package response

* adds icons to dependencies

* swap arguments for loadIcon

* remove redundant constraint

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
This commit is contained in:
Lucy C
2022-02-08 13:11:05 -07:00
committed by Keagan McClelland
parent f0b958e6df
commit 14d7adc0c9
3 changed files with 89 additions and 54 deletions

View File

@@ -14,6 +14,7 @@ default-extensions:
dependencies: dependencies:
- base >=4.12 && <5 - base >=4.12 && <5
- base64
- aeson - aeson
- ansi-terminal - ansi-terminal
- attoparsec - attoparsec

View File

@@ -25,7 +25,9 @@ import Conduit ( (.|)
, sourceFile , sourceFile
, takeC , takeC
) )
import Control.Monad.Logger ( MonadLogger, logWarn ) import Control.Monad.Logger ( MonadLogger
, logWarn
)
import Control.Monad.Reader.Has ( Has import Control.Monad.Reader.Has ( Has
, ask , ask
) )
@@ -36,10 +38,12 @@ import Data.Aeson ( decode
, eitherDecodeStrict , eitherDecodeStrict
) )
import qualified Data.Attoparsec.Text as Atto import qualified Data.Attoparsec.Text as Atto
import Data.ByteArray.Encoding ( Base(Base16)
import Data.ByteArray.Encoding ( Base(..)
, convertToBase , convertToBase
) )
import qualified Data.ByteString.Lazy as BS import Data.ByteString.Base64
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Conduit.List as CL import qualified Data.Conduit.List as CL
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import Data.List ( head import Data.List ( head
@@ -61,31 +65,36 @@ import Database.Esqueleto.Experimental
, select , select
, table , table
) )
import Database.Marketplace ( getPkgData import Database.Marketplace ( fetchAllAppVersions
, searchServices
, zipVersions
, fetchAllAppVersions
, fetchLatestApp , fetchLatestApp
, getPkgDependencyData, zipDependencyVersions, zipCategories , getPkgData
, getPkgDependencyData
, searchServices
, zipCategories
, zipDependencyVersions
, zipVersions
) )
import Database.Persist ( PersistUniqueRead(getBy) import Database.Persist ( PersistUniqueRead(getBy)
, insertUnique , insertUnique
) )
import Foundation ( Handler import Foundation ( Handler
, RegistryCtx(appSettings, appConnPool), Route (IconsR, InstructionsR, LicenseR) , RegistryCtx(appConnPool, appSettings)
, Route(InstructionsR, LicenseR)
) )
import Handler.Types.Marketplace import Handler.Types.Marketplace
import Lib.Error ( S9Error(..) import Lib.Error ( S9Error(..) )
) import Lib.PkgRepository ( PkgRepo
import Lib.PkgRepository ( getManifest ) , getIcon
import Lib.Types.AppIndex ( PkgId(PkgId) , getManifest
) )
import Lib.Types.AppIndex ( PkgId(PkgId) )
import Lib.Types.AppIndex ( ) import Lib.Types.AppIndex ( )
import Lib.Types.Category ( CategoryTitle(..) ) import Lib.Types.Category ( CategoryTitle(..) )
import Lib.Types.Emver ( Version import Lib.Types.Emver ( Version
, VersionRange
, parseRange , parseRange
, parseVersion , parseVersion
, satisfies, VersionRange , satisfies
) )
import Model ( Category(..) import Model ( Category(..)
, EntityField(..) , EntityField(..)
@@ -107,24 +116,25 @@ import UnliftIO.Async ( concurrently
, mapConcurrently , mapConcurrently
) )
import UnliftIO.Directory ( listDirectory ) import UnliftIO.Directory ( listDirectory )
import Util.Shared ( getVersionSpecFromQuery import Util.Shared ( filterDependencyBestVersion
, filterDependencyOsCompatible
, filterLatestVersionFromSpec , filterLatestVersionFromSpec
, filterPkgOsCompatible , filterPkgOsCompatible
, filterDependencyOsCompatible , getVersionSpecFromQuery
, filterDependencyBestVersion
) )
import Yesod.Core ( MonadResource import Yesod.Core ( MonadResource
, RenderRoute(renderRoute)
, TypedContent , TypedContent
, YesodRequest(..) , YesodRequest(..)
, addHeader , addHeader
, getRequest , getRequest
, getYesod
, getsYesod , getsYesod
, lookupGetParam , lookupGetParam
, respondSource , respondSource
, sendChunkBS , sendChunkBS
, sendResponseStatus , sendResponseStatus
, typeOctet , typeOctet
, getYesod, RenderRoute (renderRoute)
) )
import Yesod.Persist ( YesodDB ) import Yesod.Persist ( YesodDB )
import Yesod.Persist.Core ( YesodPersist(runDB) ) import Yesod.Persist.Core ( YesodPersist(runDB) )
@@ -205,7 +215,7 @@ getVersionLatestR = do
getParameters <- reqGetParams <$> getRequest getParameters <- reqGetParams <$> getRequest
case lookup "ids" getParameters of case lookup "ids" getParameters of
Nothing -> sendResponseStatus status400 (InvalidParamsE "get:ids" "<MISSING>") Nothing -> sendResponseStatus status400 (InvalidParamsE "get:ids" "<MISSING>")
Just packages -> case eitherDecode $ BS.fromStrict $ encodeUtf8 packages of Just packages -> case eitherDecode $ LBS.fromStrict $ encodeUtf8 packages of
Left _ -> sendResponseStatus status400 (InvalidParamsE "get:ids" packages) Left _ -> sendResponseStatus status400 (InvalidParamsE "get:ids" packages)
Right p -> do Right p -> do
let packageList = (, Nothing) <$> p let packageList = (, Nothing) <$> p
@@ -315,8 +325,20 @@ getPackageListR = do
$logWarn (show e) $logWarn (show e)
sendResponseStatus status400 e sendResponseStatus status400 e
Right v -> pure $ Just v Right v -> pure $ Just v
getPackageDependencies :: (MonadIO m, MonadLogger m) => (Version -> Bool) -> PackageMetadata -> ReaderT SqlBackend m (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) getPackageDependencies :: (MonadIO m, MonadLogger m)
getPackageDependencies osPredicate PackageMetadata { packageMetadataPkgRecord = pkg, packageMetadataPkgVersionRecords = pkgVersions, packageMetadataPkgCategories = pkgCategories, packageMetadataPkgVersion = pkgVersion} = do => (Version -> Bool)
-> PackageMetadata
-> ReaderT
SqlBackend
m
( Key PkgRecord
, [Category]
, [Version]
, Version
, [(Key PkgRecord, Text, Version)]
)
getPackageDependencies osPredicate PackageMetadata { packageMetadataPkgRecord = pkg, packageMetadataPkgVersionRecords = pkgVersions, packageMetadataPkgCategories = pkgCategories, packageMetadataPkgVersion = pkgVersion }
= do
let pkgId = entityKey pkg let pkgId = entityKey pkg
let pkgVersions' = versionRecordNumber . entityVal <$> pkgVersions let pkgVersions' = versionRecordNumber . entityVal <$> pkgVersions
let pkgCategories' = entityVal <$> pkgCategories let pkgCategories' = entityVal <$> pkgCategories
@@ -325,32 +347,44 @@ getPackageListR = do
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', pkgVersions', pkgVersion, res)
constructPackageListApiRes :: (MonadResource m, MonadReader r m, Has AppSettings r) => (Key PkgRecord, [Category], [Version], Version, [(Key PkgRecord, Text, Version)]) -> m PackageRes constructPackageListApiRes :: (MonadResource m, MonadReader r m, Has AppSettings r, Has PkgRepo r)
=> ( Key PkgRecord
, [Category]
, [Version]
, Version
, [(Key PkgRecord, Text, Version)]
)
-> m PackageRes
constructPackageListApiRes (pkgKey, pkgCategories, pkgVersions, pkgVersion, dependencies) = do constructPackageListApiRes (pkgKey, pkgCategories, pkgVersions, pkgVersion, dependencies) = do
settings <- ask @_ @_ @AppSettings settings <- ask @_ @_ @AppSettings
let pkgId = unPkgRecordKey pkgKey let pkgId = unPkgRecordKey pkgKey
manifest <- flip runReaderT settings $ (snd <$> getManifest pkgId pkgVersion) >>= \bs -> manifest <- flip runReaderT settings $ (snd <$> getManifest pkgId pkgVersion) >>= \bs ->
runConduit $ bs .| CL.foldMap BS.fromStrict runConduit $ bs .| CL.foldMap LBS.fromStrict
pure $ PackageRes icon <- loadIcon pkgId pkgVersion
{ packageResIcon = basicRender $ IconsR pkgId deps <- constructDependenciesApiRes dependencies
pure $ PackageRes { packageResIcon = encodeBase64 icon
-- pass through raw JSON Value, we have checked its correct parsing above -- 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 pkgId
, packageResLicense = basicRender $ LicenseR pkgId , packageResLicense = basicRender $ LicenseR pkgId
, packageResVersions = pkgVersions , packageResVersions = pkgVersions
, packageResDependencies = HM.fromList $ constructDependenciesApiRes dependencies , packageResDependencies = HM.fromList deps
} }
constructDependenciesApiRes :: [(Key PkgRecord, Text, Version)] constructDependenciesApiRes :: (MonadResource m, MonadReader r m, Has PkgRepo r)
-> [(PkgId, DependencyRes)] => [(Key PkgRecord, Text, Version)]
constructDependenciesApiRes deps = fmap (\(depKey, depTitle, depVersion) -> do -> m [(PkgId, DependencyRes)]
constructDependenciesApiRes deps = traverse
(\(depKey, depTitle, depVersion) -> do
let depId = unPkgRecordKey depKey let depId = unPkgRecordKey depKey
(depId, DependencyRes { dependencyResTitle = depTitle, dependencyResIcon = (basicRender $ IconsR depId) <> [i|?spec==#{depVersion}|]})) deps icon <- loadIcon depId depVersion
pure (depId, DependencyRes { dependencyResTitle = depTitle, dependencyResIcon = encodeBase64 icon })
)
deps
loadIcon :: (MonadResource m, MonadReader r m, Has PkgRepo r) => PkgId -> Version -> m ByteString
loadIcon pkg version = do
(_, _, src) <- getIcon pkg version
runConduit $ src .| CL.foldMap id
basicRender :: RenderRoute a => Route a -> Text basicRender :: RenderRoute a => Route a -> Text
basicRender = TL.toStrict basicRender = TL.toStrict . TB.toLazyText . fold . fmap (mappend (TB.singleton '/') . TB.fromText) . fst . renderRoute
. TB.toLazyText
. fold
. fmap (mappend (TB.singleton '/') . TB.fromText)
. fst
. renderRoute

View File

@@ -69,7 +69,7 @@ instance FromJSON PackageRes where
pure PackageRes { .. } pure PackageRes { .. }
data DependencyRes = DependencyRes data DependencyRes = DependencyRes
{ dependencyResTitle :: Text -- TODO switch to `Text` to display actual title in Marketplace. Confirm with FE that this will not break loading. Perhaps return title and id? { dependencyResTitle :: Text -- TODO switch to `Text` to display actual title in Marketplace. Confirm with FE that this will not break loading. Perhaps return title and id?
, dependencyResIcon :: URL , dependencyResIcon :: Text
} }
deriving (Eq, Show) deriving (Eq, Show)
instance ToJSON DependencyRes where instance ToJSON DependencyRes where