Skip to content

Commit 1d8b69a

Browse files
authored
chore: revert back to FileServer to prevent bytes in memory (#87)
1 parent cd7ab5f commit 1d8b69a

File tree

9 files changed

+58
-100
lines changed

9 files changed

+58
-100
lines changed

api/api.go

+1-1
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

+4-1
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

+4-6
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

+18-32
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

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"io/fs"
87
"net/http"
98
"os"
109
"path/filepath"
@@ -142,8 +141,8 @@ func (s *Local) AddExtension(ctx context.Context, manifest *VSIXManifest, vsix [
142141
return dir, nil
143142
}
144143

145-
func (s *Local) Open(_ context.Context, fp string) (fs.File, error) {
146-
return http.Dir(s.extdir).Open(fp)
144+
func (s *Local) FileServer() http.Handler {
145+
return http.FileServer(http.Dir(s.extdir))
147146
}
148147

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

storage/signature.go

+25-18
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ package storage
22

33
import (
44
"context"
5-
"io/fs"
6-
"path/filepath"
5+
"net/http"
6+
"strconv"
77
"strings"
88

9-
"github.com/spf13/afero/mem"
10-
"golang.org/x/xerrors"
11-
129
"cdr.dev/slog"
10+
"github.com/coder/code-marketplace/api/httpapi"
11+
"github.com/coder/code-marketplace/api/httpmw"
1312

1413
"github.com/coder/code-marketplace/extensionsign"
1514
)
@@ -70,7 +69,7 @@ func (s *Signature) Manifest(ctx context.Context, publisher, name string, versio
7069
return manifest, nil
7170
}
7271

73-
// Open will intercept requests for signed extensions payload.
72+
// FileServer will intercept requests for signed extensions payload.
7473
// It does this by looking for 'SigzipFileExtension' or p7s.sig.
7574
//
7675
// The signed payload is completely empty. Nothing it actually signed.
@@ -85,18 +84,26 @@ func (s *Signature) Manifest(ctx context.Context, publisher, name string, versio
8584
// for linux users.
8685
// For windows users, the signature must be valid, and this implementation
8786
// will not work.
88-
func (s *Signature) Open(ctx context.Context, fp string) (fs.File, error) {
89-
if s.SigningEnabled() && strings.HasSuffix(filepath.Base(fp), SigzipFileExtension) {
90-
// hijack this request, return an empty signature payload
91-
signed, err := extensionsign.IncludeEmptySignature()
92-
if err != nil {
93-
return nil, xerrors.Errorf("sign and zip manifest: %w", err)
94-
}
87+
func (s *Signature) FileServer() http.Handler {
88+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
89+
if s.SigningEnabled() && strings.HasSuffix(r.URL.Path, SigzipFileExtension) {
90+
// hijack this request, return an empty signature payload
91+
signed, err := extensionsign.IncludeEmptySignature()
92+
if err != nil {
93+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.ErrorResponse{
94+
Message: "Unable to generate empty signature for extension",
95+
Detail: err.Error(),
96+
RequestID: httpmw.RequestID(r),
97+
})
98+
return
99+
}
95100

96-
f := mem.NewFileHandle(mem.CreateFile(fp))
97-
_, err = f.Write(signed)
98-
return f, err
99-
}
101+
rw.Header().Set("Content-Length", strconv.FormatInt(int64(len(signed)), 10))
102+
rw.WriteHeader(http.StatusOK)
103+
_, _ = rw.Write(signed)
104+
return
105+
}
100106

101-
return s.Storage.Open(ctx, fp)
107+
s.Storage.FileServer().ServeHTTP(rw, r)
108+
})
102109
}

storage/storage.go

+3-25
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/xml"
77
"fmt"
88
"io"
9-
"io/fs"
109
"net/http"
1110
"os"
1211
"regexp"
@@ -211,10 +210,9 @@ type Storage interface {
211210
// for verification purposes. Extra files can be included, but not required.
212211
// All extra files will be placed relative to the manifest outside the vsix.
213212
AddExtension(ctx context.Context, manifest *VSIXManifest, vsix []byte, extra ...File) (string, error)
214-
// Open mirrors the fs.FS interface of Open, except with a context.
215-
// The Open should return files from the extension storage, and used for
216-
// serving extensions.
217-
Open(ctx context.Context, name string) (fs.File, error)
213+
// FileServer provides a handler for fetching extension repository files from
214+
// a client.
215+
FileServer() http.Handler
218216
// Manifest returns the manifest bytes for the provided extension. The
219217
// extension asset itself (the VSIX) will be included on the manifest even if
220218
// it does not exist on the manifest on disk.
@@ -237,17 +235,6 @@ type Storage interface {
237235
WalkExtensions(ctx context.Context, fn func(manifest *VSIXManifest, versions []Version) error) error
238236
}
239237

240-
// HTTPFileServer creates an http.Handler that serves files from the provided
241-
// storage.
242-
func HTTPFileServer(s Storage) http.Handler {
243-
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
244-
http.FileServerFS(&contextFs{
245-
ctx: r.Context(),
246-
open: s.Open,
247-
}).ServeHTTP(rw, r)
248-
})
249-
}
250-
251238
type File struct {
252239
RelativePath string
253240
Content []byte
@@ -442,12 +429,3 @@ func ParseExtensionID(id string) (string, string, string, error) {
442429
}
443430
return match[0][1], match[0][2], match[0][3], nil
444431
}
445-
446-
type contextFs struct {
447-
ctx context.Context
448-
open func(ctx context.Context, name string) (fs.File, error)
449-
}
450-
451-
func (c *contextFs) Open(name string) (fs.File, error) {
452-
return c.open(c.ctx, name)
453-
}

storage/storage_test.go

+1-1
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

-13
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)