From d148aedf1446be3b12365be61db419fa29b0109f Mon Sep 17 00:00:00 2001 From: tcnksm Date: Thu, 9 Oct 2014 01:44:32 +0900 Subject: [PATCH] Add new flag --replace --- asset.go | 51 +++++++++++++++++++++++++++++++++++++++++ asset_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ delete.go | 7 ++++++ delete_test.go | 14 +++++++++++- main.go | 52 +++++++++++++++++++++++++++++++++++------- request.go | 49 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 asset.go create mode 100644 asset_test.go diff --git a/asset.go b/asset.go new file mode 100644 index 0000000..52be0b3 --- /dev/null +++ b/asset.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" +) + +type Assets struct { + Name string `json:"name"` + AssetId int `json:"id"` +} + +type DeleteTarget Assets + +const ( + // GET /repos/:owner/:repo/releases/:id/assets + LIST_ASSET_URL = "https://api.github.com/repos/%s/%s/releases/%d/assets" +) + +func listAssetsURL(info *Info) string { + return fmt.Sprintf(LIST_ASSET_URL, info.OwnerName, info.RepoName, info.ID) +} + +// DeleteTargets extract asset IDs which is already uploaded +// It decide `already uploaded` based on filename to upload +func SearchDeleteTargets(r io.Reader, uploads []string) ([]DeleteTarget, error) { + + targets := []DeleteTarget{} + body, err := ioutil.ReadAll(r) + if err != nil { + return targets, err + } + + var assetsUploaded []Assets + err = json.Unmarshal(body, &assetsUploaded) + if err != nil { + return targets, err + } + + for _, upload := range uploads { + for _, asset := range assetsUploaded { + if upload == asset.Name { + targets = append(targets, + DeleteTarget{Name: asset.Name, AssetId: asset.AssetId}) + } + } + } + return targets, nil +} diff --git a/asset_test.go b/asset_test.go new file mode 100644 index 0000000..b3c1faa --- /dev/null +++ b/asset_test.go @@ -0,0 +1,61 @@ +package main + +import ( + . "github.com/onsi/gomega" + "strings" + "testing" +) + +func TestListAssetURL(t *testing.T) { + RegisterTestingT(t) + + info := &Info{ + OwnerName: "tcnksm", + RepoName: "ghr", + ID: 1234, + } + + url := listAssetsURL(info) + Expect(url).To(Equal("https://api.github.com/repos/tcnksm/ghr/releases/1234/assets")) +} + +func TestExtractDeleteTarget(t *testing.T) { + RegisterTestingT(t) + + uploads := []string{"example1.zip", "example3.zip"} + + json := ` +[ + { + "url": "https://api.github.com/repos/octocat/Hello-World/releases/assets/1", + "browser_download_url": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/example1.zip", + "id": 1, + "name": "example1.zip", + "label": "short description", + "state": "uploaded" + }, + { + "url": "https://api.github.com/repos/octocat/Hello-World/releases/assets/2", + "browser_download_url": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/example2.zip", + "id": 2, + "name": "example2.zip", + "label": "short description", + "state": "uploaded" + }, + { + "url": "https://api.github.com/repos/octocat/Hello-World/releases/assets/3", + "browser_download_url": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/example3.zip", + "id": 3, + "name": "example3.zip", + "label": "short description", + "state": "uploaded" + } +] +` + targets, err := SearchDeleteTargets(strings.NewReader(json), uploads) + Expect(err).NotTo(HaveOccurred()) + Expect(targets).To( + Equal([]DeleteTarget{ + {Name: "example1.zip", AssetId: 1}, + {Name: "example3.zip", AssetId: 3}})) +} diff --git a/delete.go b/delete.go index f26b491..83e1c35 100644 --- a/delete.go +++ b/delete.go @@ -7,8 +7,15 @@ import ( const ( // DELETE /repos/:owner/:repo/releases/:id DELETE_RELEASE_URL = "https://api.github.com/repos/%s/%s/releases/%d" + + // DELETE /repos/:owner/:repo/releases/assets/:id + DELETE_ASSET_URL = "https://api.github.com/repos/%s/%s/releases/assets/%d" ) func deleteReleaseURL(info *Info) string { return fmt.Sprintf(DELETE_RELEASE_URL, info.OwnerName, info.RepoName, info.ID) } + +func deleteAssetURL(info *Info, assetId int) string { + return fmt.Sprintf(DELETE_ASSET_URL, info.OwnerName, info.RepoName, assetId) +} diff --git a/delete_test.go b/delete_test.go index 07ea4ce..6799db9 100644 --- a/delete_test.go +++ b/delete_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestDeleteURL(t *testing.T) { +func TestDeleteReleaseURL(t *testing.T) { RegisterTestingT(t) info := &Info{ @@ -17,3 +17,15 @@ func TestDeleteURL(t *testing.T) { url := deleteReleaseURL(info) Expect(url).To(Equal("https://api.github.com/repos/taichi/tool/releases/123")) } + +func TestDeleteAssetURL(t *testing.T) { + RegisterTestingT(t) + + info := &Info{ + OwnerName: "taichi", + RepoName: "tool", + } + + url := deleteAssetURL(info, 12345) + Expect(url).To(Equal("https://api.github.com/repos/taichi/tool/releases/assets/12345")) +} diff --git a/main.go b/main.go index fad47c7..dee8b11 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ var ( repo = flag.String([]string{"r", "-repository"}, "", "Repository name") token = flag.String([]string{"t", "-token"}, "", "Github API Token") parallel = flag.Int([]string{"p", "--parallel"}, -1, "Parallelization factor") + flReplace = flag.Bool([]string{"-replace"}, false, "Replace asset if target is already uploaded") flDelete = flag.Bool([]string{"-delete"}, false, "Delete release if it exists") flDraft = flag.Bool([]string{"-draft"}, false, "Create unpublised release") flPrerelease = flag.Bool([]string{"-prerelease"}, false, "Create prerelease") @@ -58,6 +59,16 @@ func artifacts(path string) ([]string, error) { return files, nil } +func artifactNames(artifacts []string) []string { + names := []string{} + for _, artifact := range artifacts { + if f, err := os.Stat(artifact); err == nil { + names = append(names, f.Name()) + } + } + return names +} + func main() { // call ghrMain in a separate function // so that it can use defer and have them @@ -92,12 +103,6 @@ func ghrMain() int { tag := flag.Arg(0) inputPath := flag.Arg(1) - // Limit amount of parallelism - // by number of logic CPU - if *parallel <= 0 { - *parallel = runtime.NumCPU() - } - if *token == "" { *token = os.Getenv("GITHUB_TOKEN") if *token == "" { @@ -173,6 +178,12 @@ func ghrMain() int { return 1 } + // Limit amount of parallelism + // by number of logic CPU + if *parallel <= 0 { + *parallel = runtime.NumCPU() + } + // Use CPU efficiently cpu := runtime.NumCPU() runtime.GOMAXPROCS(cpu) @@ -181,6 +192,30 @@ func ghrMain() int { var wg sync.WaitGroup errors := make([]string, 0) semaphore := make(chan int, *parallel) + + if *flReplace { + deleteTargets, err := GetDeleteTargets(info, artifactNames(files)) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + } + for _, target := range deleteTargets { + wg.Add(1) + go func(target DeleteTarget) { + defer wg.Done() + semaphore <- 1 + fmt.Fprintf(os.Stderr, "--> %15s is already exists, delete it\n", target.Name) + if err := DeleteAsset(info, target.AssetId); err != nil { + errorLock.Lock() + defer errorLock.Unlock() + errors = append(errors, + fmt.Sprintf("deleting %s error: %s", target.Name, err)) + } + <-semaphore + }(target) + } + } + wg.Wait() + for _, path := range files { wg.Add(1) go func(path string) { @@ -198,7 +233,7 @@ func ghrMain() int { errorLock.Lock() defer errorLock.Unlock() errors = append(errors, - fmt.Sprintf("%s error: %s", path, err)) + fmt.Sprintf("upload %s error: %s", path, err)) } <-semaphore }(path) @@ -226,6 +261,7 @@ Options: -t, --token Github API Token -r, --repository Github repository name -p, --parallel=-1 Amount of parallelism, defaults to number of CPUs + --replace Replace asset if target already exists  --delete Delete release if same version exists --draft Create unpublised release --prerelease Create prerelease @@ -235,6 +271,6 @@ Options: Example: $ ghr v1.0.0 dist/ - $ ghr --delete v1.0.0 dist/ + $ ghr --replace v1.0.0 dist/ $ ghr v1.0.2 dist/tool.zip ` diff --git a/request.go b/request.go index af5a577..2a2af01 100644 --- a/request.go +++ b/request.go @@ -49,6 +49,30 @@ func GetReleaseID(info *Info) (int, error) { return SearchIDByTag(res.Body, info.TagName) } +func GetDeleteTargets(info *Info, uploads []string) ([]DeleteTarget, error) { + requestURL := listAssetsURL(info) + debug(requestURL) + + req, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return []DeleteTarget{}, err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return []DeleteTarget{}, err + } + debug(res.Status) + + err = checkStatusOK(res.StatusCode, res.Status) + if err != nil { + return []DeleteTarget{}, err + } + + defer res.Body.Close() + return SearchDeleteTargets(res.Body, uploads) +} + func DeleteRelease(info *Info) error { requestURL := deleteReleaseURL(info) debug(requestURL) @@ -74,6 +98,31 @@ func DeleteRelease(info *Info) error { return nil } +func DeleteAsset(info *Info, assetId int) error { + requestURL := deleteAssetURL(info, assetId) + debug(requestURL) + + req, err := http.NewRequest("DELETE", requestURL, nil) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/vnd.github.v3+json") + req.Header.Add("Authorization", fmt.Sprintf("token %s", info.Token)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + debug(res.Status) + + if res.StatusCode != http.StatusNoContent { + return fmt.Errorf("Github returned %s\n", res.Status) + } + return nil +} + func CreateNewRelease(info *Info) (int, error) { requestURL := releaseURL(info)