Skip to content

Commit

Permalink
[#100] add support for posting, fetching, listing and removing attach…
Browse files Browse the repository at this point in the history
…ments
  • Loading branch information
coryb committed Sep 17, 2017
1 parent abc82b9 commit 66eb7bf
Show file tree
Hide file tree
Showing 18 changed files with 1,232 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ jira
schemas/*.json
t/.gnupg/random_seed
t/issue.props
t/attach.props
t/garbage.bin
t/attach1.txt
t/binary.out
t/foobar.bin
dist
46 changes: 46 additions & 0 deletions attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package jira

import (
"fmt"

"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)

// https://docs.atlassian.com/jira/REST/cloud/#api/2/attachment-getAttachment
func (j *Jira) GetAttachment(id string) (*jiradata.Attachment, error) {
return GetAttachment(j.UA, j.Endpoint, id)
}

func GetAttachment(ua HttpClient, endpoint string, id string) (*jiradata.Attachment, error) {
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode == 200 {
results := &jiradata.Attachment{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}

// https://docs.atlassian.com/jira/REST/cloud/#api/2/attachment-removeAttachment
func (j *Jira) RemoveAttachment(id string) error {
return RemoveAttachment(j.UA, j.Endpoint, id)
}

func RemoveAttachment(ua HttpClient, endpoint string, id string) error {
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
resp, err := ua.Delete(uri)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
18 changes: 18 additions & 0 deletions cmd/jira/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,24 @@ func main() {
Command: "issuetypes",
Entry: jiracmd.CmdIssueTypesRegistry(),
},
jiracli.CommandRegistry{
Command: "attach create",
Entry: jiracmd.CmdAttachCreateRegistry(),
},
jiracli.CommandRegistry{
Command: "attach list",
Entry: jiracmd.CmdAttachListRegistry(),
Aliases: []string{"ls"},
},
jiracli.CommandRegistry{
Command: "attach get",
Entry: jiracmd.CmdAttachGetRegistry(),
},
jiracli.CommandRegistry{
Command: "attach remove",
Entry: jiracmd.CmdAttachRemoveRegistry(),
Aliases: []string{"rm"},
},
jiracli.CommandRegistry{
Command: "export-templates",
Entry: jiracmd.CmdExportTemplatesRegistry(),
Expand Down
52 changes: 52 additions & 0 deletions issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/url"
"strings"

"github.com/coryb/oreo"

"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)

Expand Down Expand Up @@ -533,3 +538,50 @@ func IssueAssign(ua HttpClient, endpoint string, issue, name string) error {
}
return responseError(resp)
}

// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/attachments-addAttachment
func (j *Jira) IssueAttachFile(issue, filename string, contents io.Reader) (*jiradata.ListOfAttachment, error) {
return IssueAttachFile(j.UA, j.Endpoint, issue, filename, contents)
}

func IssueAttachFile(ua HttpClient, endpoint string, issue, filename string, contents io.Reader) (*jiradata.ListOfAttachment, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
formFile, err := w.CreateFormFile("file", filename)
if err != nil {
return nil, err
}
_, err = io.Copy(formFile, contents)
if err != nil {
return nil, err
}

uri, err := url.Parse(fmt.Sprintf("%s/rest/api/2/issue/%s/attachments", endpoint, issue))
req := oreo.RequestBuilder(uri).WithMethod("POST").WithHeader(
"X-Atlassian-Token", "no-check",
).WithHeader(
"Accept", "application/json",
).WithContentType(w.FormDataContentType()).WithBody(&buf).Build()
w.Close()

resp, err := ua.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode == 200 {
// FIXME move this to a test, and run go tests as part of our regression
if false {
// this is because schema is wrong, defaults to type `int`, so we manually change it
// to `string`. If the jiradata is regenerated we need to manually make the change
// again.
log.Debugf("Assert Attachment.ID is a string, rather than int: %v", &jiradata.Attachment{
ID: jiradata.IntOrString(0),
})
}
results := jiradata.ListOfAttachment{}
return &results, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}
29 changes: 20 additions & 9 deletions jiracli/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ func TemplateProcessor() *template.Template {
}

func ConfigTemplate(fig *figtree.FigTree, template, command string, opts interface{}) (string, error) {
tmp, err := translateOptions(opts)
var tmp interface{}
err := ConvertType(opts, &tmp)
if err != nil {
return "", err
}
Expand All @@ -178,11 +179,11 @@ func ConfigTemplate(fig *figtree.FigTree, template, command string, opts interfa
return buf.String(), nil
}

func translateOptions(opts interface{}) (interface{}, error) {
func ConvertType(input interface{}, output interface{}) error {
// HACK HACK HACK: convert data formats to json for backwards compatibilty with templates
jsonData, err := json.Marshal(opts)
jsonData, err := json.Marshal(input)
if err != nil {
return nil, err
return err
}

defer func(mapType, iface reflect.Type) {
Expand All @@ -193,11 +194,10 @@ func translateOptions(opts interface{}) (interface{}, error) {
yaml.DefaultMapType = reflect.TypeOf(map[string]interface{}{})
yaml.IfaceType = yaml.DefaultMapType.Elem()

var rawData interface{}
if err := yaml.Unmarshal(jsonData, &rawData); err != nil {
return nil, err
if err := yaml.Unmarshal(jsonData, output); err != nil {
return err
}
return &rawData, nil
return nil

}

Expand All @@ -212,7 +212,8 @@ func RunTemplate(templateName string, data interface{}, out io.Writer) error {
out = os.Stdout
}

rawData, err := translateOptions(data)
var rawData interface{}
err = ConvertType(data, &rawData)
if err != nil {
return err
}
Expand All @@ -228,6 +229,7 @@ func RunTemplate(templateName string, data interface{}, out io.Writer) error {
}

var AllTemplates = map[string]string{
"attach-list": defaultAttachListTemplate,
"comment": defaultCommentTemplate,
"component-add": defaultComponentAddTemplate,
"components": defaultComponentsTemplate,
Expand Down Expand Up @@ -268,6 +270,15 @@ const defaultTableTemplate = `{{/* table template */ -}}
{{ end -}}
+{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
`
const defaultAttachListTemplate = `{{/* table template */ -}}
+{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
| {{printf "%-10s" "id"}} | {{printf "%-28s" "filename"}} | {{printf "%-10s" "bytes"}} | {{printf "%-12s" "user"}} | {{printf "%-12s" "created"}} |
+{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
{{range . -}}
| {{.id | printf "%10d" }} | {{.filename | printf "%-28s"}} | {{.size | printf "%10d"}} | {{.author.name | printf "%-12s"}} | {{.created | age | printf "%-12s"}} |
{{end -}}
+{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
`

const defaultViewTemplate = `{{/* view template */ -}}
issue: {{ .key }}
Expand Down
99 changes: 99 additions & 0 deletions jiracmd/attachCreate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package jiracmd

import (
"fmt"
"os"
"sort"

"golang.org/x/crypto/ssh/terminal"

"github.com/coryb/figtree"
"github.com/coryb/oreo"
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
yaml "gopkg.in/coryb/yaml.v2"
)

type AttachCreateOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Attachment string `yaml:"attachment,omitempty" json:"attachment,omitempty"`
Filename string `yaml:"filename,omitempty" json:"filename,omitempty"`
SaveFile string `yaml:"savefile,omitempty" json:"savefile,omitempty"`
}

func CmdAttachCreateRegistry() *jiracli.CommandRegistryEntry {
opts := AttachCreateOptions{}

return &jiracli.CommandRegistryEntry{
"Attach file to issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAttachCreateUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdAttachCreate(o, globals, &opts)
},
}
}

func CmdAttachCreateUsage(cmd *kingpin.CmdClause, opts *AttachCreateOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Flag("saveFile", "Write attachment information as yaml to file").StringVar(&opts.SaveFile)
cmd.Flag("filename", "Filename to use for attachment").Short('f').StringVar(&opts.Filename)
cmd.Arg("ISSUE", "issue to assign").Required().StringVar(&opts.Issue)
cmd.Arg("ATTACHMENT", "File to attach to issue, if not provided read from stdin").StringVar(&opts.Attachment)
return nil
}

func CmdAttachCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachCreateOptions) error {
var contents *os.File
var err error
if opts.Attachment == "" {
if terminal.IsTerminal(int(os.Stdin.Fd())) {
return fmt.Errorf("ATTACHMENT argument required or redirect from STDIN")
}
contents = os.Stdin
if opts.Filename == "" {
return fmt.Errorf("--filename required when reading from stdin")
}
} else {
contents, err = os.Open(opts.Attachment)
if err != nil {
return err
}
if opts.Filename == "" {
opts.Filename = opts.Attachment
}
}
attachments, err := jira.IssueAttachFile(o, globals.Endpoint.Value, opts.Issue, opts.Filename, contents)
if err != nil {
return err
}

sort.Sort(sort.Reverse(attachments))

if opts.SaveFile != "" {
fh, err := os.Create(opts.SaveFile)
if err != nil {
return err
}
defer fh.Close()
out, err := yaml.Marshal((*attachments)[0])
if err != nil {
return err
}
fh.Write(out)
}

if !globals.Quiet.Value {
fmt.Printf("OK %d %s\n", (*attachments)[0].ID, (*attachments)[0].Content)
}

if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}

return nil
}
Loading

0 comments on commit 66eb7bf

Please sign in to comment.