11package api
22
33import (
4+ "context"
5+ "encoding/json"
46 "net/http"
7+ "os"
58
69 "github.com/go-chi/chi/v5"
710 "github.com/go-chi/chi/v5/middleware"
811 "github.com/go-chi/cors"
912
1013 "cdr.dev/slog"
14+ "github.com/coder/code-marketplace/api/httpapi"
1115 "github.com/coder/code-marketplace/api/httpmw"
16+ "github.com/coder/code-marketplace/database"
1217)
1318
19+ // QueryRequest implements an untyped object. It is the data sent to the API to
20+ // query for extensions.
21+ // https://github.com/microsoft/vscode/blob/a69f95fdf3dc27511517eef5ff62b21c7a418015/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L338-L342
22+ type QueryRequest struct {
23+ Filters []database.Filter `json:"filters"`
24+ Flags database.Flag `json:"flags"`
25+ }
26+
27+ // QueryResponse implements IRawGalleryQueryResult. This is the response sent
28+ // to extension queries.
29+ // https://github.com/microsoft/vscode/blob/29234f0219bdbf649d6107b18651a1038d6357ac/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L81-L92
30+ type QueryResponse struct {
31+ Results []QueryResult `json:"results"`
32+ }
33+
34+ // QueryResult implements IRawGalleryQueryResult.results.
35+ // https://github.com/microsoft/vscode/blob/29234f0219bdbf649d6107b18651a1038d6357ac/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L82-L91
36+ type QueryResult struct {
37+ Extensions []* database.Extension `json:"extensions"`
38+ Metadata []ResultMetadata `json:"resultMetadata"`
39+ }
40+
41+ // ResultMetadata implements IRawGalleryQueryResult.resultMetadata.
42+ // https://github.com/microsoft/vscode/blob/29234f0219bdbf649d6107b18651a1038d6357ac/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L84-L90
43+ type ResultMetadata struct {
44+ Type string `json:"metadataType"`
45+ Items []ResultMetadataItem `json:"metadataItems"`
46+ }
47+
48+ // ResultMetadataItem implements IRawGalleryQueryResult.metadataItems.
49+ // https://github.com/microsoft/vscode/blob/29234f0219bdbf649d6107b18651a1038d6357ac/src/vs/platform/extensionManagement/common/extensionGalleryService.ts#L86-L89
50+ type ResultMetadataItem struct {
51+ Name string `json:"name"`
52+ Count int `json:"count"`
53+ }
54+
1455type Options struct {
56+ Database database.Database
57+ // TODO: Abstract file storage for use with storage services like jFrog.
1558 ExtDir string
1659 Logger slog.Logger
1760 // Set to <0 to disable.
1861 RateLimit int
1962}
2063
2164type API struct {
22- Handler http.Handler
65+ Database database.Database
66+ Handler http.Handler
67+ Logger slog.Logger
2368}
2469
2570// New creates a new API server.
@@ -48,6 +93,12 @@ func New(options *Options) *API {
4893 httpmw .Logger (options .Logger ),
4994 )
5095
96+ api := & API {
97+ Database : options .Database ,
98+ Handler : r ,
99+ Logger : options .Logger ,
100+ }
101+
51102 r .Get ("/" , func (rw http.ResponseWriter , r * http.Request ) {
52103 httpapi .WriteBytes (rw , http .StatusOK , []byte ("Marketplace is running" ))
53104 })
@@ -56,7 +107,120 @@ func New(options *Options) *API {
56107 httpapi .WriteBytes (rw , http .StatusOK , []byte ("API server running" ))
57108 })
58109
59- return & API {
60- Handler : r ,
110+ // TODO: Read API version header and output a warning if it has changed since
111+ // that could indicate something needs to be updated.
112+ r .Post ("/api/extensionquery" , api .extensionQuery )
113+
114+ // Endpoint for getting an extension's files or the extension zip.
115+ options .Logger .Info (context .Background (), "Serving files" , slog .F ("dir" , options .ExtDir ))
116+ r .Mount ("/files" , http .StripPrefix ("/files" , http .FileServer (http .Dir (options .ExtDir ))))
117+
118+ // VS Code can use the files in the response to get file paths but it will
119+ // sometimes ignore that and use use requests to /assets with hardcoded
120+ // types to get files.
121+ r .Get ("/assets/{publisher}/{extension}/{version}/{type}" , api .assetRedirect )
122+
123+ return api
124+ }
125+
126+ func (api * API ) extensionQuery (rw http.ResponseWriter , r * http.Request ) {
127+ ctx := r .Context ()
128+
129+ var query QueryRequest
130+ if r .ContentLength <= 0 {
131+ query = QueryRequest {}
132+ } else {
133+ err := json .NewDecoder (r .Body ).Decode (& query )
134+ if err != nil {
135+ httpapi .Write (rw , http .StatusBadRequest , httpapi.ErrorResponse {
136+ Message : "Unable to read query" ,
137+ Detail : "Check that the posted data is valid JSON" ,
138+ RequestID : httpmw .RequestID (r ),
139+ })
140+ return
141+ }
142+ }
143+
144+ // Validate query sizes.
145+ if len (query .Filters ) == 0 {
146+ query .Filters = append (query .Filters , database.Filter {})
147+ } else if len (query .Filters ) > 1 {
148+ // VS Code always seems to use one filter.
149+ httpapi .Write (rw , http .StatusBadRequest , httpapi.ErrorResponse {
150+ Message : "Too many filters" ,
151+ Detail : "Check that you only have one filter" ,
152+ RequestID : httpmw .RequestID (r ),
153+ })
61154 }
155+ for _ , filter := range query .Filters {
156+ if filter .PageSize < 0 || filter .PageSize > 50 {
157+ httpapi .Write (rw , http .StatusBadRequest , httpapi.ErrorResponse {
158+ Message : "Invalid page size" ,
159+ Detail : "Check that the page size is between zero and fifty" ,
160+ RequestID : httpmw .RequestID (r ),
161+ })
162+ }
163+ }
164+
165+ baseURL := httpapi .RequestBaseURL (r , "/" )
166+
167+ // Each filter gets its own entry in the results.
168+ results := []QueryResult {}
169+ for _ , filter := range query .Filters {
170+ extensions , count , err := api .Database .GetExtensions (ctx , filter , query .Flags , baseURL )
171+ if err != nil {
172+ api .Logger .Error (ctx , "Unable to execute query" , slog .Error (err ))
173+ httpapi .Write (rw , http .StatusInternalServerError , httpapi.ErrorResponse {
174+ Message : "Internal server error while executing query" ,
175+ Detail : "Contact an administrator with the request ID" ,
176+ RequestID : httpmw .RequestID (r ),
177+ })
178+ return
179+ }
180+
181+ api .Logger .Debug (ctx , "Got extensions for filter" ,
182+ slog .F ("filter" , filter ),
183+ slog .F ("count" , count ))
184+
185+ results = append (results , QueryResult {
186+ Extensions : extensions ,
187+ Metadata : []ResultMetadata {{
188+ Type : "ResultCount" ,
189+ Items : []ResultMetadataItem {{
190+ Count : count ,
191+ Name : "TotalCount" ,
192+ }},
193+ }},
194+ })
195+ }
196+
197+ httpapi .Write (rw , http .StatusOK , QueryResponse {Results : results })
198+ }
199+
200+ func (api * API ) assetRedirect (rw http.ResponseWriter , r * http.Request ) {
201+ // TODO: Asset URIs can contain a targetPlatform query variable.
202+ baseURL := httpapi .RequestBaseURL (r , "/" )
203+ url , err := api .Database .GetExtensionAssetPath (r .Context (), & database.Asset {
204+ Extension : chi .URLParam (r , "extension" ),
205+ Publisher : chi .URLParam (r , "publisher" ),
206+ Type : chi .URLParam (r , "type" ),
207+ Version : chi .URLParam (r , "version" ),
208+ }, baseURL )
209+ if err != nil && os .IsNotExist (err ) {
210+ httpapi .Write (rw , http .StatusNotFound , httpapi.ErrorResponse {
211+ Message : "Extension asset does not exist" ,
212+ Detail : "Please check the asset path" ,
213+ RequestID : httpmw .RequestID (r ),
214+ })
215+ return
216+ } else if err != nil {
217+ httpapi .Write (rw , http .StatusInternalServerError , httpapi.ErrorResponse {
218+ Message : "Unable to read extension" ,
219+ Detail : "Contact an administrator with the request ID" ,
220+ RequestID : httpmw .RequestID (r ),
221+ })
222+ return
223+ }
224+
225+ http .Redirect (rw , r , url , http .StatusMovedPermanently )
62226}
0 commit comments