From 66eb7bff38231ef05238d71f2e80a70b229f53de Mon Sep 17 00:00:00 2001 From: Cory Bennett Date: Sun, 17 Sep 2017 15:18:06 -0700 Subject: [PATCH] [#100] add support for posting, fetching, listing and removing attachments --- .gitignore | 5 + attachment.go | 46 +++++++ cmd/jira/main.go | 18 +++ issue.go | 52 ++++++++ jiracli/templates.go | 29 +++-- jiracmd/attachCreate.go | 99 +++++++++++++++ jiracmd/attachGet.go | 79 ++++++++++++ jiracmd/attachList.go | 67 ++++++++++ jiracmd/attachRemove.go | 46 +++++++ jiradata/Attachment.go | 179 ++++++++++++++++++++++++++ jiradata/Group.go | 29 +++++ jiradata/Groups.go | 29 +++++ jiradata/ListOfAttachment.go | 202 ++++++++++++++++++++++++++++++ jiradata/ListOfAttachmentFuncs.go | 13 ++ jiradata/SimpleListWrapper.go | 45 +++++++ jiradata/User.go | 95 ++++++++++++-- jiradata/intOrString.go | 29 +++++ t/140basic-attach.t | 189 ++++++++++++++++++++++++++++ 18 files changed, 1232 insertions(+), 19 deletions(-) create mode 100644 attachment.go create mode 100644 jiracmd/attachCreate.go create mode 100644 jiracmd/attachGet.go create mode 100644 jiracmd/attachList.go create mode 100644 jiracmd/attachRemove.go create mode 100644 jiradata/Attachment.go create mode 100644 jiradata/Group.go create mode 100644 jiradata/Groups.go create mode 100644 jiradata/ListOfAttachment.go create mode 100644 jiradata/ListOfAttachmentFuncs.go create mode 100644 jiradata/SimpleListWrapper.go create mode 100644 jiradata/intOrString.go create mode 100755 t/140basic-attach.t diff --git a/.gitignore b/.gitignore index 4138cfe0..619b7f98 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/attachment.go b/attachment.go new file mode 100644 index 00000000..13586193 --- /dev/null +++ b/attachment.go @@ -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) +} diff --git a/cmd/jira/main.go b/cmd/jira/main.go index 03ea617e..508b74df 100644 --- a/cmd/jira/main.go +++ b/cmd/jira/main.go @@ -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(), diff --git a/issue.go b/issue.go index 57d50f0b..2e8692ee 100644 --- a/issue.go +++ b/issue.go @@ -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" ) @@ -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) +} diff --git a/jiracli/templates.go b/jiracli/templates.go index 251286a7..a9070248 100644 --- a/jiracli/templates.go +++ b/jiracli/templates.go @@ -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 } @@ -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) { @@ -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 } @@ -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 } @@ -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, @@ -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 }} diff --git a/jiracmd/attachCreate.go b/jiracmd/attachCreate.go new file mode 100644 index 00000000..81aca2e0 --- /dev/null +++ b/jiracmd/attachCreate.go @@ -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 +} diff --git a/jiracmd/attachGet.go b/jiracmd/attachGet.go new file mode 100644 index 00000000..5f227656 --- /dev/null +++ b/jiracmd/attachGet.go @@ -0,0 +1,79 @@ +package jiracmd + +import ( + "fmt" + "io" + "os" + + "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" +) + +type AttachGetOptions struct { + AttachmentID string `yaml:"attachment-id,omitempty" json:"attachment-id,omitempty"` + OutputFile string `yaml:"output-file,omitempty" json:"output-file,omitempty"` +} + +func CmdAttachGetRegistry() *jiracli.CommandRegistryEntry { + opts := AttachGetOptions{} + + return &jiracli.CommandRegistryEntry{ + "Fetch attachment", + func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error { + jiracli.LoadConfigs(cmd, fig, &opts) + return CmdAttachGetUsage(cmd, &opts) + }, + func(o *oreo.Client, globals *jiracli.GlobalOptions) error { + return CmdAttachGet(o, globals, &opts) + }, + } +} + +func CmdAttachGetUsage(cmd *kingpin.CmdClause, opts *AttachGetOptions) error { + cmd.Flag("output", "Write attachment to specified file name, '-' for stdout").Short('o').StringVar(&opts.OutputFile) + cmd.Arg("ATTACHMENT-ID", "Attachment id to fetch").StringVar(&opts.AttachmentID) + return nil +} + +func CmdAttachGet(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachGetOptions) error { + attachment, err := jira.GetAttachment(o, globals.Endpoint.Value, opts.AttachmentID) + if err != nil { + return err + } + + resp, err := o.Get(attachment.Content) + if err != nil { + return err + } + defer resp.Body.Close() + + var output *os.File + if opts.OutputFile == "-" { + output = os.Stdout + } else if opts.OutputFile != "" { + output, err = os.Create(opts.OutputFile) + if err != nil { + return err + } + defer output.Close() + } else { + output, err = os.Create(attachment.Filename) + if err != nil { + return err + } + defer output.Close() + } + + _, err = io.Copy(output, resp.Body) + if err != nil { + return err + } + output.Close() + if opts.OutputFile != "-" && !globals.Quiet.Value { + fmt.Printf("OK Wrote %s\n", output.Name()) + } + return nil +} diff --git a/jiracmd/attachList.go b/jiracmd/attachList.go new file mode 100644 index 00000000..e1bd8b05 --- /dev/null +++ b/jiracmd/attachList.go @@ -0,0 +1,67 @@ +package jiracmd + +import ( + "sort" + + "github.com/coryb/figtree" + "github.com/coryb/oreo" + "gopkg.in/Netflix-Skunkworks/go-jira.v1" + "gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli" + "gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata" + kingpin "gopkg.in/alecthomas/kingpin.v2" +) + +type AttachListOptions struct { + jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"` + Issue string `yaml:"issue,omitempty" json:"issue,omitempty"` +} + +func CmdAttachListRegistry() *jiracli.CommandRegistryEntry { + opts := AttachListOptions{ + CommonOptions: jiracli.CommonOptions{ + Template: figtree.NewStringOption("attach-list"), + }, + } + + return &jiracli.CommandRegistryEntry{ + "Prints issue details", + func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error { + jiracli.LoadConfigs(cmd, fig, &opts) + return CmdAttachListUsage(cmd, &opts) + }, + func(o *oreo.Client, globals *jiracli.GlobalOptions) error { + return CmdAttachList(o, globals, &opts) + }, + } +} + +func CmdAttachListUsage(cmd *kingpin.CmdClause, opts *AttachListOptions) error { + jiracli.BrowseUsage(cmd, &opts.CommonOptions) + jiracli.TemplateUsage(cmd, &opts.CommonOptions) + cmd.Arg("ISSUE", "Issue id to lookup attachments").Required().StringVar(&opts.Issue) + return nil +} + +func CmdAttachList(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachListOptions) error { + data, err := jira.GetIssue(o, globals.Endpoint.Value, opts.Issue, nil) + if err != nil { + return err + } + + // need to conver the interface{} "attachment" field to an actual + // ListOfAttachment object so we can sort it + var attachments jiradata.ListOfAttachment + err = jiracli.ConvertType(data.Fields["attachment"], &attachments) + if err != nil { + return err + } + sort.Sort(&attachments) + + if err := opts.PrintTemplate(attachments); err != nil { + return err + } + if opts.Browse.Value { + return CmdBrowse(globals, opts.Issue) + } + return nil +} diff --git a/jiracmd/attachRemove.go b/jiracmd/attachRemove.go new file mode 100644 index 00000000..65992428 --- /dev/null +++ b/jiracmd/attachRemove.go @@ -0,0 +1,46 @@ +package jiracmd + +import ( + "fmt" + + "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" +) + +type AttachRemoveOptions struct { + AttachmentID string `yaml:"attachment-id,omitempty" json:"attachment-id,omitempty"` +} + +func CmdAttachRemoveRegistry() *jiracli.CommandRegistryEntry { + opts := AttachRemoveOptions{} + + return &jiracli.CommandRegistryEntry{ + "Delete attachment", + func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error { + jiracli.LoadConfigs(cmd, fig, &opts) + return CmdAttachRemoveUsage(cmd, &opts) + }, + func(o *oreo.Client, globals *jiracli.GlobalOptions) error { + return CmdAttachRemove(o, globals, &opts) + }, + } +} + +func CmdAttachRemoveUsage(cmd *kingpin.CmdClause, opts *AttachRemoveOptions) error { + cmd.Arg("ATTACHMENT-ID", "Attachment id to fetch").StringVar(&opts.AttachmentID) + return nil +} + +func CmdAttachRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachRemoveOptions) error { + if err := jira.RemoveAttachment(o, globals.Endpoint.Value, opts.AttachmentID); err != nil { + return err + } + + if !globals.Quiet.Value { + fmt.Printf("OK Deleted Attachment %s\n", opts.AttachmentID) + } + return nil +} diff --git a/jiradata/Attachment.go b/jiradata/Attachment.go new file mode 100644 index 00000000..9c42f737 --- /dev/null +++ b/jiradata/Attachment.go @@ -0,0 +1,179 @@ +package jiradata + +///////////////////////////////////////////////////////////////////////// +// This Code is Generated by SlipScheme Project: +// https://github.com/coryb/slipscheme +// +// Generated with command: +// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ListofAttachment.json +///////////////////////////////////////////////////////////////////////// +// DO NOT EDIT // +///////////////////////////////////////////////////////////////////////// + +// Attachment defined from schema: +// { +// "title": "Attachment", +// "type": "object", +// "properties": { +// "author": { +// "title": "User", +// "type": "object", +// "properties": { +// "accountId": { +// "title": "accountId", +// "type": "string" +// }, +// "active": { +// "title": "active", +// "type": "boolean" +// }, +// "applicationRoles": { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// }, +// "avatarUrls": { +// "title": "avatarUrls", +// "type": "object", +// "patternProperties": { +// ".+": { +// "type": "string" +// } +// } +// }, +// "displayName": { +// "title": "displayName", +// "type": "string" +// }, +// "emailAddress": { +// "title": "emailAddress", +// "type": "string" +// }, +// "expand": { +// "title": "expand", +// "type": "string" +// }, +// "groups": { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// }, +// "key": { +// "title": "key", +// "type": "string" +// }, +// "locale": { +// "title": "locale", +// "type": "string" +// }, +// "name": { +// "title": "name", +// "type": "string" +// }, +// "self": { +// "title": "self", +// "type": "string" +// }, +// "timeZone": { +// "title": "timeZone", +// "type": "string" +// } +// } +// }, +// "content": { +// "title": "content", +// "type": "string" +// }, +// "created": { +// "title": "created", +// "type": "string" +// }, +// "filename": { +// "title": "filename", +// "type": "string" +// }, +// "id": { +// "title": "id", +// "type": "integer" +// }, +// "mimeType": { +// "title": "mimeType", +// "type": "string" +// }, +// "properties": { +// "title": "properties", +// "type": "object", +// "patternProperties": { +// ".+": {} +// } +// }, +// "self": { +// "title": "self", +// "type": "string" +// }, +// "size": { +// "title": "size", +// "type": "integer" +// }, +// "thumbnail": { +// "title": "thumbnail", +// "type": "string" +// } +// } +// } +type Attachment struct { + Author *User `json:"author,omitempty" yaml:"author,omitempty"` + Content string `json:"content,omitempty" yaml:"content,omitempty"` + Created string `json:"created,omitempty" yaml:"created,omitempty"` + Filename string `json:"filename,omitempty" yaml:"filename,omitempty"` + ID IntOrString `json:"id,omitempty" yaml:"id,omitempty"` + MimeType string `json:"mimeType,omitempty" yaml:"mimeType,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty" yaml:"properties,omitempty"` + Self string `json:"self,omitempty" yaml:"self,omitempty"` + Size int `json:"size,omitempty" yaml:"size,omitempty"` + Thumbnail string `json:"thumbnail,omitempty" yaml:"thumbnail,omitempty"` +} diff --git a/jiradata/Group.go b/jiradata/Group.go new file mode 100644 index 00000000..687adad1 --- /dev/null +++ b/jiradata/Group.go @@ -0,0 +1,29 @@ +package jiradata + +///////////////////////////////////////////////////////////////////////// +// This Code is Generated by SlipScheme Project: +// https://github.com/coryb/slipscheme +// +// Generated with command: +// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ListofAttachment.json +///////////////////////////////////////////////////////////////////////// +// DO NOT EDIT // +///////////////////////////////////////////////////////////////////////// + +// Group defined from schema: +// { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +type Group struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Self string `json:"self,omitempty" yaml:"self,omitempty"` +} diff --git a/jiradata/Groups.go b/jiradata/Groups.go new file mode 100644 index 00000000..1cbd0288 --- /dev/null +++ b/jiradata/Groups.go @@ -0,0 +1,29 @@ +package jiradata + +///////////////////////////////////////////////////////////////////////// +// This Code is Generated by SlipScheme Project: +// https://github.com/coryb/slipscheme +// +// Generated with command: +// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ListofAttachment.json +///////////////////////////////////////////////////////////////////////// +// DO NOT EDIT // +///////////////////////////////////////////////////////////////////////// + +// Groups defined from schema: +// { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// } +type Groups []*Group diff --git a/jiradata/ListOfAttachment.go b/jiradata/ListOfAttachment.go new file mode 100644 index 00000000..c869b415 --- /dev/null +++ b/jiradata/ListOfAttachment.go @@ -0,0 +1,202 @@ +package jiradata + +///////////////////////////////////////////////////////////////////////// +// This Code is Generated by SlipScheme Project: +// https://github.com/coryb/slipscheme +// +// Generated with command: +// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ListofAttachment.json +///////////////////////////////////////////////////////////////////////// +// DO NOT EDIT // +///////////////////////////////////////////////////////////////////////// + +// ListOfAttachment defined from schema: +// { +// "title": "List of Attachment", +// "id": "https://docs.atlassian.com/jira/REST/schema/list-of-attachment#", +// "type": "array", +// "definitions": { +// "simple-list-wrapper": { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// } +// }, +// "items": { +// "title": "Attachment", +// "type": "object", +// "properties": { +// "author": { +// "title": "User", +// "type": "object", +// "properties": { +// "accountId": { +// "title": "accountId", +// "type": "string" +// }, +// "active": { +// "title": "active", +// "type": "boolean" +// }, +// "applicationRoles": { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// }, +// "avatarUrls": { +// "title": "avatarUrls", +// "type": "object", +// "patternProperties": { +// ".+": { +// "type": "string" +// } +// } +// }, +// "displayName": { +// "title": "displayName", +// "type": "string" +// }, +// "emailAddress": { +// "title": "emailAddress", +// "type": "string" +// }, +// "expand": { +// "title": "expand", +// "type": "string" +// }, +// "groups": { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// }, +// "key": { +// "title": "key", +// "type": "string" +// }, +// "locale": { +// "title": "locale", +// "type": "string" +// }, +// "name": { +// "title": "name", +// "type": "string" +// }, +// "self": { +// "title": "self", +// "type": "string" +// }, +// "timeZone": { +// "title": "timeZone", +// "type": "string" +// } +// } +// }, +// "content": { +// "title": "content", +// "type": "string" +// }, +// "created": { +// "title": "created", +// "type": "string" +// }, +// "filename": { +// "title": "filename", +// "type": "string" +// }, +// "id": { +// "title": "id", +// "type": "integer" +// }, +// "mimeType": { +// "title": "mimeType", +// "type": "string" +// }, +// "properties": { +// "title": "properties", +// "type": "object", +// "patternProperties": { +// ".+": {} +// } +// }, +// "self": { +// "title": "self", +// "type": "string" +// }, +// "size": { +// "title": "size", +// "type": "integer" +// }, +// "thumbnail": { +// "title": "thumbnail", +// "type": "string" +// } +// } +// } +// } +type ListOfAttachment []*Attachment diff --git a/jiradata/ListOfAttachmentFuncs.go b/jiradata/ListOfAttachmentFuncs.go new file mode 100644 index 00000000..9f354dc5 --- /dev/null +++ b/jiradata/ListOfAttachmentFuncs.go @@ -0,0 +1,13 @@ +package jiradata + +func (l *ListOfAttachment) Len() int { + return len(*l) +} + +func (l *ListOfAttachment) Less(i, j int) bool { + return (*l)[i].ID < (*l)[j].ID +} + +func (l *ListOfAttachment) Swap(i, j int) { + (*l)[i], (*l)[j] = (*l)[j], (*l)[i] +} diff --git a/jiradata/SimpleListWrapper.go b/jiradata/SimpleListWrapper.go new file mode 100644 index 00000000..b4ace5e6 --- /dev/null +++ b/jiradata/SimpleListWrapper.go @@ -0,0 +1,45 @@ +package jiradata + +///////////////////////////////////////////////////////////////////////// +// This Code is Generated by SlipScheme Project: +// https://github.com/coryb/slipscheme +// +// Generated with command: +// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ListofAttachment.json +///////////////////////////////////////////////////////////////////////// +// DO NOT EDIT // +///////////////////////////////////////////////////////////////////////// + +// SimpleListWrapper defined from schema: +// { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// } +type SimpleListWrapper struct { + Items Groups `json:"items,omitempty" yaml:"items,omitempty"` + MaxResults int `json:"max-results,omitempty" yaml:"max-results,omitempty"` + Size int `json:"size,omitempty" yaml:"size,omitempty"` +} diff --git a/jiradata/User.go b/jiradata/User.go index b86c129b..74b50232 100644 --- a/jiradata/User.go +++ b/jiradata/User.go @@ -5,7 +5,7 @@ package jiradata // https://github.com/coryb/slipscheme // // Generated with command: -// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/WorklogWithPagination.json +// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ListofAttachment.json ///////////////////////////////////////////////////////////////////////// // DO NOT EDIT // ///////////////////////////////////////////////////////////////////////// @@ -16,12 +16,42 @@ package jiradata // "type": "object", // "properties": { // "accountId": { +// "title": "accountId", // "type": "string" // }, // "active": { +// "title": "active", // "type": "boolean" // }, +// "applicationRoles": { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// }, // "avatarUrls": { +// "title": "avatarUrls", // "type": "object", // "patternProperties": { // ".+": { @@ -30,33 +60,78 @@ package jiradata // } // }, // "displayName": { +// "title": "displayName", // "type": "string" // }, // "emailAddress": { +// "title": "emailAddress", +// "type": "string" +// }, +// "expand": { +// "title": "expand", // "type": "string" // }, +// "groups": { +// "title": "Simple List Wrapper", +// "type": "object", +// "properties": { +// "items": { +// "type": "array", +// "items": { +// "title": "Group", +// "type": "object", +// "properties": { +// "name": { +// "type": "string" +// }, +// "self": { +// "type": "string" +// } +// } +// } +// }, +// "max-results": { +// "type": "integer" +// }, +// "size": { +// "type": "integer" +// } +// } +// }, // "key": { +// "title": "key", +// "type": "string" +// }, +// "locale": { +// "title": "locale", // "type": "string" // }, // "name": { +// "title": "name", // "type": "string" // }, // "self": { +// "title": "self", // "type": "string" // }, // "timeZone": { +// "title": "timeZone", // "type": "string" // } // } // } type User struct { - AccountID string `json:"accountId,omitempty" yaml:"accountId,omitempty"` - Active bool `json:"active,omitempty" yaml:"active,omitempty"` - AvatarUrls map[string]string `json:"avatarUrls,omitempty" yaml:"avatarUrls,omitempty"` - DisplayName string `json:"displayName,omitempty" yaml:"displayName,omitempty"` - EmailAddress string `json:"emailAddress,omitempty" yaml:"emailAddress,omitempty"` - Key string `json:"key,omitempty" yaml:"key,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Self string `json:"self,omitempty" yaml:"self,omitempty"` - TimeZone string `json:"timeZone,omitempty" yaml:"timeZone,omitempty"` + AccountID string `json:"accountId,omitempty" yaml:"accountId,omitempty"` + Active bool `json:"active,omitempty" yaml:"active,omitempty"` + ApplicationRoles *SimpleListWrapper `json:"applicationRoles,omitempty" yaml:"applicationRoles,omitempty"` + AvatarUrls map[string]string `json:"avatarUrls,omitempty" yaml:"avatarUrls,omitempty"` + DisplayName string `json:"displayName,omitempty" yaml:"displayName,omitempty"` + EmailAddress string `json:"emailAddress,omitempty" yaml:"emailAddress,omitempty"` + Expand string `json:"expand,omitempty" yaml:"expand,omitempty"` + Groups *SimpleListWrapper `json:"groups,omitempty" yaml:"groups,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Locale string `json:"locale,omitempty" yaml:"locale,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Self string `json:"self,omitempty" yaml:"self,omitempty"` + TimeZone string `json:"timeZone,omitempty" yaml:"timeZone,omitempty"` } diff --git a/jiradata/intOrString.go b/jiradata/intOrString.go new file mode 100644 index 00000000..8f192b1b --- /dev/null +++ b/jiradata/intOrString.go @@ -0,0 +1,29 @@ +package jiradata + +import ( + "encoding/json" + "strconv" +) + +// this is for some bad schemas like Attachments.ID where in some api's it is an `int` and some it is a `string` +type IntOrString int + +func (i *IntOrString) UnmarshalYAML(unmarshal func(interface{}) error) error { + var tmp string + if err := unmarshal(&tmp); err != nil { + return unmarshal((*int)(i)) + } + tmpInt, err := strconv.Atoi(tmp) + *i = IntOrString(tmpInt) + return err +} + +func (i *IntOrString) UnmarshalJSON(b []byte) error { + var tmp string + if err := json.Unmarshal(b, &tmp); err != nil { + return json.Unmarshal(b, (*int)(i)) + } + tmpInt, err := strconv.Atoi(tmp) + *i = IntOrString(tmpInt) + return err +} diff --git a/t/140basic-attach.t b/t/140basic-attach.t new file mode 100755 index 00000000..58bbf66d --- /dev/null +++ b/t/140basic-attach.t @@ -0,0 +1,189 @@ +#!/bin/bash +eval "$(curl -q -s https://raw.githubusercontent.com/coryb/osht/master/osht.sh)" +cd $(dirname $0) +jira="../jira" +. env.sh + +PLAN 43 + +# reset login +RUNS $jira logout +RUNS $jira login + +# cleanup from previous failed test executions +($jira ls --project BASIC | awk -F: '{print $1}' | while read issue; do ../jira done $issue; done) | sed 's/^/# CLEANUP: /g' + +############################################################################### +## Create an issue +############################################################################### +RUNS $jira create --project BASIC -o summary="Attach To Me" -o description=description --noedit --saveFile issue.props +issue=$(awk '/issue/{print $2}' issue.props) + +DIFF < attach1.txt" + +# verify no diffs +RUNS diff -q README.md attach1.txt + +############################################################################### +## Fetch text attachment as same name +############################################################################### +RUNS $jira attach get $attach1 +DIFF < binary.out" + +# verify no diffs +RUNS diff -q garbage.bin binary.out + +############################################################################### +## Fetch binary attachment +############################################################################### +RUNS $jira attach get $attach3 +DIFF < binary.out" + +# verify no diffs +RUNS diff -q garbage.bin binary.out + +############################################################################### +## Delete attachment +############################################################################### +RUNS $jira attach remove $attach1 +DIFF <