1
1
package api
2
2
3
3
import (
4
+ "context"
5
+ "encoding/json"
4
6
"net/http"
7
+ "os"
5
8
6
9
"github.com/go-chi/chi/v5"
7
10
"github.com/go-chi/chi/v5/middleware"
8
11
"github.com/go-chi/cors"
9
12
10
13
"cdr.dev/slog"
14
+ "github.com/coder/code-marketplace/api/httpapi"
11
15
"github.com/coder/code-marketplace/api/httpmw"
16
+ "github.com/coder/code-marketplace/database"
12
17
)
13
18
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
+
14
55
type Options struct {
56
+ Database database.Database
57
+ // TODO: Abstract file storage for use with storage services like jFrog.
15
58
ExtDir string
16
59
Logger slog.Logger
17
60
// Set to <0 to disable.
18
61
RateLimit int
19
62
}
20
63
21
64
type API struct {
22
- Handler http.Handler
65
+ Database database.Database
66
+ Handler http.Handler
67
+ Logger slog.Logger
23
68
}
24
69
25
70
// New creates a new API server.
@@ -48,6 +93,12 @@ func New(options *Options) *API {
48
93
httpmw .Logger (options .Logger ),
49
94
)
50
95
96
+ api := & API {
97
+ Database : options .Database ,
98
+ Handler : r ,
99
+ Logger : options .Logger ,
100
+ }
101
+
51
102
r .Get ("/" , func (rw http.ResponseWriter , r * http.Request ) {
52
103
httpapi .WriteBytes (rw , http .StatusOK , []byte ("Marketplace is running" ))
53
104
})
@@ -56,7 +107,120 @@ func New(options *Options) *API {
56
107
httpapi .WriteBytes (rw , http .StatusOK , []byte ("API server running" ))
57
108
})
58
109
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
+ })
61
154
}
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 )
62
226
}
0 commit comments