mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-04-25 12:14:56 +02:00
* Add ability to add and download attachments, including multi-part form handling
* Add method to report if the current session is authenticated or not
This commit is contained in:
parent
559b76c3ef
commit
2bc0c88214
92
issue.go
92
issue.go
@ -1,7 +1,10 @@
|
|||||||
package jira
|
package jira
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,6 +29,19 @@ type Issue struct {
|
|||||||
Fields *IssueFields `json:"fields,omitempty"`
|
Fields *IssueFields `json:"fields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachment represents a JIRA attachment
|
||||||
|
type Attachment struct {
|
||||||
|
Self string `json:"self,omitempty"`
|
||||||
|
Id string `json:"id,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
// TODO Missing fields
|
||||||
|
//Author string `json:"author,omitempty"`
|
||||||
|
Created string `json:"created,omitempty"`
|
||||||
|
Size int `json:"size,omitempty"`
|
||||||
|
MimeType string `json:"mimeType,omitempty"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// IssueFields represents single fields of a JIRA issue.
|
// IssueFields represents single fields of a JIRA issue.
|
||||||
// Every JIRA issue has several fields attached.
|
// Every JIRA issue has several fields attached.
|
||||||
type IssueFields struct {
|
type IssueFields struct {
|
||||||
@ -39,7 +55,6 @@ type IssueFields struct {
|
|||||||
// * "aggregatetimeoriginalestimate": null,
|
// * "aggregatetimeoriginalestimate": null,
|
||||||
// * "timeoriginalestimate": null,
|
// * "timeoriginalestimate": null,
|
||||||
// * "timetracking": {},
|
// * "timetracking": {},
|
||||||
// * "attachment": [],
|
|
||||||
// * "aggregatetimeestimate": null,
|
// * "aggregatetimeestimate": null,
|
||||||
// * "subtasks": [],
|
// * "subtasks": [],
|
||||||
// * "environment": null,
|
// * "environment": null,
|
||||||
@ -65,6 +80,7 @@ type IssueFields struct {
|
|||||||
IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
|
IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
|
||||||
Comments []*Comment `json:"comment.comments,omitempty"`
|
Comments []*Comment `json:"comment.comments,omitempty"`
|
||||||
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
|
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
|
||||||
|
Attachments []*Attachment `json:"attachment,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueType represents a type of a JIRA issue.
|
// IssueType represents a type of a JIRA issue.
|
||||||
@ -186,14 +202,14 @@ type IssueLinkType struct {
|
|||||||
|
|
||||||
// Comment represents a comment by a person to an issue in JIRA.
|
// Comment represents a comment by a person to an issue in JIRA.
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name,omitempty"`
|
||||||
Author Assignee `json:"author"`
|
Author Assignee `json:"author,omitempty"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body,omitempty"`
|
||||||
UpdateAuthor Assignee `json:"updateAuthor"`
|
UpdateAuthor Assignee `json:"updateAuthor,omitempty"`
|
||||||
Updated string `json:"updated"`
|
Updated string `json:"updated,omitempty"`
|
||||||
Created string `json:"created"`
|
Created string `json:"created,omitempty"`
|
||||||
Visibility CommentVisibility `json:"visibility"`
|
Visibility CommentVisibility `json:"visibility,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FixVersion represents a software release in which an issue is fixed.
|
// FixVersion represents a software release in which an issue is fixed.
|
||||||
@ -211,8 +227,8 @@ type FixVersion struct {
|
|||||||
// CommentVisibility represents he visibility of a comment.
|
// CommentVisibility represents he visibility of a comment.
|
||||||
// E.g. Type could be "role" and Value "Administrators"
|
// E.g. Type could be "role" and Value "Administrators"
|
||||||
type CommentVisibility struct {
|
type CommentVisibility struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type,omitempty"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns a full representation of the issue for the given issue key.
|
// Get returns a full representation of the issue for the given issue key.
|
||||||
@ -237,6 +253,60 @@ func (s *IssueService) Get(issueID string) (*Issue, *http.Response, error) {
|
|||||||
return issue, resp, nil
|
return issue, resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadAttachment returns an ioReader of an attachment for a given attachment Id
|
||||||
|
// The attachment is in the Body of the response
|
||||||
|
// The caller should close resp.Body
|
||||||
|
func (s *IssueService) DownloadAttachment(attachmentID string) (*http.Response, error) {
|
||||||
|
apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID)
|
||||||
|
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.client.DoNoClose(req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostAttachment uploads an attachment provided as an io.Reader to a given attachment ID
|
||||||
|
func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachmentName string) (*Issue, *http.Response, error) {
|
||||||
|
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", attachmentID)
|
||||||
|
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
writer := multipart.NewWriter(b)
|
||||||
|
|
||||||
|
fw, err := writer.CreateFormFile("file", attachmentName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file
|
||||||
|
if _, err = io.Copy(fw, r); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
issue := new(Issue)
|
||||||
|
resp, err := s.client.Do(req, issue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return issue, resp, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Create creates an issue or a sub-task from a JSON representation.
|
// Create creates an issue or a sub-task from a JSON representation.
|
||||||
// Creating a sub-task is similar to creating a regular issue, with two important differences:
|
// Creating a sub-task is similar to creating a regular issue, with two important differences:
|
||||||
// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue.
|
// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue.
|
||||||
|
60
jira.go
60
jira.go
@ -89,6 +89,34 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewMultiPartRequest creates an API request including a multi-part file
|
||||||
|
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
|
||||||
|
// Relative URLs should always be specified without a preceding slash.
|
||||||
|
// If specified, the value pointed to by buf is a multipart form
|
||||||
|
func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
|
||||||
|
rel, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := c.baseURL.ResolveReference(rel)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, u.String(), buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set required headers
|
||||||
|
req.Header.Set("X-Atlassian-Token", "nocheck")
|
||||||
|
|
||||||
|
// Set session cookie if there is one
|
||||||
|
if c.session != nil {
|
||||||
|
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", c.session.Session.Name, c.session.Session.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Do sends an API request and returns the API response.
|
// Do sends an API request and returns the API response.
|
||||||
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
|
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
|
||||||
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
|
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
|
||||||
@ -113,6 +141,38 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do sends an API request and returns the API response.
|
||||||
|
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
|
||||||
|
// The caller needs to call Body.Close when the response has been handled
|
||||||
|
func (c *Client) DoNoClose(req *http.Request, v interface{}) (*http.Response, error) {
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CheckResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
// Even though there was an error, we still return the response
|
||||||
|
// in case the caller wants to inspect it further
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v != nil {
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticated reports if the current Client has an authenticated session with JIRA
|
||||||
|
func (c *Client) Authenticated() bool {
|
||||||
|
if c != nil {
|
||||||
|
return c.session != nil
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CheckResponse checks the API response for errors, and returns them if present.
|
// CheckResponse checks the API response for errors, and returns them if present.
|
||||||
// A response is considered an error if it has a status code outside the 200 range.
|
// A response is considered an error if it has a status code outside the 200 range.
|
||||||
// API error responses are expected to have either no response body, or a JSON response body that maps to ErrorResponse.
|
// API error responses are expected to have either no response body, or a JSON response body that maps to ErrorResponse.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user