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.

206 lines
4.6 KiB

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