Skip to content

Commit 884acba

Browse files
committed
chore: revert back to FileServer returning an http handler
Allows bytes to be copied without reading them all into memory first.
1 parent 0971b71 commit 884acba

File tree

9 files changed

+63
-126
lines changed

9 files changed

+63
-126
lines changed

api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func New(options *Options) *API {
112112
r.Post("/api/extensionquery", api.extensionQuery)
113113

114114
// Endpoint for getting an extension's files or the extension zip.
115-
r.Mount("/files", http.StripPrefix("/files", storage.HTTPFileServer(options.Storage)))
115+
r.Mount("/files", http.StripPrefix("/files", options.Storage.FileServer()))
116116

117117
// VS Code can use the files in the response to get file paths but it will
118118
// sometimes ignore that and use requests to /assets with hardcoded types to

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ require (
99
github.com/go-chi/httprate v0.14.1
1010
github.com/google/uuid v1.6.0
1111
github.com/lithammer/fuzzysearch v1.1.8
12-
github.com/spf13/afero v1.11.0
1312
github.com/spf13/cobra v1.8.1
1413
github.com/stretchr/testify v1.9.0
1514
golang.org/x/mod v0.19.0
@@ -18,6 +17,7 @@ require (
1817
)
1918

2019
require (
20+
cloud.google.com/go/logging v1.8.1 // indirect
2121
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2222
github.com/cespare/xxhash/v2 v2.3.0 // indirect
2323
github.com/charmbracelet/lipgloss v0.7.1 // indirect
@@ -38,6 +38,9 @@ require (
3838
golang.org/x/sys v0.17.0 // indirect
3939
golang.org/x/term v0.17.0 // indirect
4040
golang.org/x/text v0.14.0 // indirect
41+
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
42+
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
43+
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
4144
google.golang.org/protobuf v1.32.0 // indirect
4245
gopkg.in/yaml.v3 v3.0.1 // indirect
4346
)

go.sum

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiV
55
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
66
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
77
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
8-
cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
9-
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
10-
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
11-
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
8+
cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU=
9+
cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI=
10+
cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg=
11+
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
1212
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
1313
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
1414
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -56,8 +56,6 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
5656
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
5757
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
5858
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
59-
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
60-
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
6159
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
6260
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
6361
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=

storage/artifactory.go

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"errors"
88
"fmt"
99
"io"
10-
"io/fs"
1110
"net/http"
1211
"os"
1312
"path"
@@ -16,7 +15,6 @@ import (
1615
"sync"
1716
"time"
1817

19-
"github.com/spf13/afero/mem"
2018
"golang.org/x/sync/errgroup"
2119
"golang.org/x/xerrors"
2220

@@ -277,37 +275,25 @@ func (s *Artifactory) AddExtension(ctx context.Context, manifest *VSIXManifest,
277275
return s.uri + dir, nil
278276
}
279277

280-
// Open returns a file from Artifactory.
281-
// TODO: Since we only extract a subset of files perhaps if the file does not
282-
// exist we should download the vsix and extract the requested file as a
283-
// fallback. Obviously this seems like quite a bit of overhead so we would
284-
// then emit a warning so we can notice that VS Code has added new asset types
285-
// that we should be extracting to avoid that overhead. Other solutions could
286-
// be implemented though like extracting the VSIX to disk locally and only
287-
// going to Artifactory for the VSIX when it is missing on disk (basically
288-
// using the disk as a cache).
289-
func (s *Artifactory) Open(ctx context.Context, fp string) (fs.File, error) {
290-
resp, code, err := s.read(ctx, fp)
291-
if code != http.StatusOK || err != nil {
292-
switch code {
293-
case http.StatusNotFound:
294-
return nil, fs.ErrNotExist
295-
case http.StatusForbidden:
296-
return nil, fs.ErrPermission
297-
default:
298-
return nil, err
278+
func (s *Artifactory) FileServer() http.Handler {
279+
// TODO: Since we only extract a subset of files perhaps if the file does not
280+
// exist we should download the vsix and extract the requested file as a
281+
// fallback. Obviously this seems like quite a bit of overhead so we would
282+
// then emit a warning so we can notice that VS Code has added new asset types
283+
// that we should be extracting to avoid that overhead. Other solutions could
284+
// be implemented though like extracting the VSIX to disk locally and only
285+
// going to Artifactory for the VSIX when it is missing on disk (basically
286+
// using the disk as a cache).
287+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
288+
reader, code, err := s.read(r.Context(), r.URL.Path)
289+
if err != nil {
290+
http.Error(rw, err.Error(), code)
291+
return
299292
}
300-
}
301-
302-
// TODO: Do no copy the bytes into memory, stream them rather than
303-
// storing the entire file into memory.
304-
f := mem.NewFileHandle(mem.CreateFile(fp))
305-
_, err = io.Copy(f, resp)
306-
if err != nil {
307-
return nil, err
308-
}
309-
310-
return f, nil
293+
defer reader.Close()
294+
rw.WriteHeader(http.StatusOK)
295+
_, _ = io.Copy(rw, reader)
296+
})
311297
}
312298

313299
func (s *Artifactory) Manifest(ctx context.Context, publisher, name string, version Version) (*VSIXManifest, error) {

storage/local.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix [
142142
return dir, nil
143143
}
144144

145+
func (s *Local) FileServer() http.Handler {
146+
return http.FileServer(http.Dir(s.extdir))
147+
}
148+
145149
func (s *Local) Open(_ context.Context, fp string) (fs.File, error) {
146150
return http.Dir(s.extdir).Open(fp)
147151
}

storage/signature.go

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import (
44
"context"
55
"crypto"
66
"encoding/json"
7-
"io"
8-
"io/fs"
9-
"path/filepath"
7+
"net/http"
8+
"strconv"
109
"strings"
1110

12-
"github.com/spf13/afero/mem"
1311
"golang.org/x/xerrors"
1412

1513
"cdr.dev/slog"
14+
"github.com/coder/code-marketplace/api/httpapi"
15+
"github.com/coder/code-marketplace/api/httpmw"
1616

1717
"github.com/coder/code-marketplace/extensionsign"
1818
)
@@ -113,7 +113,7 @@ func (s *Signature) Manifest(ctx context.Context, publisher, name string, versio
113113
return manifest, nil
114114
}
115115

116-
// Open will intercept requests for signed extensions payload.
116+
// FileServer will intercept requests for signed extensions payload.
117117
// It does this by looking for 'SigzipFileExtension' or p7s.sig.
118118
//
119119
// The signed payload and signing process is taken from:
@@ -125,61 +125,32 @@ func (s *Signature) Manifest(ctx context.Context, publisher, name string, versio
125125
// the signature. Meaning the signature could be empty, incorrect, or a
126126
// picture of cat and it would work. There is no signature verification.
127127
//
128-
// - VSCode requires a signature payload to exist, but the context appear
129-
// to be somewhat optional.
130-
// Following another open source implementation, it appears the '.signature.p7s'
131-
// file must exist, but it can be empty.
132-
// The signature is stored in a '.signature.sig' file, although it is unclear
133-
// is VSCode ever reads this file.
134-
// TODO: Properly implement the p7s file, and diverge from the other open
135-
// source implementation. Ideally this marketplace would match Microsoft's
136-
// marketplace API.
137-
func (s *Signature) Open(ctx context.Context, fp string) (fs.File, error) {
138-
if s.SigningEnabled() && strings.HasSuffix(filepath.Base(fp), SigzipFileExtension) {
139-
base := filepath.Base(fp)
140-
vsixPath := strings.TrimSuffix(base, SigzipFileExtension)
141-
142-
// hijack this request, sign the sig manifest
143-
manifest, err := s.Storage.Open(ctx, filepath.Join(filepath.Dir(fp), sigManifestName))
144-
if err != nil {
145-
// If this file is missing, it means the extension was added before
146-
// signatures were handled by the marketplace.
147-
// TODO: Generate the sig manifest payload and insert it?
148-
return nil, xerrors.Errorf("open signature manifest: %w", err)
149-
}
150-
defer manifest.Close()
151-
152-
manifestData, err := io.ReadAll(manifest)
153-
if err != nil {
154-
return nil, xerrors.Errorf("read signature manifest: %w", err)
155-
}
156-
157-
vsix, err := s.Storage.Open(ctx, filepath.Join(filepath.Dir(fp), vsixPath+".vsix"))
158-
if err != nil {
159-
// If this file is missing, it means the extension was added before
160-
// signatures were handled by the marketplace.
161-
// TODO: Generate the sig manifest payload and insert it?
162-
return nil, xerrors.Errorf("open signature manifest: %w", err)
163-
}
164-
defer vsix.Close()
128+
// - VSCode requires a signature payload to exist, but the content is optional
129+
// for linux users.
130+
// For windows (maybe mac?) users, the signature must be valid, and this
131+
// implementation will not work.
132+
func (s *Signature) FileServer() http.Handler {
133+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
134+
if s.SigningEnabled() && strings.HasSuffix(r.URL.Path, SigzipFileExtension) {
135+
// hijack this request, return an empty signature payload
136+
signed, err := extensionsign.IncludeEmptySignature()
137+
if err != nil {
138+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.ErrorResponse{
139+
Message: "Unable to generate empty signature for extension",
140+
Detail: err.Error(),
141+
RequestID: httpmw.RequestID(r),
142+
})
143+
return
144+
}
165145

166-
vsixData, err := io.ReadAll(vsix)
167-
if err != nil {
168-
return nil, xerrors.Errorf("read signature manifest: %w", err)
146+
rw.Header().Set("Content-Length", strconv.FormatInt(int64(len(signed)), 10))
147+
rw.WriteHeader(http.StatusOK)
148+
_, _ = rw.Write(signed)
149+
return
169150
}
170151

171-
// TODO: Fetch the VSIX payload from the storage
172-
signed, err := s.SigZip(ctx, vsixData, manifestData)
173-
if err != nil {
174-
return nil, xerrors.Errorf("sign and zip manifest: %w", err)
175-
}
176-
177-
f := mem.NewFileHandle(mem.CreateFile(fp))
178-
_, err = f.Write(signed)
179-
return f, err
180-
}
181-
182-
return s.Storage.Open(ctx, fp)
152+
s.Storage.FileServer().ServeHTTP(rw, r)
153+
})
183154
}
184155

185156
func (s *Signature) SigZip(ctx context.Context, vsix []byte, sigManifest []byte) ([]byte, error) {

storage/storage.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,9 @@ type Storage interface {
213213
// for verification purposes. Extra files can be included, but not required.
214214
// All extra files will be placed relative to the manifest outside the vsix.
215215
AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte, extra ...File) (string, error)
216-
// Open mirrors the fs.FS interface of Open, except with a context.
217-
// The Open should return files from the extension storage, and used for
218-
// serving extensions.
219-
Open(ctx context.Context, name string) (fs.File, error)
216+
// FileServer provides a handler for fetching extension repository files from
217+
// a client.
218+
FileServer() http.Handler
220219
// Manifest returns the manifest bytes for the provided extension. The
221220
// extension asset itself (the VSIX) will be included on the manifest even if
222221
// it does not exist on the manifest on disk.
@@ -239,17 +238,6 @@ type Storage interface {
239238
WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []Version) error) error
240239
}
241240

242-
// HTTPFileServer creates an http.Handler that serves files from the provided
243-
// storage.
244-
func HTTPFileServer(s Storage) http.Handler {
245-
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
246-
http.FileServerFS(&contextFs{
247-
ctx: r.Context(),
248-
open: s.Open,
249-
}).ServeHTTP(rw, r)
250-
})
251-
}
252-
253241
type File struct {
254242
RelativePath string
255243
Content []byte

storage/storage_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func testFileServer(t *testing.T, factory storageFactory) {
201201
req := httptest.NewRequest("GET", test.path, nil)
202202
rec := httptest.NewRecorder()
203203

204-
server := storage.HTTPFileServer(f.storage)
204+
server := f.storage.FileServer()
205205
server.ServeHTTP(rec, req)
206206

207207
resp := rec.Result()

testutil/mockstorage.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@ package testutil
33
import (
44
"context"
55
"errors"
6-
"io/fs"
76
"net/http"
87
"os"
9-
"path/filepath"
108
"sort"
119

12-
"github.com/spf13/afero/mem"
13-
1410
"github.com/coder/code-marketplace/storage"
1511
)
1612

@@ -26,15 +22,6 @@ func NewMockStorage() *MockStorage {
2622
func (s *MockStorage) AddExtension(ctx context.Context, manifest *storage.VSIXManifest, vsix []byte, extra ...storage.File) (string, error) {
2723
return "", errors.New("not implemented")
2824
}
29-
func (s *MockStorage) Open(ctx context.Context, path string) (fs.File, error) {
30-
if filepath.Base(path) == "nonexistent" {
31-
return nil, fs.ErrNotExist
32-
}
33-
34-
f := mem.NewFileHandle(mem.CreateFile(path))
35-
_, _ = f.Write([]byte("foobar"))
36-
return f, nil
37-
}
3825

3926
func (s *MockStorage) FileServer() http.Handler {
4027
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)