grim/youtrack-import

Fix the maxIssues bug where if you didn't specify a number of issues to import it didn't import any issues.
package youtrack
import (
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/avast/retry-go"
)
type Attachment struct {
issue int
author string
created time.Time
payload *strings.Builder
writer *multipart.Writer
files []string
}
func NewAttachment(issue int, author string, created time.Time) *Attachment {
a := &Attachment{
issue: issue,
author: author,
created: created,
files: []string{},
}
a.payload = &strings.Builder{}
a.writer = multipart.NewWriter(a.payload)
return a
}
func (a *Attachment) Payload() (string, error) {
if err := a.writer.Close(); err != nil {
return "", err
}
return a.payload.String(), nil
}
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
func (a *Attachment) AddFile(r io.ReadCloser, filename string) error {
header := textproto.MIMEHeader{}
header.Set(
"Content-Disposition",
fmt.Sprintf("form-data; name=%q; filename=%q",
escapeQuotes(filename),
escapeQuotes(filename),
),
)
contentType := ""
ext := filepath.Ext(filename)
if ext != "" {
contentType = mime.TypeByExtension(ext)
}
if contentType == "" {
contentType = "application/octet-stream"
}
header.Set("Content-Type", contentType)
w, err := a.writer.CreatePart(header)
if err != nil {
return err
}
_, err = io.Copy(w, r)
if err != nil {
return err
}
defer r.Close()
a.files = append(a.files, filename)
return nil
}
func (a *Attachment) ContentType() string {
return a.writer.FormDataContentType()
}
func (c *Client) waitForIssue(issueID string) error {
return retry.Do(
func() error {
resp, err := c.request(
http.MethodGet,
c.uri+"/issue/"+issueID,
nil,
)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return err
}
return nil
},
retry.Attempts(3),
)
}
func (c *Client) importAttachment(p *Project, a *Attachment) error {
issueID := fmt.Sprintf("%s-%d", p.ID, a.issue)
err := c.waitForIssue(issueID)
if err != nil {
return err
}
values := url.Values{}
values.Add("authorLogin", a.author)
values.Add("created", formatTimeString(a.created))
payload, err := a.Payload()
if err != nil {
return err
}
reader := strings.NewReader(payload)
resp, err := c.requestContentType(
http.MethodPost,
c.uri+"/import/"+issueID+"/attachment?"+values.Encode(),
a.ContentType(),
reader,
)
if err != nil {
return err
}
defer resp.Body.Close()
return c.validateXmlImportReport(
[]int{http.StatusOK, http.StatusCreated, http.StatusBadRequest},
resp,
)
}
func (c *Client) ImportAttachments(p *Project) error {
for _, issue := range p.Issues {
l := len(issue.Attachments)
for idx, attachment := range issue.Attachments {
fmt.Printf(
"importing attachment for issue %d: (%d of %d) ... ",
attachment.issue,
idx+1,
l,
)
if err := c.importAttachment(p, attachment); err != nil {
fmt.Printf("failed.\n")
return err
}
fmt.Printf("done.\n")
}
}
return nil
}
func extractAttachments(issue int, author, message string, created time.Time) (string, []*Attachment, error) {
pattern := `!\[(.*?)\]\((.+?)(\s+["'].*?["'])?\)`
regex := regexp.MustCompile(pattern)
output := message
attachments := []*Attachment{}
matches := regex.FindAllStringSubmatch(message, -1)
for _, match := range matches {
old := match[0]
filename := match[1]
uri := match[2]
if filename == "" {
decoded, err := url.QueryUnescape(uri)
if err != nil {
return "", attachments, err
}
filename = filepath.Base(decoded)
}
img := fmt.Sprintf("![](%s)", filename)
resp, err := http.Get(uri)
if err != nil {
return "", attachments, err
}
if resp.StatusCode != http.StatusOK {
return "", attachments, fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
attachment := NewAttachment(issue, author, created)
if err := attachment.AddFile(resp.Body, filename); err != nil {
return "", attachments, err
}
output = strings.Replace(output, old, img, 1)
attachments = append(attachments, attachment)
}
return output, attachments, nil
}