package attach import ( "bytes" "encoding/json" "errors" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "os" "strings" "git.drinkme.beer/yinghe/log" "git.drinkme.beer/yinghe/asana/util" ) const ( URI = "/api/1.0/tasks/%s/attachments" ) type Request struct { Body io.Reader TaskID string Name string Path string PAToken string TicketID string } type Data struct { Response Response `json:"data"` } type Response struct { ID string `json:"gid"` ResourceType string `json:"resource_type"` Name string `json:"name"` // ResourceSubtype - asana, dropbox, gdrive, onedrive, box, and external ResourceSubtype string `json:"resource_subtype"` } var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } // Create - upload attachments func (r Request) Create() (resp *Response, err error) { var ( tempPath string data struct { Response Response `json:"data"` } ) tempPath = "./static/" + r.Name // TODO err = download(r.Path, tempPath) if nil != err { return nil, err } attachments, _ := os.Open(tempPath) defer func() { err := attachments.Close() if nil != err { log.Errorf("failed to close temporary file: %s", err.Error()) } err = os.Remove(tempPath) if nil != err { log.Errorf("failed to remove temporary file: %s", err.Error()) } }() // Step 1. Try to determine the contentType. contentType, body, err := fDetectContentType(attachments) if nil != err { log.Errorf("failed to detect content type: %s", err.Error()) return } if nil == body { log.Errorf("empty attachment") return nil, errors.New("generating attachment failed") } // Step 2: // Initiate and then make the upload. prc, pwc := io.Pipe() mpartW := multipart.NewWriter(pwc) go func() { defer func() { _ = mpartW.Close() _ = pwc.Close() }() h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes("file"), escapeQuotes(r.Name))) //h.Set("Content-Type", "application/octet-stream") log.Infof("Content-Type: %s", contentType) h.Set("Content-Type", contentType) formFile, err := mpartW.CreatePart(h) //formFile, err := mpartW.CreateFormFile("file", r.Name) if err != nil { return } _, _ = io.Copy(formFile, body) //writeStringField(mpartW, "Content-Type", contentType) //writeStringField(mpartW, "resource_subtype", "asana") //writeStringField(mpartW, "name", "20210811065901") }() fullURL := fmt.Sprintf("%s/api/1.0/tasks/%s/attachments", util.AsanaHost, r.TaskID) req, err := http.NewRequest("POST", fullURL, prc) if err != nil { log.Errorf("failed to build attachment request: %s", err.Error()) return nil, err } req.Header.Set("Content-Type", mpartW.FormDataContentType()) req.Header.Set("Authorization", r.PAToken) buf, err := util.Request(req) if nil != err { return nil, err } err = json.Unmarshal(buf, &data) if nil != err { log.Errorf("[%s][%s]failed to upload attachments: %s", r.TicketID, r.TaskID, err.Error()) return nil, err } return &data.Response, err } func fDetectContentType(r io.Reader) (string, io.Reader, error) { if r == nil { return "", nil, errors.New("empty attachments") } seeker, seekable := r.(io.Seeker) sniffBuf := make([]byte, 512) n, err := io.ReadAtLeast(r, sniffBuf, 1) if err != nil { log.Errorf(err.Error()) return "", nil, err } contentType := http.DetectContentType(sniffBuf) needsRepad := !seekable if seekable { if _, err = seeker.Seek(int64(-n), io.SeekCurrent); err != nil { // Since we failed to rewind it, mark it as needing repad needsRepad = true } } if needsRepad { r = io.MultiReader(bytes.NewReader(sniffBuf), r) } return contentType, r, nil } func writeStringField(w *multipart.Writer, key, value string) { fw, err := w.CreateFormField(key) if err == nil { _, _ = io.WriteString(fw, value) } } var ( errValidation = errors.New("invalid request") ) func download(src, filename string) error { if "" == src || "" == filename { return errValidation } f, err := os.Create(filename) if nil != err { log.Errorf("failed to create attachments") return errors.New("failed to create file") } defer f.Close() //defer os.Remove(filename) resp, err := http.Get(src) if nil != err { log.Infof("download report failed: %s", err.Error()) return err } defer resp.Body.Close() _, err = io.Copy(f, resp.Body) return err }