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
202 lines
4.4 KiB
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
|
|
}
|