diff --git a/hackage-server.cabal b/hackage-server.cabal
index eaf343fb6..b7370601e 100644
--- a/hackage-server.cabal
+++ b/hackage-server.cabal
@@ -313,6 +313,8 @@ library lib-server
Distribution.Server.Features.HoogleData
Distribution.Server.Features.HaskellPlatform
Distribution.Server.Features.HaskellPlatform.State
+ Distribution.Server.Features.PackageInfoJSON
+ Distribution.Server.Features.PackageInfoJSON.State
Distribution.Server.Features.Search
Distribution.Server.Features.Search.BM25F
Distribution.Server.Features.Search.DocIdSet
@@ -561,3 +563,4 @@ test-suite HashTests
-- component-specific dependencies
, tasty ^>= 1.4
, tasty-hunit ^>= 0.10
+
diff --git a/shell.nix b/shell.nix
index 8051010bf..64e7ed49b 100644
--- a/shell.nix
+++ b/shell.nix
@@ -17,5 +17,6 @@ pkgs.mkShell {
# Dependencies
pkgs.icu
pkgs.zlib
+ pkgs.brotli
];
}
diff --git a/src/Distribution/Server/Features.hs b/src/Distribution/Server/Features.hs
index 669342b43..76d9c5145 100644
--- a/src/Distribution/Server/Features.hs
+++ b/src/Distribution/Server/Features.hs
@@ -29,6 +29,7 @@ import Distribution.Server.Features.Distro (initDistroFeature)
import Distribution.Server.Features.PackageContents (initPackageContentsFeature)
import Distribution.Server.Features.Documentation (initDocumentationFeature)
import Distribution.Server.Features.BuildReports (initBuildReportsFeature)
+import Distribution.Server.Features.PackageInfoJSON (initPackageInfoJSONFeature)
import Distribution.Server.Features.LegacyRedirects (legacyRedirectsFeature)
import Distribution.Server.Features.PreferredVersions (initVersionsFeature)
-- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies (initReverseFeature)
@@ -151,6 +152,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
initSitemapFeature env
mkPackageFeedFeature <- logStartup "package feed" $
initPackageFeedFeature env
+ mkPackageJSONFeature <- logStartup "package info JSON" $
+ initPackageInfoJSONFeature env
#endif
loginfo verbosity "Initialising features, part 2"
@@ -324,6 +327,10 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
usersFeature
tarIndexCacheFeature
+ packageInfoJSONFeature <- mkPackageJSONFeature
+ coreFeature
+ versionsFeature
+
#endif
-- The order of initialization above should be the same as
@@ -364,6 +371,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
, getFeatureInterface adminLogFeature
, getFeatureInterface siteMapFeature
, getFeatureInterface packageFeedFeature
+ , getFeatureInterface packageInfoJSONFeature
#endif
, staticFilesFeature
, serverIntrospectFeature allFeatures
diff --git a/src/Distribution/Server/Features/Core.hs b/src/Distribution/Server/Features/Core.hs
index 3a34fe6a8..d40241c03 100644
--- a/src/Distribution/Server/Features/Core.hs
+++ b/src/Distribution/Server/Features/Core.hs
@@ -220,6 +220,8 @@ data CoreResource = CoreResource {
coreCabalFile :: Resource,
-- | A tarball for a package version.
corePackageTarball :: Resource,
+ -- | A Cabal file metatada revision.
+ coreCabalFileRev :: Resource,
-- Rendering resources.
-- | URI for `corePackagesPage`, given a format (blank for none).
diff --git a/src/Distribution/Server/Features/PackageInfoJSON.hs b/src/Distribution/Server/Features/PackageInfoJSON.hs
new file mode 100644
index 000000000..4e2b90831
--- /dev/null
+++ b/src/Distribution/Server/Features/PackageInfoJSON.hs
@@ -0,0 +1,272 @@
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Distribution.Server.Features.PackageInfoJSON (
+ PackageInfoJSONFeature(..)
+ , PackageInfoJSONResource(..)
+ , initPackageInfoJSONFeature
+
+ , PackageBasicDescription(..)
+ , PackageVersions(..)
+ ) where
+
+import Prelude ()
+import Distribution.Server.Prelude
+
+import qualified Data.Aeson as Aeson
+import qualified Data.ByteString.Lazy.Char8 as BS (toStrict)
+import qualified Data.Text as T
+import qualified Data.Vector as Vector
+
+import Distribution.License (licenseToSPDX)
+import Distribution.Package (PackageIdentifier(..),
+ PackageName, packageName,
+ packageVersion)
+import qualified Distribution.Parsec as Parsec
+import qualified Distribution.PackageDescription.Parsec as PkgDescr
+import qualified Distribution.Types.GenericPackageDescription as PkgDescr
+import qualified Distribution.Types.PackageDescription as PkgDescr
+import Distribution.Version (nullVersion)
+
+import Distribution.Server.Framework ((>))
+import qualified Distribution.Server.Framework as Framework
+import Distribution.Server.Features.Core (CoreFeature(..),
+ CoreResource(..),
+ isPackageChangeAny)
+import qualified Distribution.Server.Features.PreferredVersions as Preferred
+import Distribution.Server.Packages.Types (CabalFileText(..), pkgMetadataRevisions)
+import Distribution.Server.Framework.BackupRestore (RestoreBackup(..))
+
+import Distribution.Server.Features.PackageInfoJSON.State (PackageBasicDescription(..),
+ PackageVersions(..),
+ PackageInfoState(..),
+ GetPackageInfo(..),
+ ReplacePackageInfo(..),
+ GetDescriptionFor(..),
+ SetDescriptionFor(..),
+ GetVersionsFor(..),
+ SetVersionsFor(..),
+ initialPackageInfoState
+ )
+import Distribution.Utils.ShortText (fromShortText)
+import Data.Foldable (toList)
+import Data.Traversable (for)
+import qualified Data.List as List
+
+
+data PackageInfoJSONFeature = PackageInfoJSONFeature {
+ packageInfoJSONFeatureInterface :: Framework.HackageFeature
+}
+
+
+instance Framework.IsHackageFeature PackageInfoJSONFeature where
+ getFeatureInterface = packageInfoJSONFeatureInterface
+
+
+data PackageInfoJSONResource = PackageInfoJSONResource {
+ packageJSONResource :: Framework.Resource,
+ packageVersionJSONResource :: Framework.Resource
+}
+
+
+-- | Initializing our feature involves adding JSON variants to the
+-- endpoints that serve basic information about a package-version,
+-- and a packages version deprecation status.
+-- Aditionally we set up caching for these endpoints,
+-- and attach a package change hook that invalidates the cache
+-- line for a package when it changes
+initPackageInfoJSONFeature
+ :: Framework.ServerEnv
+ -> IO (CoreFeature -> Preferred.VersionsFeature -> IO PackageInfoJSONFeature)
+initPackageInfoJSONFeature env = do
+ packageInfoState <- packageInfoStateComponent False (Framework.serverStateDir env)
+ return $ \core preferred -> do
+
+ let coreR = coreResource core
+ info = "Get basic package information"
+ vInfo = "Get basic package information at a specific metadata revision"
+
+ jsonResources = [
+ (Framework.extendResource (corePackagePage coreR)) {
+ Framework.resourceDesc = [(Framework.GET, info)]
+ , Framework.resourceGet =
+ [("json", servePackageBasicDescription coreR
+ preferred packageInfoState)]
+ }
+ , (Framework.extendResource (coreCabalFileRev coreR)) {
+ Framework.resourceDesc = [(Framework.GET, vInfo)]
+ , Framework.resourceGet =
+ [("json", servePackageBasicDescription coreR
+ preferred packageInfoState)]
+ }
+ ]
+
+ -- When a package is modified in any way, delet all its
+ -- PackageInfoState cache lines.
+ -- They will be recalculated next time the endpoint
+ -- is hit
+ postInit = Framework.registerHookJust
+ (packageChangeHook core)
+ isPackageChangeAny $ \(pkgid, _) -> do
+
+ Framework.updateState packageInfoState $
+ SetDescriptionFor (pkgid, Nothing) Nothing
+ Framework.updateState packageInfoState $
+ SetVersionsFor (packageName pkgid) Nothing
+
+ return $ PackageInfoJSONFeature {
+ packageInfoJSONFeatureInterface =
+ (Framework.emptyHackageFeature "package-info-json")
+ { Framework.featureDesc = "Provide JSON endpoints for basic package descriptions"
+ , Framework.featureResources = jsonResources
+ , Framework.featureCaches = []
+ , Framework.featurePostInit = postInit
+ , Framework.featureState =
+ [Framework.abstractAcidStateComponent packageInfoState]
+ }
+ }
+
+
+-- | Pure function for extrcacting basic package info from a Cabal file
+getBasicDescription
+ :: CabalFileText
+ -> Int
+ -- ^ Metadata revision. This will be added to the resulting
+ -- @PackageBasicDescription@
+ -> Either String PackageBasicDescription
+getBasicDescription (CabalFileText cf) metadataRev =
+ let parseResult = PkgDescr.parseGenericPackageDescription (BS.toStrict cf)
+ in case PkgDescr.runParseResult parseResult of
+ (_, Right pkg) -> let
+ pkgd = PkgDescr.packageDescription pkg
+ pbd_author = T.pack . fromShortText $ PkgDescr.author pkgd
+ pbd_copyright = T.pack . fromShortText $ PkgDescr.copyright pkgd
+ pbd_synopsis = T.pack . fromShortText $ PkgDescr.synopsis pkgd
+ pbd_description = T.pack . fromShortText $ PkgDescr.description pkgd
+ pbd_license = either id licenseToSPDX $
+ PkgDescr.licenseRaw pkgd
+ pbd_homepage = T.pack . fromShortText $ PkgDescr.homepage pkgd
+ pbd_metadata_revision = metadataRev
+ in
+ return $ PackageBasicDescription {..}
+ (_, Left (_, perrs)) ->
+ let errs = List.intersperse '\n' $ mconcat $ for (toList perrs) $ \err -> Parsec.showPError "" err
+ in Left $ "Could not parse cabal file: "
+ <> errs
+
+
+-- | Get a JSON @PackageBasicDescription@ for a particular
+-- package/version/metadata-revision
+-- OR
+-- A listing of versions and their deprecation states
+servePackageBasicDescription
+ :: CoreResource
+ -> Preferred.VersionsFeature
+ -> Framework.StateComponent Framework.AcidState PackageInfoState
+ -> Framework.DynamicPath
+ -- ^ URI specifying a package and version `e.g. lens or lens-4.11`
+ -> Framework.ServerPartE Framework.Response
+servePackageBasicDescription resource preferred packageInfoState dpath = do
+
+ let metadataRev :: Maybe Int = lookup "revision" dpath >>= Framework.fromReqURI
+
+ pkgid@(PackageIdentifier name version) <- packageInPath resource dpath
+ guardValidPackageName resource name
+
+ if version /= nullVersion
+ then lookupOrInsertDescr pkgid metadataRev
+ else lookupOrInsertVersions name
+
+ where
+
+ lookupOrInsertDescr
+ :: PackageIdentifier
+ -> Maybe Int
+ -> Framework.ServerPartE Framework.Response
+ lookupOrInsertDescr pkgid metadataRev = do
+ cachedDescr <- Framework.queryState packageInfoState $
+ GetDescriptionFor (pkgid, metadataRev)
+ descr :: PackageBasicDescription <- case cachedDescr of
+ Just d -> return d
+ Nothing -> do
+ d <- getPackageDescr pkgid metadataRev
+ Framework.updateState packageInfoState $
+ SetDescriptionFor (pkgid, metadataRev) (Just d)
+ return d
+ return $ Framework.toResponse $ Aeson.toJSON descr
+
+ getPackageDescr pkgid metadataRev = do
+ guardValidPackageId resource pkgid
+ pkg <- lookupPackageId resource pkgid
+
+ let metadataRevs = fst <$> pkgMetadataRevisions pkg
+ nMetadata = Vector.length metadataRevs
+ metadataInd = fromMaybe (nMetadata - 1) metadataRev
+
+ when (metadataInd < 0 || metadataInd >= nMetadata)
+ (Framework.errNotFound "Revision not found"
+ [Framework.MText
+ $ "There are " <> show nMetadata <> " metadata revisions. Index "
+ <> show metadataInd <> " is out of bounds."]
+ )
+
+ let cabalFile = metadataRevs Vector.! metadataInd
+ pkgDescr = getBasicDescription cabalFile metadataInd
+ case pkgDescr of
+ Left e -> Framework.errInternalError [Framework.MText e]
+ Right d -> return d
+
+ lookupOrInsertVersions
+ :: PackageName
+ -> Framework.ServerPartE Framework.Response
+ lookupOrInsertVersions pkgname = do
+ cachedVersions <- Framework.queryState packageInfoState $
+ GetVersionsFor pkgname
+ vers :: PackageVersions <- case cachedVersions of
+ Just vs -> return vs
+ Nothing -> do
+ vs <- getVersionListing pkgname
+ Framework.updateState packageInfoState $
+ SetVersionsFor pkgname (Just vs)
+ return vs
+ return $ Framework.toResponse $ Aeson.toJSON vers
+
+ getVersionListing name = do
+ pkgs <- lookupPackageName resource name
+ prefInfo <- Preferred.queryGetPreferredInfo preferred name
+ return
+ . PackageVersions
+ . Preferred.classifyVersions prefInfo
+ $ fmap packageVersion pkgs
+
+
+-- | Our backup doesn't produce any entries, and backup restore
+-- returns an empty state. Our responses are cheap enough to
+-- compute that we would rather regenerate them by need than
+-- deal with the complexity persisting backups in
+-- yet-another-format
+packageInfoStateComponent
+ :: Bool
+ -> FilePath
+ -> IO (Framework.StateComponent Framework.AcidState PackageInfoState)
+packageInfoStateComponent freshDB stateDir = do
+ st <- Framework.openLocalStateFrom
+ (stateDir > "db" > "PackageInfoJSON")
+ (initialPackageInfoState freshDB)
+ return Framework.StateComponent {
+ stateDesc = "Preferred package versions"
+ , stateHandle = st
+ , getState = Framework.query st GetPackageInfo
+ , putState = Framework.update st . ReplacePackageInfo
+ , resetState = packageInfoStateComponent True
+ , backupState = \_ -> return []
+ , restoreState = nullRestore (initialPackageInfoState True)
+ }
+ where
+
+ nullRestore :: PackageInfoState -> RestoreBackup PackageInfoState
+ nullRestore st = RestoreBackup {
+ restoreEntry = \_ -> nullRestore <$> pure (initialPackageInfoState True)
+ , restoreFinalize = return st
+ }
diff --git a/src/Distribution/Server/Features/PackageInfoJSON/State.hs b/src/Distribution/Server/Features/PackageInfoJSON/State.hs
new file mode 100644
index 000000000..6aac15d91
--- /dev/null
+++ b/src/Distribution/Server/Features/PackageInfoJSON/State.hs
@@ -0,0 +1,252 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Distribution.Server.Features.PackageInfoJSON.State where
+
+import Control.Arrow (first, second)
+import Control.Applicative ((<|>))
+import Control.Monad.Reader (ask, asks)
+import qualified Control.Monad.State as State
+import qualified Data.Aeson as Aeson
+import Data.Aeson ((.=), (.:))
+import Data.Acid (Query, Update, makeAcidic)
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Map.Strict as Map
+import Data.Monoid (Sum(..))
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Data.SafeCopy (SafeCopy(..), base, contain,
+ deriveSafeCopy)
+import Data.Serialize (Get, get, getListOf, getTwoOf, put,
+ putListOf, putTwoOf)
+import Data.Typeable (Typeable)
+import Data.Word (Word8)
+import Distribution.License (licenseToSPDX)
+import Distribution.Text (display, simpleParse)
+import GHC.Generics (Generic)
+
+import Distribution.SPDX.License (License)
+import Distribution.Package (PackageIdentifier, PackageName)
+import Distribution.Version (Version, mkVersion, versionNumbers)
+import qualified Distribution.Pretty as Pretty
+import qualified Distribution.Parsec as Parsec
+
+import qualified Distribution.Server.Features.PreferredVersions as Preferred
+import Distribution.Server.Framework.MemSize (MemSize,
+ memSize,
+ memSize7)
+
+
+-- | Basic information about a package. These values are
+-- used in the `/package/:packagename` JSON endpoint
+data PackageBasicDescription = PackageBasicDescription
+ { pbd_license :: !License
+ , pbd_copyright :: !T.Text
+ , pbd_synopsis :: !T.Text
+ , pbd_description :: !T.Text
+ , pbd_author :: !T.Text
+ , pbd_homepage :: !T.Text
+ , pbd_metadata_revision :: !Int
+ } deriving (Eq, Show, Generic)
+
+instance SafeCopy PackageBasicDescription where
+
+ putCopy PackageBasicDescription{..} = contain $ do
+ put (Pretty.prettyShow pbd_license)
+ put $ T.encodeUtf8 pbd_copyright
+ put $ T.encodeUtf8 pbd_synopsis
+ put $ T.encodeUtf8 pbd_description
+ put $ T.encodeUtf8 pbd_author
+ put $ T.encodeUtf8 pbd_homepage
+ put pbd_metadata_revision
+
+ getCopy = contain $ do
+ licenseStr <- get
+ case Parsec.eitherParsec licenseStr of
+ Left e -> fail $ unwords ["Could not parse", licenseStr, "as license:" , e]
+ Right pbd_license -> do
+ pbd_copyright <- T.decodeUtf8 <$> get
+ pbd_synopsis <- T.decodeUtf8 <$> get
+ pbd_description <- T.decodeUtf8 <$> get
+ pbd_author <- T.decodeUtf8 <$> get
+ pbd_homepage <- T.decodeUtf8 <$> get
+ pbd_metadata_revision <- get
+ return PackageBasicDescription{..}
+
+
+-- | Aeson instances are used for building the package-description
+-- endpoint. Any changes will impact the API endpoint.
+instance Aeson.ToJSON PackageBasicDescription where
+ toJSON PackageBasicDescription {..} =
+ Aeson.object
+ [ T.pack "license" .= Pretty.prettyShow pbd_license
+ , T.pack "copyright" .= pbd_copyright
+ , T.pack "synopsis" .= pbd_synopsis
+ , T.pack "description" .= pbd_description
+ , T.pack "author" .= pbd_author
+ , T.pack "homepage" .= pbd_homepage
+ , T.pack "metadata_revision" .= pbd_metadata_revision
+ ]
+
+
+instance Aeson.FromJSON PackageBasicDescription where
+ parseJSON = Aeson.withObject "PackageBasicDescription" $ \obj -> do
+ pbd_version' <- obj .: T.pack "license"
+ let parseEitherLicense t =
+ Parsec.simpleParsec t <|> fmap licenseToSPDX (simpleParse t)
+ case parseEitherLicense pbd_version' of
+ Nothing -> fail $ concat ["Could not parse version: \"", pbd_version', "\""]
+ Just pbd_license -> do
+ pbd_copyright <- obj .: T.pack "copyright"
+ pbd_synopsis <- obj .: T.pack "synopsis"
+ pbd_description <- obj .: T.pack "description"
+ pbd_author <- obj .: T.pack "author"
+ pbd_homepage <- obj .: T.pack "homepage"
+ pbd_metadata_revision <- obj .: T.pack "metadata_revision"
+ return $
+ PackageBasicDescription {..}
+
+-- | An index of versions for one hackage package
+-- and their preferred/deprecated status
+newtype PackageVersions = PackageVersions {
+ unPackageVersions :: [(Version, Preferred.VersionStatus)]
+ } deriving (Eq, Show)
+
+instance SafeCopy PackageVersions where
+
+ putCopy (PackageVersions vs) =
+ contain
+ $ putListOf (putTwoOf put put)
+ $ first versionNumbers . second statusTag <$> vs
+ where
+ statusTag = \case
+ Preferred.NormalVersion -> 0 :: Word8
+ Preferred.DeprecatedVersion -> 1
+ Preferred.UnpreferredVersion -> 2
+
+ getCopy = contain $
+ fmap PackageVersions $ getListOf $ getTwoOf getVersion getStatus
+ where
+ getVersion = mkVersion <$> getListOf get
+ getStatus = (get :: Get Word8) >>= \case
+ 0 -> return Preferred.NormalVersion
+ 1 -> return Preferred.DeprecatedVersion
+ 2 -> return Preferred.UnpreferredVersion
+ n -> fail $ "Unsupported tag for VersionStatus: " ++ show n
+
+
+-- | This encoding of @PackageVersions@ is used in the
+-- `/package/$package` endpoint (when the URI doesn't specify)
+-- a version. Any change here is an API change.
+instance Aeson.ToJSON PackageVersions where
+ toJSON (PackageVersions p) =
+ Aeson.toJSON
+ $ Map.mapKeys display
+ $ fmap encodeStatus
+ $ Map.fromList p
+ where
+ encodeStatus = \case
+ Preferred.NormalVersion -> "normal"
+ Preferred.DeprecatedVersion -> "deprecated"
+ Preferred.UnpreferredVersion -> "unpreferred"
+
+
+instance Aeson.FromJSON PackageVersions where
+ parseJSON = Aeson.withObject "PackageVersions" $ \obj ->
+ fmap PackageVersions
+ $ traverse (parsePair)
+ $ HashMap.toList obj
+ where
+ parsePair (vStr, vStatus) =
+ (,) <$> parseVersion vStr <*> parseStatus vStatus
+
+ parseVersion verText =
+ let verString = T.unpack verText
+ in case simpleParse verString of
+ Just ver -> return ver
+ Nothing -> fail $ concat ["Could not parse \""
+ , verString ++ "\" as Version. "
+ , "expected \"a.b.c\" form"]
+
+ parseStatus (Aeson.String s) = case T.unpack s of
+ "normal" -> return Preferred.NormalVersion
+ "deprecated" -> return Preferred.DeprecatedVersion
+ "unpreferred" -> return Preferred.UnpreferredVersion
+ other -> fail $ concat ["Could not parse \"" ++ other
+ ++ "\" as status. Expected \"normal\""
+ ++ "\"deprecated\" or \"unpreferred\""]
+ parseStatus _ = fail "Expected a string"
+
+data PackageInfoState = PackageInfoState {
+ descriptions :: !(Map.Map (PackageIdentifier, Maybe Int) PackageBasicDescription)
+ , versions :: !(Map.Map PackageName PackageVersions)
+ , migratedEphemeralData :: Bool
+ } deriving (Show, Typeable, Eq)
+
+getDescriptionFor
+ :: (PackageIdentifier, Maybe Int)
+ -> Query PackageInfoState (Maybe PackageBasicDescription)
+getDescriptionFor pkgId = asks $ Map.lookup pkgId . descriptions
+
+getVersionsFor
+ :: PackageName
+ -> Query PackageInfoState (Maybe PackageVersions)
+getVersionsFor pkgName = asks $ Map.lookup pkgName . versions
+
+setDescriptionFor
+ :: (PackageIdentifier, Maybe Int)
+ -> Maybe PackageBasicDescription
+ -> Update PackageInfoState ()
+setDescriptionFor pkgId descr = State.modify $ \p ->
+ case descr of
+ Just d -> p {descriptions = Map.alter (const (Just d)) pkgId (descriptions p)}
+ Nothing -> p {descriptions = Map.filterWithKey (\pkgId' _ -> fst pkgId' /= fst pkgId) (descriptions p)}
+
+setVersionsFor
+ :: PackageName
+ -> Maybe PackageVersions
+ -> Update PackageInfoState ()
+setVersionsFor pkgName vs = State.modify $ \p ->
+ p { versions = Map.alter (const vs) pkgName (versions p) }
+
+getPackageInfo :: Query PackageInfoState PackageInfoState
+getPackageInfo = ask
+
+replacePackageInfo :: PackageInfoState -> Update PackageInfoState ()
+replacePackageInfo = State.put
+
+makeAcidic ''PackageInfoState ['getDescriptionFor
+ ,'getVersionsFor
+ ,'setDescriptionFor
+ ,'setVersionsFor
+ ,'getPackageInfo
+ ,'replacePackageInfo
+ ]
+
+deriveSafeCopy 0 'base ''PackageInfoState
+
+instance MemSize PackageBasicDescription where
+ memSize PackageBasicDescription{..} =
+ memSize7 (Pretty.prettyShow pbd_license) pbd_copyright pbd_synopsis
+ pbd_description pbd_author pbd_homepage pbd_metadata_revision
+
+instance MemSize PackageVersions where
+ memSize (PackageVersions ps) = getSum $
+ foldMap (\(v,_) -> Sum (memSize v) `mappend` Sum (memSize (0 :: Word))) ps
+
+instance MemSize PackageInfoState where
+ memSize (PackageInfoState {..}) = memSize descriptions + memSize versions
+
+
+initialPackageInfoState :: Bool -> PackageInfoState
+initialPackageInfoState freshDB = PackageInfoState
+ { descriptions = mempty
+ , versions = mempty
+ , migratedEphemeralData = freshDB
+ }
diff --git a/src/Distribution/Server/Framework/Resource.hs b/src/Distribution/Server/Framework/Resource.hs
index 3ffbb95ce..3067d8ac4 100644
--- a/src/Distribution/Server/Framework/Resource.hs
+++ b/src/Distribution/Server/Framework/Resource.hs
@@ -521,14 +521,11 @@ negotiateContent :: (FilterMonad Response m, ServerMonad m)
negotiateContent def available = do
when (length available > 1) $
setHeaderM "Vary" "Accept"
- maccept <- getHeaderM "Accept"
- case maccept of
- Nothing -> return def
- Just accept ->
- return $ fromMaybe def $ listToMaybe $ catMaybes
+ accept <- maybe "text/html" BS.unpack <$> getHeaderM "Accept"
+ return $ fromMaybe def $ listToMaybe $ catMaybes
[ simpleContentTypeMapping ct
>>= \f -> find (\x -> fst x == f) available
- | let acceptable = parseContentAccept (BS.unpack accept)
+ | let acceptable = parseContentAccept accept
, ct <- acceptable ]
where
-- This is rather a non-extensible hack
@@ -597,4 +594,3 @@ addServerNode trunk response tree = treeFold trunk (ServerTree (Just response) M
treeFold :: Monoid a => BranchPath -> ServerTree a -> ServerTree a -> ServerTree a
treeFold [] newChild topLevel = combine newChild topLevel
treeFold (sdir:otherTree) newChild topLevel = treeFold otherTree (ServerTree Nothing $ Map.singleton sdir newChild) topLevel
-
diff --git a/tests/HighLevelTest.hs b/tests/HighLevelTest.hs
index e21ca3ac8..2063b135f 100644
--- a/tests/HighLevelTest.hs
+++ b/tests/HighLevelTest.hs
@@ -213,14 +213,27 @@ runPackageTests = do
die "Bad index contents"
do info "Getting package index with etag"
validateETagHandling "/packages/index.tar.gz"
+
do info "Getting testpackage info"
- xs <- validate NoAuth "/package/testpackage"
+ xs <- validate NoAuth "/package/testpackage.html"
unless (">testpackage: test package testpackage" `isInfixOf` xs) $
- die ("Bad package info: " ++ show xs)
+ die ("Bad package info for unversioned HTML /package/testpackage.html endpoint: " ++ show xs)
+
do info "Getting testpackage-1.0.0.0 info"
xs <- validate NoAuth "/package/testpackage-1.0.0.0"
unless (">testpackage: test package testpackage" `isInfixOf` xs) $
- die ("Bad package info: " ++ show xs)
+ die ("Bad package info for versioned HTML /package/testpackage-1.0.0.0 endpoint: " ++ show xs)
+
+ do info "Getting testpackage-1.0.0.0 info (JSON)"
+ xs <- validate NoAuth "/package/testpackage-1.0.0.0.json"
+ unless ("\"synopsis\":\"test package testpackage\"" `isInfixOf` xs) $
+ die ("Bad package info for versioned JSON endpoint, expected the synopsis field to contain \"test package testpackage\": " ++ show xs)
+
+ do info "Getting testpackage version info (JSON)"
+ xs <- validate NoAuth "/package/testpackage.json"
+ unless ("\"1.0.0.0\":\"normal\"" `isInfixOf` xs) $
+ die ("Bad package version info: " ++ show xs)
+
do info "Getting testpackage Cabal file"
cabalFile <- getUrl NoAuth "/package/testpackage-1.0.0.0/testpackage.cabal"
unless (cabalFile == testpackageCabalFile) $