-
Notifications
You must be signed in to change notification settings - Fork 198
/
Copy pathPackageInfoJSON.hs
287 lines (249 loc) · 12.2 KB
/
PackageInfoJSON.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
{-# 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
import Data.Time (UTCTime)
import Distribution.Server.Users.Types (UserName, UserInfo(..))
import Distribution.Server.Features.Users (UserFeature(lookupUserInfo))
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.
-- Additionally 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 -> UserFeature -> IO PackageInfoJSONFeature)
initPackageInfoJSONFeature env = do
packageInfoState <- packageInfoStateComponent False (Framework.serverStateDir env)
return $ \core preferred userFeature -> do
let coreR = coreResource core
info = "Get basic package information: \
\The response contains a JSON object where the keys are version numbers as strings, \
\and the values are whether the version is preferred or not"
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 userFeature
preferred packageInfoState)]
}
, (Framework.extendResource (coreCabalFileRev coreR)) {
Framework.resourceDesc = [(Framework.GET, vInfo)]
, Framework.resourceGet =
[("json", servePackageBasicDescription coreR userFeature
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 extracting basic package info from a Cabal file
getBasicDescription
:: UserName
-> UTCTime
-- ^ Time of upload
-> CabalFileText
-> Int
-- ^ Metadata revision. This will be added to the resulting
-- @PackageBasicDescription@
-> Either String PackageBasicDescription
getBasicDescription uploader uploadedAt (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
pbd_uploaded_at = uploadedAt
pbd_uploader = uploader
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
-> UserFeature
-> 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 userFeature 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
uploadInfos = snd <$> 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
uploadedAt = fst $ uploadInfos Vector.! metadataInd
uploaderId = snd $ uploadInfos Vector.! metadataInd
uploader <- userName <$> lookupUserInfo userFeature uploaderId
let pkgDescr = getBasicDescription uploader uploadedAt 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
}