Skip to content

Commit 39ee043

Browse files
committed
Add the ability to add extensions in bulk
1 parent 9284a8b commit 39ee043

File tree

4 files changed

+211
-51
lines changed

4 files changed

+211
-51
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Artifactory integration.
13+
- Bulk add from a directory. This only works when adding from a local directory
14+
and not from web URLs.
1315

1416
## [1.1.0](https://github.com/coder/code-marketplace/releases/tag/v1.1.0) - 2022-10-03
1517

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ like Cloudflare.
7070

7171
When hosting the marketplace behind a reverse proxy set either the `Forwarded`
7272
header or both the `X-Forwarded-Host` and `X-Forwarded-Proto` headers. These
73-
headers are used to generate absolute URIs to extension assets in API responses.
74-
One way to test this is to make a query and check one of the URIs in the
73+
headers are used to generate absolute URLs to extension assets in API responses.
74+
One way to test this is to make a query and check one of the URLs in the
7575
response:
7676

7777
```console
@@ -89,11 +89,11 @@ receive requests.
8989

9090
## Adding extensions
9191

92-
Extensions can be added to the marketplace by file or URL. The extensions
93-
directory does not need to be created beforehand.
92+
Extensions can be added to the marketplace by file, directory, or web URL.
9493

9594
```console
9695
./code-marketplace add extension.vsix [flags]
96+
./code-marketplace add extension-vsixs/ [flags]
9797
./code-marketplace add https://domain.tld/extension.vsix [flags]
9898
```
9999

cli/add.go

+89-46
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package cli
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
"path/filepath"
68
"strings"
79

810
"github.com/spf13/cobra"
11+
"golang.org/x/xerrors"
912

1013
"cdr.dev/slog"
1114
"cdr.dev/slog/sloggers/sloghuman"
@@ -27,6 +30,7 @@ func add() *cobra.Command {
2730
Example: strings.Join([]string{
2831
" marketplace add https://domain.tld/extension.vsix --extensions-dir ./extensions",
2932
" marketplace add extension.vsix --artifactory http://artifactory.server/artifactory --repo extensions",
33+
" marketplace add extension-vsixs/ --extensions-dir ./extensions",
3034
}, "\n"),
3135
Args: cobra.ExactArgs(1),
3236
RunE: func(cmd *cobra.Command, args []string) error {
@@ -52,62 +56,42 @@ func add() *cobra.Command {
5256
return err
5357
}
5458

55-
// Read in the extension. In the future we might support stdin as well.
56-
vsix, err := storage.ReadVSIX(ctx, args[0])
59+
stat, err := os.Stat(args[0])
5760
if err != nil {
5861
return err
5962
}
6063

61-
// The manifest is required to know where to place the extension since it
62-
// is unsafe to rely on the file name or URI.
63-
manifest, err := storage.ReadVSIXManifest(vsix)
64-
if err != nil {
65-
return err
66-
}
67-
68-
location, err := store.AddExtension(ctx, manifest, vsix)
69-
if err != nil {
70-
return err
71-
}
72-
73-
deps := []string{}
74-
pack := []string{}
75-
for _, prop := range manifest.Metadata.Properties.Property {
76-
if prop.Value == "" {
77-
continue
64+
var summary []string
65+
var failed []string
66+
if stat.IsDir() {
67+
files, err := os.ReadDir(args[0])
68+
if err != nil {
69+
return err
7870
}
79-
switch prop.ID {
80-
case storage.DependencyPropertyType:
81-
deps = append(deps, strings.Split(prop.Value, ",")...)
82-
case storage.PackPropertyType:
83-
pack = append(pack, strings.Split(prop.Value, ",")...)
84-
}
85-
}
86-
87-
depCount := len(deps)
88-
id := storage.ExtensionIDFromManifest(manifest)
89-
summary := []string{
90-
fmt.Sprintf("Unpacked %s to %s", id, location),
91-
fmt.Sprintf("%s has %s", id, util.Plural(depCount, "dependency", "dependencies")),
92-
}
93-
94-
if depCount > 0 {
95-
for _, id := range deps {
96-
summary = append(summary, fmt.Sprintf(" - %s", id))
97-
}
98-
}
99-
100-
packCount := len(pack)
101-
if packCount > 0 {
102-
summary = append(summary, fmt.Sprintf("%s is in a pack with %s", id, util.Plural(packCount, "other extension", "")))
103-
for _, id := range pack {
104-
summary = append(summary, fmt.Sprintf(" - %s", id))
71+
for _, file := range files {
72+
s, err := doAdd(ctx, filepath.Join(args[0], file.Name()), store)
73+
if err != nil {
74+
failed = append(failed, file.Name())
75+
summary = append(summary, fmt.Sprintf("Failed to unpack %s: %s", file.Name(), err.Error()))
76+
} else {
77+
summary = append(summary, s...)
78+
}
10579
}
10680
} else {
107-
summary = append(summary, fmt.Sprintf("%s is not in a pack", id))
81+
summary, err = doAdd(ctx, args[0], store)
82+
if err != nil {
83+
return err
84+
}
10885
}
10986

11087
_, err = fmt.Fprintln(cmd.OutOrStdout(), strings.Join(summary, "\n"))
88+
failedCount := len(failed)
89+
if failedCount > 0 {
90+
return xerrors.Errorf(
91+
"Failed to add %s: %s",
92+
util.Plural(failedCount, "extension", ""),
93+
strings.Join(failed, ", "))
94+
}
11195
return err
11296
},
11397
}
@@ -118,3 +102,62 @@ func add() *cobra.Command {
118102

119103
return cmd
120104
}
105+
106+
func doAdd(ctx context.Context, source string, store storage.Storage) ([]string, error) {
107+
// Read in the extension. In the future we might support stdin as well.
108+
vsix, err := storage.ReadVSIX(ctx, source)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
// The manifest is required to know where to place the extension since it
114+
// is unsafe to rely on the file name or URI.
115+
manifest, err := storage.ReadVSIXManifest(vsix)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
location, err := store.AddExtension(ctx, manifest, vsix)
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
deps := []string{}
126+
pack := []string{}
127+
for _, prop := range manifest.Metadata.Properties.Property {
128+
if prop.Value == "" {
129+
continue
130+
}
131+
switch prop.ID {
132+
case storage.DependencyPropertyType:
133+
deps = append(deps, strings.Split(prop.Value, ",")...)
134+
case storage.PackPropertyType:
135+
pack = append(pack, strings.Split(prop.Value, ",")...)
136+
}
137+
}
138+
139+
depCount := len(deps)
140+
id := storage.ExtensionIDFromManifest(manifest)
141+
summary := []string{
142+
fmt.Sprintf("Unpacked %s to %s", id, location),
143+
fmt.Sprintf(" - %s has %s", id, util.Plural(depCount, "dependency", "dependencies")),
144+
}
145+
146+
if depCount > 0 {
147+
for _, id := range deps {
148+
summary = append(summary, fmt.Sprintf(" - %s", id))
149+
}
150+
}
151+
152+
packCount := len(pack)
153+
if packCount > 0 {
154+
summary = append(summary, fmt.Sprintf(" - %s is in a pack with %s", id, util.Plural(packCount, "other extension", "")))
155+
for _, id := range pack {
156+
summary = append(summary, fmt.Sprintf(" - %s", id))
157+
}
158+
} else {
159+
summary = append(summary, fmt.Sprintf(" - %s is not in a pack", id))
160+
}
161+
162+
return summary, nil
163+
}

cli/add_test.go

+116-1
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ package cli_test
22

33
import (
44
"bytes"
5+
"fmt"
6+
"os"
7+
"path/filepath"
58
"testing"
69

710
"github.com/stretchr/testify/require"
811

912
"github.com/coder/code-marketplace/cli"
13+
"github.com/coder/code-marketplace/storage"
14+
"github.com/coder/code-marketplace/testutil"
1015
)
1116

12-
func TestAdd(t *testing.T) {
17+
func TestAddHelp(t *testing.T) {
1318
t.Parallel()
1419

1520
cmd := cli.Root()
@@ -23,3 +28,113 @@ func TestAdd(t *testing.T) {
2328
output := buf.String()
2429
require.Contains(t, output, "Add an extension", "has help")
2530
}
31+
32+
func TestAdd(t *testing.T) {
33+
t.Parallel()
34+
35+
tests := []struct {
36+
// error is the expected error.
37+
error string
38+
// extensions are extensions to add. Use for success cases.
39+
extensions []testutil.Extension
40+
// name is the name of the test.
41+
name string
42+
// vsixes contains raw bytes of extensions to add. Use for failure cases.
43+
vsixes [][]byte
44+
}{
45+
{
46+
name: "OK",
47+
extensions: []testutil.Extension{testutil.Extensions[0]},
48+
},
49+
{
50+
name: "InvalidVSIX",
51+
error: "not a valid zip",
52+
vsixes: [][]byte{[]byte{}},
53+
},
54+
{
55+
name: "BulkOK",
56+
extensions: []testutil.Extension{
57+
testutil.Extensions[0],
58+
testutil.Extensions[1],
59+
testutil.Extensions[2],
60+
testutil.Extensions[3],
61+
},
62+
},
63+
{
64+
name: "BulkInvalid",
65+
error: "Failed to add 2 extensions: 0.vsix, 1.vsix",
66+
extensions: []testutil.Extension{
67+
testutil.Extensions[0],
68+
testutil.Extensions[1],
69+
testutil.Extensions[2],
70+
testutil.Extensions[3],
71+
},
72+
vsixes: [][]byte{
73+
[]byte{},
74+
[]byte("foo"),
75+
},
76+
},
77+
}
78+
79+
for _, test := range tests {
80+
test := test
81+
t.Run(test.name, func(t *testing.T) {
82+
t.Parallel()
83+
84+
extdir := t.TempDir()
85+
count := 0
86+
create := func(vsix []byte) {
87+
source := filepath.Join(extdir, fmt.Sprintf("%d.vsix", count))
88+
err := os.WriteFile(source, vsix, 0o644)
89+
require.NoError(t, err)
90+
count++
91+
}
92+
for _, vsix := range test.vsixes {
93+
create(vsix)
94+
}
95+
for _, ext := range test.extensions {
96+
create(testutil.CreateVSIXFromExtension(t, ext))
97+
}
98+
99+
// With multiple extensions use bulk add by pointing to the directory
100+
// otherwise point to the vsix file.
101+
source := extdir
102+
if count == 1 {
103+
source = filepath.Join(extdir, "0.vsix")
104+
}
105+
106+
cmd := cli.Root()
107+
args := []string{"add", source, "--extensions-dir", extdir}
108+
cmd.SetArgs(args)
109+
buf := new(bytes.Buffer)
110+
cmd.SetOutput(buf)
111+
112+
err := cmd.Execute()
113+
output := buf.String()
114+
115+
if test.error != "" {
116+
require.Error(t, err)
117+
require.Regexp(t, test.error, err.Error())
118+
} else {
119+
require.NoError(t, err)
120+
}
121+
// Should list all the extensions that worked.
122+
for _, ext := range test.extensions {
123+
// Should exist on disk.
124+
dest := filepath.Join(extdir, ext.Publisher, ext.Name, ext.LatestVersion)
125+
_, err := os.Stat(dest)
126+
require.NoError(t, err)
127+
// Should tell you where it went.
128+
id := storage.ExtensionID(ext.Publisher, ext.Name, ext.LatestVersion)
129+
require.Contains(t, output, fmt.Sprintf("Unpacked %s to %s", id, dest))
130+
// Should mention the dependencies and pack.
131+
require.Contains(t, output, fmt.Sprintf("%s has %d dep", id, len(ext.Dependencies)))
132+
if len(ext.Pack) > 0 {
133+
require.Contains(t, output, fmt.Sprintf("%s is in a pack with %d other", id, len(ext.Pack)))
134+
} else {
135+
require.Contains(t, output, fmt.Sprintf("%s is not in a pack", id))
136+
}
137+
}
138+
})
139+
}
140+
}

0 commit comments

Comments
 (0)