Skip to content

Commit 429c1f8

Browse files
feat(feeds): add feeds import cli support
1 parent f6d466a commit 429c1f8

File tree

7 files changed

+413
-6
lines changed

7 files changed

+413
-6
lines changed

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.23
44

55
require (
66
github.com/AlecAivazis/survey/v2 v2.3.4
7+
github.com/GetStream/getstream-go/v4 v4.0.4
78
github.com/GetStream/stream-chat-go/v8 v8.3.0
89
github.com/MakeNowJust/heredoc v1.0.0
910
github.com/cheynewallace/tabby v1.1.1
@@ -14,6 +15,8 @@ require (
1415
github.com/spf13/viper v1.11.0
1516
)
1617

18+
require github.com/golang-jwt/jwt/v5 v5.2.1
19+
1720
require (
1821
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
1922
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -38,12 +41,12 @@ require (
3841
github.com/spf13/afero v1.8.2 // indirect
3942
github.com/spf13/cast v1.5.0 // indirect
4043
github.com/spf13/jwalterweatherman v1.1.0 // indirect
41-
github.com/stretchr/testify v1.7.1
44+
github.com/stretchr/testify v1.9.0
4245
github.com/subosito/gotenv v1.2.0 // indirect
4346
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
4447
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
4548
golang.org/x/text v0.3.8 // indirect
4649
gopkg.in/ini.v1 v1.66.4 // indirect
4750
gopkg.in/yaml.v2 v2.4.0 // indirect
48-
gopkg.in/yaml.v3 v3.0.0 // indirect
51+
gopkg.in/yaml.v3 v3.0.1 // indirect
4952
)

go.sum

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ github.com/AlecAivazis/survey/v2 v2.3.4 h1:pchTU9rsLUSvWEl2Aq9Pv3k0IE2fkqtGxazsk
4040
github.com/AlecAivazis/survey/v2 v2.3.4/go.mod h1:hrV6Y/kQCLhIZXGcriDCUBtB3wnN7156gMXJ3+b23xM=
4141
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4242
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
43+
github.com/GetStream/getstream-go/v4 v4.0.4 h1:nkD/s42M+06eOpa4m8n38ukumqxX4LlCAEzrRU27kmw=
44+
github.com/GetStream/getstream-go/v4 v4.0.4/go.mod h1:A5hd7TxT8nSZBWazr4403j05dqP0F8pt7vi8YAJj+9M=
4345
github.com/GetStream/stream-chat-go/v8 v8.3.0 h1:mFtQZ0PkcCXMPjCDlnZcex3roOvE+UOaxBcNdq3o62s=
4446
github.com/GetStream/stream-chat-go/v8 v8.3.0/go.mod h1:frj3A1yv9mjyWlGNwaZKnXcX9JYYTPWSDqzyOFeHPac=
4547
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -81,6 +83,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
8183
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
8284
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
8385
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
86+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
87+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
8488
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
8589
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
8690
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -134,6 +138,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
134138
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
135139
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
136140
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
141+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
142+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
137143
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
138144
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
139145
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -149,6 +155,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
149155
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
150156
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
151157
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
158+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
159+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
152160
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
153161
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
154162
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -219,8 +227,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
219227
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
220228
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
221229
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
222-
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
223230
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
231+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
232+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
224233
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
225234
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
226235
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -529,8 +538,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
529538
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
530539
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
531540
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
532-
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
533-
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
541+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
542+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
534543
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
535544
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
536545
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -540,4 +549,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
540549
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
541550
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
542551
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
543-
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
552+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

pkg/cmd/feeds/imports/imports.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package imports
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
"time"
14+
15+
getstream "github.com/GetStream/getstream-go/v4"
16+
"github.com/MakeNowJust/heredoc"
17+
"github.com/golang-jwt/jwt/v5"
18+
"github.com/spf13/cobra"
19+
20+
"github.com/GetStream/stream-cli/pkg/config"
21+
"github.com/GetStream/stream-cli/pkg/utils"
22+
)
23+
24+
func NewCmds() []*cobra.Command {
25+
return []*cobra.Command{
26+
uploadCmd(),
27+
getCmd(),
28+
listCmd(),
29+
}
30+
}
31+
32+
// getImportV2Task works around a server-side issue where GET /api/v2/imports/v2/{id}
33+
// requires a JSON body. The SDK sends no body for GET requests, causing a 400 error.
34+
func getImportV2Task(ctx context.Context, app *config.App, id string) (map[string]any, error) {
35+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"server": true})
36+
authToken, err := token.SignedString([]byte(app.AccessSecretKey))
37+
if err != nil {
38+
return nil, fmt.Errorf("creating auth token: %w", err)
39+
}
40+
41+
baseURL := app.ChatURL
42+
if baseURL == "" {
43+
baseURL = config.DefaultChatEdgeURL
44+
}
45+
46+
reqURL := fmt.Sprintf("%s/api/v2/imports/v2/%s?api_key=%s",
47+
baseURL, url.PathEscape(id), url.QueryEscape(app.AccessKey))
48+
49+
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, strings.NewReader("{}"))
50+
if err != nil {
51+
return nil, err
52+
}
53+
req.Header.Set("Content-Type", "application/json")
54+
req.Header.Set("Authorization", authToken)
55+
req.Header.Set("Stream-Auth-Type", "jwt")
56+
57+
resp, err := http.DefaultClient.Do(req)
58+
if err != nil {
59+
return nil, err
60+
}
61+
defer resp.Body.Close()
62+
63+
body, err := io.ReadAll(resp.Body)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
if resp.StatusCode >= 400 {
69+
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
70+
}
71+
72+
var result map[string]any
73+
if err := json.Unmarshal(body, &result); err != nil {
74+
return nil, err
75+
}
76+
return result, nil
77+
}
78+
79+
func uploadToS3(ctx context.Context, filename, url string) error {
80+
data, err := os.Open(filename)
81+
if err != nil {
82+
return err
83+
}
84+
defer data.Close()
85+
86+
stat, err := data.Stat()
87+
if err != nil {
88+
return err
89+
}
90+
91+
req, err := http.NewRequestWithContext(ctx, "PUT", url, data)
92+
if err != nil {
93+
return err
94+
}
95+
req.Header.Set("Content-Type", "application/json")
96+
req.ContentLength = stat.Size()
97+
98+
resp, err := http.DefaultClient.Do(req)
99+
if err != nil {
100+
return err
101+
}
102+
defer resp.Body.Close()
103+
104+
return nil
105+
}
106+
107+
func uploadCmd() *cobra.Command {
108+
cmd := &cobra.Command{
109+
Use: "upload-import [filename] --output-format [json|tree]",
110+
Short: "Upload an import for Feeds",
111+
Example: heredoc.Doc(`
112+
# Uploads a feeds import and prints it as JSON
113+
$ stream-cli feeds upload-import data.json
114+
115+
# Uploads a feeds import and prints it as a browsable tree
116+
$ stream-cli feeds upload-import data.json --output-format tree
117+
`),
118+
Args: cobra.ExactArgs(1),
119+
RunE: func(cmd *cobra.Command, args []string) error {
120+
client, err := config.GetConfig(cmd).GetFeedsClient(cmd)
121+
if err != nil {
122+
return err
123+
}
124+
125+
filename := args[0]
126+
127+
createURLResp, err := client.CreateImportURL(cmd.Context(), &getstream.CreateImportURLRequest{
128+
Filename: getstream.PtrTo(filepath.Base(filename)),
129+
})
130+
if err != nil {
131+
return err
132+
}
133+
if err := uploadToS3(cmd.Context(), filename, createURLResp.Data.UploadUrl); err != nil {
134+
return err
135+
}
136+
137+
bucket, region, err := utils.S3BucketAndRegionFromUploadURL(createURLResp.Data.UploadUrl)
138+
if err != nil {
139+
return err
140+
}
141+
dir := createURLResp.Data.Path
142+
143+
skipReferencesCheck, err := cmd.Flags().GetBool("skip-references-check")
144+
if err != nil {
145+
return err
146+
}
147+
148+
resp, err := client.CreateImportV2Task(cmd.Context(), &getstream.CreateImportV2TaskRequest{
149+
Product: "feeds",
150+
Settings: getstream.ImportV2TaskSettings{
151+
SkipReferencesCheck: getstream.PtrTo(skipReferencesCheck),
152+
S3: &getstream.ImportV2TaskSettingsS3{
153+
Bucket: getstream.PtrTo(bucket),
154+
Dir: &dir,
155+
Region: getstream.PtrTo(region),
156+
},
157+
},
158+
})
159+
if err != nil {
160+
return err
161+
}
162+
163+
return utils.PrintObject(cmd, resp.Data)
164+
},
165+
}
166+
167+
fl := cmd.Flags()
168+
fl.StringP("output-format", "o", "json", "[optional] Output format. Can be json or tree")
169+
fl.Bool("skip-references-check", false, "[optional] Skip references validation for the import (default false)")
170+
171+
return cmd
172+
}
173+
174+
func getCmd() *cobra.Command {
175+
cmd := &cobra.Command{
176+
Use: "get-import [task-id] --output-format [json|tree] --watch",
177+
Short: "Get a feeds import task",
178+
Example: heredoc.Doc(`
179+
# Returns a feeds import and prints it as JSON
180+
$ stream-cli feeds get-import dcb6e366-93ec-4e52-af6f-b0c030ad5272
181+
182+
# Returns a feeds import and watches for completion
183+
$ stream-cli feeds get-import dcb6e366-93ec-4e52-af6f-b0c030ad5272 --watch
184+
`),
185+
Args: cobra.ExactArgs(1),
186+
RunE: func(cmd *cobra.Command, args []string) error {
187+
cfg := config.GetConfig(cmd)
188+
app, err := cfg.GetDefaultAppOrExplicit(cmd)
189+
if err != nil {
190+
return err
191+
}
192+
193+
id := args[0]
194+
watch, _ := cmd.Flags().GetBool("watch")
195+
196+
for {
197+
result, err := getImportV2Task(cmd.Context(), app, id)
198+
if err != nil {
199+
return err
200+
}
201+
202+
err = utils.PrintObject(cmd, result)
203+
if err != nil {
204+
return err
205+
}
206+
207+
if !watch {
208+
break
209+
}
210+
211+
time.Sleep(5 * time.Second)
212+
}
213+
214+
return nil
215+
},
216+
}
217+
218+
fl := cmd.Flags()
219+
fl.BoolP("watch", "w", false, "[optional] Keep polling the import to track its status")
220+
fl.StringP("output-format", "o", "json", "[optional] Output format. Can be json or tree")
221+
222+
return cmd
223+
}
224+
225+
func listCmd() *cobra.Command {
226+
cmd := &cobra.Command{
227+
Use: "list-imports --output-format [json|tree] --state [1-4]",
228+
Short: "List feeds import tasks",
229+
Example: heredoc.Doc(`
230+
# List all feeds imports as json (default)
231+
$ stream-cli feeds list-imports
232+
233+
# List feeds imports filtered by state
234+
$ stream-cli feeds list-imports --state 2
235+
236+
# List all feeds imports as browsable tree
237+
$ stream-cli feeds list-imports --output-format tree
238+
`),
239+
RunE: func(cmd *cobra.Command, args []string) error {
240+
client, err := config.GetConfig(cmd).GetFeedsClient(cmd)
241+
if err != nil {
242+
return err
243+
}
244+
245+
state, _ := cmd.Flags().GetInt("state")
246+
247+
req := &getstream.ListImportV2TasksRequest{}
248+
if state > 0 {
249+
req.State = getstream.PtrTo(state)
250+
}
251+
252+
resp, err := client.ListImportV2Tasks(cmd.Context(), req)
253+
if err != nil {
254+
return err
255+
}
256+
257+
return utils.PrintObject(cmd, resp.Data)
258+
},
259+
}
260+
261+
fl := cmd.Flags()
262+
fl.IntP("state", "s", 0, "[optional] Filter imports by state (1-4)")
263+
fl.StringP("output-format", "o", "json", "[optional] Output format. Can be json or tree")
264+
265+
return cmd
266+
}

pkg/cmd/feeds/root.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package feeds
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/GetStream/stream-cli/pkg/cmd/feeds/imports"
7+
)
8+
9+
func NewRootCmd() *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "feeds",
12+
Short: "Allows you to interact with your Feeds applications",
13+
}
14+
15+
cmd.AddCommand(imports.NewCmds()...)
16+
17+
return cmd
18+
}

pkg/cmd/root/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/GetStream/stream-cli/pkg/cmd/chat"
1010
cfgCmd "github.com/GetStream/stream-cli/pkg/cmd/config"
11+
feedsCmd "github.com/GetStream/stream-cli/pkg/cmd/feeds"
1112
"github.com/GetStream/stream-cli/pkg/config"
1213
"github.com/GetStream/stream-cli/pkg/version"
1314
)
@@ -39,6 +40,7 @@ func NewCmd() *cobra.Command {
3940
root.AddCommand(
4041
cfgCmd.NewRootCmd(),
4142
chat.NewRootCmd(),
43+
feedsCmd.NewRootCmd(),
4244
)
4345

4446
cobra.OnInitialize(config.GetInitConfig(root, cfgPath))

0 commit comments

Comments
 (0)