X Tutup
Skip to content

Commit 4e05db9

Browse files
committed
Add release upload command
1 parent c4f5d6d commit 4e05db9

File tree

5 files changed

+345
-6
lines changed

5 files changed

+345
-6
lines changed

pkg/cmd/release/create/create.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ func createRun(opts *CreateOptions) error {
8989
return api.HandleHTTPError(resp)
9090
}
9191

92-
if resp.StatusCode == http.StatusNoContent {
93-
return nil
94-
}
95-
9692
b, err := ioutil.ReadAll(resp.Body)
9793
if err != nil {
9894
return err

pkg/cmd/release/release.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package release
33
import (
44
cmdCreate "github.com/cli/cli/pkg/cmd/release/create"
55
cmdList "github.com/cli/cli/pkg/cmd/release/list"
6+
cmdUpload "github.com/cli/cli/pkg/cmd/release/upload"
67
cmdView "github.com/cli/cli/pkg/cmd/release/view"
78
"github.com/cli/cli/pkg/cmdutil"
89
"github.com/spf13/cobra"
@@ -22,6 +23,7 @@ func NewCmdRelease(f *cmdutil.Factory) *cobra.Command {
2223
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
2324
cmd.AddCommand(cmdList.NewCmdList(f, nil))
2425
cmd.AddCommand(cmdView.NewCmdView(f, nil))
26+
cmd.AddCommand(cmdUpload.NewCmdUpload(f, nil))
2527

2628
return cmd
2729
}

pkg/cmd/release/upload/http.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package upload
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"net/http"
8+
"net/url"
9+
10+
"github.com/cli/cli/api"
11+
"github.com/cli/cli/internal/ghinstance"
12+
"github.com/cli/cli/internal/ghrepo"
13+
)
14+
15+
type Release struct {
16+
UploadURL string `json:"upload_url"`
17+
Assets []ReleaseAsset
18+
}
19+
20+
type ReleaseAsset struct {
21+
Name string
22+
State string
23+
URL string
24+
}
25+
26+
func fetchRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) {
27+
path := fmt.Sprintf("repos/%s/%s/releases/tags/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), tagName)
28+
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
29+
req, err := http.NewRequest("GET", url, nil)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
req.Header.Set("Content-Type", "application/json; charset=utf-8")
35+
36+
resp, err := httpClient.Do(req)
37+
if err != nil {
38+
return nil, err
39+
}
40+
defer resp.Body.Close()
41+
42+
success := resp.StatusCode >= 200 && resp.StatusCode < 300
43+
if !success {
44+
return nil, api.HandleHTTPError(resp)
45+
}
46+
47+
b, err := ioutil.ReadAll(resp.Body)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
var release Release
53+
err = json.Unmarshal(b, &release)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
return &release, nil
59+
}
60+
61+
func uploadAsset(httpClient *http.Client, uploadURL string, asset AssetForUpload) (*ReleaseAsset, error) {
62+
u, err := url.Parse(uploadURL)
63+
if err != nil {
64+
return nil, err
65+
}
66+
params := u.Query()
67+
params.Set("name", asset.Name)
68+
params.Set("label", asset.Label)
69+
u.RawQuery = params.Encode()
70+
71+
req, err := http.NewRequest("POST", u.String(), asset.Data)
72+
if err != nil {
73+
return nil, err
74+
}
75+
req.ContentLength = asset.Size
76+
req.Header.Set("Content-Type", asset.MIMEType)
77+
78+
resp, err := httpClient.Do(req)
79+
if err != nil {
80+
return nil, err
81+
}
82+
defer resp.Body.Close()
83+
84+
success := resp.StatusCode >= 200 && resp.StatusCode < 300
85+
if !success {
86+
return nil, api.HandleHTTPError(resp)
87+
}
88+
89+
b, err := ioutil.ReadAll(resp.Body)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
var newAsset ReleaseAsset
95+
err = json.Unmarshal(b, &newAsset)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
return &newAsset, nil
101+
}
102+
103+
func deleteAsset(httpClient *http.Client, assetURL string) error {
104+
req, err := http.NewRequest("DELETE", assetURL, nil)
105+
if err != nil {
106+
return err
107+
}
108+
109+
resp, err := httpClient.Do(req)
110+
if err != nil {
111+
return err
112+
}
113+
defer resp.Body.Close()
114+
115+
success := resp.StatusCode >= 200 && resp.StatusCode < 300
116+
if !success {
117+
return api.HandleHTTPError(resp)
118+
}
119+
120+
return nil
121+
}

pkg/cmd/release/upload/upload.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package upload
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/cli/cli/internal/ghrepo"
12+
"github.com/cli/cli/pkg/cmdutil"
13+
"github.com/cli/cli/pkg/iostreams"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
type UploadOptions struct {
18+
HttpClient func() (*http.Client, error)
19+
IO *iostreams.IOStreams
20+
BaseRepo func() (ghrepo.Interface, error)
21+
22+
TagName string
23+
Assets []*AssetForUpload
24+
25+
// maximum number of simultaneous uploads
26+
Concurrency int
27+
OverwriteExisting bool
28+
}
29+
30+
type AssetForUpload struct {
31+
Name string
32+
Label string
33+
34+
Data io.ReadCloser
35+
Size int64
36+
MIMEType string
37+
38+
ExistingURL string
39+
}
40+
41+
func NewCmdUpload(f *cmdutil.Factory, runF func(*UploadOptions) error) *cobra.Command {
42+
opts := &UploadOptions{
43+
IO: f.IOStreams,
44+
HttpClient: f.HttpClient,
45+
}
46+
47+
cmd := &cobra.Command{
48+
Use: "upload <tag> <files>...",
49+
Short: "Upload assets to a release",
50+
Args: cobra.MinimumNArgs(2),
51+
RunE: func(cmd *cobra.Command, args []string) error {
52+
// support `-R, --repo` override
53+
opts.BaseRepo = f.BaseRepo
54+
55+
opts.TagName = args[0]
56+
57+
var err error
58+
opts.Assets, err = assetsFromArgs(args[1:])
59+
if err != nil {
60+
return err
61+
}
62+
63+
opts.Concurrency = 5
64+
65+
if runF != nil {
66+
return runF(opts)
67+
}
68+
return uploadRun(opts)
69+
},
70+
}
71+
72+
cmd.Flags().BoolVar(&opts.OverwriteExisting, "clobber", false, "Overwrite existing assets of the same name")
73+
74+
return cmd
75+
}
76+
77+
func assetsFromArgs(args []string) (assets []*AssetForUpload, err error) {
78+
for _, fn := range args {
79+
var f *os.File
80+
var fi os.FileInfo
81+
82+
f, err = os.Open(fn)
83+
if err != nil {
84+
return
85+
}
86+
fi, err = f.Stat()
87+
if err != nil {
88+
return
89+
}
90+
91+
assets = append(assets, &AssetForUpload{
92+
Data: f,
93+
Size: fi.Size(),
94+
Name: filepath.Base(fn),
95+
Label: "",
96+
// TODO: infer content type from file extension
97+
MIMEType: "application/octet-stream",
98+
})
99+
}
100+
return
101+
}
102+
103+
func uploadRun(opts *UploadOptions) error {
104+
httpClient, err := opts.HttpClient()
105+
if err != nil {
106+
return err
107+
}
108+
109+
baseRepo, err := opts.BaseRepo()
110+
if err != nil {
111+
return err
112+
}
113+
114+
release, err := fetchRelease(httpClient, baseRepo, opts.TagName)
115+
if err != nil {
116+
return err
117+
}
118+
119+
uploadURL := release.UploadURL
120+
if idx := strings.IndexRune(uploadURL, '{'); idx > 0 {
121+
uploadURL = uploadURL[:idx]
122+
}
123+
124+
var existingNames []string
125+
for _, a := range opts.Assets {
126+
for _, ea := range release.Assets {
127+
if ea.Name == a.Name {
128+
a.ExistingURL = ea.URL
129+
existingNames = append(existingNames, ea.Name)
130+
break
131+
}
132+
}
133+
}
134+
135+
if len(existingNames) > 0 && !opts.OverwriteExisting {
136+
return fmt.Errorf("asset under the same name already exists: %v", existingNames)
137+
}
138+
139+
opts.IO.StartProgressIndicator()
140+
err = concurrentUpload(httpClient, uploadURL, opts.Concurrency, opts.Assets)
141+
opts.IO.EndProgressIndicator()
142+
if err != nil {
143+
return err
144+
}
145+
146+
return nil
147+
}
148+
149+
func concurrentUpload(httpClient *http.Client, uploadURL string, numWorkers int, assets []*AssetForUpload) error {
150+
jobs := make(chan AssetForUpload, len(assets))
151+
results := make(chan error, len(assets))
152+
153+
for w := 1; w <= numWorkers; w++ {
154+
go func() {
155+
for a := range jobs {
156+
results <- uploadWithDelete(httpClient, uploadURL, a)
157+
}
158+
}()
159+
}
160+
161+
for _, a := range assets {
162+
jobs <- *a
163+
}
164+
close(jobs)
165+
166+
var uploadError error
167+
for i := 0; i < len(assets); i++ {
168+
if err := <-results; err != nil {
169+
uploadError = err
170+
}
171+
}
172+
return uploadError
173+
}
174+
175+
func uploadWithDelete(httpClient *http.Client, uploadURL string, a AssetForUpload) error {
176+
if a.ExistingURL != "" {
177+
err := deleteAsset(httpClient, a.ExistingURL)
178+
if err != nil {
179+
return err
180+
}
181+
}
182+
183+
defer a.Data.Close()
184+
_, err := uploadAsset(httpClient, uploadURL, a)
185+
return err
186+
}

0 commit comments

Comments
 (0)
X Tutup