Asana Integeration
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

202 lines
4.4 KiB

3 years ago
3 years ago
  1. package attach
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "mime/multipart"
  9. "net/http"
  10. "net/textproto"
  11. "os"
  12. "strings"
  13. "git.drinkme.beer/yinghe/log"
  14. "git.drinkme.beer/yinghe/asana/util"
  15. )
  16. const (
  17. URI = "/api/1.0/tasks/%s/attachments"
  18. )
  19. type Request struct {
  20. Body io.Reader
  21. TaskID string
  22. Name string
  23. Path string
  24. PAToken string
  25. TicketID string
  26. }
  27. type Data struct {
  28. Response Response `json:"data"`
  29. }
  30. type Response struct {
  31. ID string `json:"gid"`
  32. ResourceType string `json:"resource_type"`
  33. Name string `json:"name"`
  34. // ResourceSubtype - asana, dropbox, gdrive, onedrive, box, and external
  35. ResourceSubtype string `json:"resource_subtype"`
  36. }
  37. var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
  38. func escapeQuotes(s string) string {
  39. return quoteEscaper.Replace(s)
  40. }
  41. // Create - upload attachments
  42. func (r Request) Create() (resp *Response, err error) {
  43. var (
  44. tempPath string
  45. data struct {
  46. Response Response `json:"data"`
  47. }
  48. )
  49. tempPath = "./static/" + r.Name
  50. // TODO
  51. err = download(r.Path, tempPath)
  52. if nil != err {
  53. return nil, err
  54. }
  55. attachments, _ := os.Open(tempPath)
  56. defer func() {
  57. err := attachments.Close()
  58. if nil != err {
  59. log.Errorf("failed to close temporary file: %s", err.Error())
  60. }
  61. err = os.Remove(tempPath)
  62. if nil != err {
  63. log.Errorf("failed to remove temporary file: %s", err.Error())
  64. }
  65. }()
  66. // Step 1. Try to determine the contentType.
  67. contentType, body, err := fDetectContentType(attachments)
  68. if nil != err {
  69. log.Errorf("failed to detect content type: %s", err.Error())
  70. return
  71. }
  72. if nil == body {
  73. log.Errorf("empty attachment")
  74. return nil, errors.New("generating attachment failed")
  75. }
  76. // Step 2:
  77. // Initiate and then make the upload.
  78. prc, pwc := io.Pipe()
  79. mpartW := multipart.NewWriter(pwc)
  80. go func() {
  81. defer func() {
  82. _ = mpartW.Close()
  83. _ = pwc.Close()
  84. }()
  85. h := make(textproto.MIMEHeader)
  86. h.Set("Content-Disposition",
  87. fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
  88. escapeQuotes("file"), escapeQuotes(r.Name)))
  89. //h.Set("Content-Type", "application/octet-stream")
  90. log.Infof("Content-Type: %s", contentType)
  91. h.Set("Content-Type", contentType)
  92. formFile, err := mpartW.CreatePart(h)
  93. //formFile, err := mpartW.CreateFormFile("file", r.Name)
  94. if err != nil {
  95. return
  96. }
  97. _, _ = io.Copy(formFile, body)
  98. //writeStringField(mpartW, "Content-Type", contentType)
  99. //writeStringField(mpartW, "resource_subtype", "asana")
  100. //writeStringField(mpartW, "name", "20210811065901")
  101. }()
  102. fullURL := fmt.Sprintf("%s/api/1.0/tasks/%s/attachments", util.AsanaHost, r.TaskID)
  103. req, err := http.NewRequest("POST", fullURL, prc)
  104. if err != nil {
  105. log.Errorf("failed to build attachment request: %s", err.Error())
  106. return nil, err
  107. }
  108. req.Header.Set("Content-Type", mpartW.FormDataContentType())
  109. req.Header.Set("Authorization", r.PAToken)
  110. buf, err := util.Request(req)
  111. if nil != err {
  112. return nil, err
  113. }
  114. err = json.Unmarshal(buf, &data)
  115. if nil != err {
  116. log.Errorf("[%s][%s]failed to upload attachments: %s", r.TicketID, r.TaskID, err.Error())
  117. return nil, err
  118. }
  119. return &data.Response, err
  120. }
  121. func fDetectContentType(r io.Reader) (string, io.Reader, error) {
  122. if r == nil {
  123. return "", nil, errors.New("empty attachments")
  124. }
  125. seeker, seekable := r.(io.Seeker)
  126. sniffBuf := make([]byte, 512)
  127. n, err := io.ReadAtLeast(r, sniffBuf, 1)
  128. if err != nil {
  129. log.Errorf(err.Error())
  130. return "", nil, err
  131. }
  132. contentType := http.DetectContentType(sniffBuf)
  133. needsRepad := !seekable
  134. if seekable {
  135. if _, err = seeker.Seek(int64(-n), io.SeekCurrent); err != nil {
  136. // Since we failed to rewind it, mark it as needing repad
  137. needsRepad = true
  138. }
  139. }
  140. if needsRepad {
  141. r = io.MultiReader(bytes.NewReader(sniffBuf), r)
  142. }
  143. return contentType, r, nil
  144. }
  145. func writeStringField(w *multipart.Writer, key, value string) {
  146. fw, err := w.CreateFormField(key)
  147. if err == nil {
  148. _, _ = io.WriteString(fw, value)
  149. }
  150. }
  151. var (
  152. errValidation = errors.New("invalid request")
  153. )
  154. func download(src, filename string) error {
  155. if "" == src || "" == filename {
  156. return errValidation
  157. }
  158. f, err := os.Create(filename)
  159. if nil != err {
  160. log.Errorf("failed to create attachments")
  161. return errors.New("failed to create file")
  162. }
  163. defer f.Close()
  164. //defer os.Remove(filename)
  165. resp, err := http.Get(src)
  166. if nil != err {
  167. log.Infof("download report failed: %s", err.Error())
  168. return err
  169. }
  170. defer resp.Body.Close()
  171. _, err = io.Copy(f, resp.Body)
  172. return err
  173. }