@@ 5,18 5,23 @@ import (
"context"
"encoding/json"
"fmt"
+ "io"
"io/ioutil"
+ "mime/multipart"
"net/http"
+ "net/textproto"
"git.sr.ht/~sircmpwn/core-go/config"
"git.sr.ht/~sircmpwn/core-go/crypto"
+ "github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type GraphQLQuery struct {
- Query string `json:"query"`
- Variables map[string]interface{} `json:"variables"`
+ Query string `json:"query"`
+ Variables map[string]interface{} `json:"variables"`
+ Uploads map[string]graphql.Upload `json:"-"`
}
type InternalAuth struct {
@@ 26,13 31,8 @@ type InternalAuth struct {
}
func Do(ctx context.Context, username string, svc string,
- query GraphQLQuery, result interface{}) error {
-
- body, err := json.Marshal(query)
- if err != nil {
- panic(err) // Programmer error
- }
-
+ query GraphQLQuery, result interface{},
+) error {
conf := config.ForContext(ctx)
origin, _ := conf.Get(svc, "api-origin")
if origin == "" {
@@ 42,13 42,97 @@ func Do(ctx context.Context, username string, svc string,
panic(fmt.Errorf("No %s origin specified in config.ini", svc))
}
- reader := bytes.NewBuffer(body)
+ var (
+ contentType string
+ reader io.Reader
+ )
+
+ if len(query.Uploads) > 0 {
+ var (
+ body *multipart.Writer
+ buf bytes.Buffer
+ filemap map[string][]string
+ uploads []graphql.Upload
+ h textproto.MIMEHeader
+ )
+
+ // Prepare the "map" field which associates the index of each
+ // uploaded file to a variable name. Related 'null' values are
+ // required in the variables map.
+ filemap = make(map[string][]string)
+ uploads = make([]graphql.Upload, 0, len(query.Uploads))
+ i := 0
+ for name, upload := range query.Uploads {
+ filemap[fmt.Sprintf("%d", i)] = []string{
+ fmt.Sprintf("variables.%s", name),
+ }
+ query.Variables[name] = nil
+ uploads = append(uploads, upload)
+ i++
+ }
+
+ // Create a new multipart body
+ body = multipart.NewWriter(&buf)
+
+ // Add the GraphQLQuery object serialized as JSON as a first
+ // multipart field named "operations".
+ h = make(textproto.MIMEHeader)
+ h.Add("Content-Disposition", `form-data; name="operations"`)
+ h.Add("Content-Type", "application/json")
+ pw, _ := body.CreatePart(h)
+ if err := json.NewEncoder(pw).Encode(query); err != nil {
+ panic(err) // Programmer error
+ }
+
+ // The second multipart field is the filemap we created
+ // previously. The field name must be "map" and be serialized
+ // as JSON.
+ h = make(textproto.MIMEHeader)
+ h.Add("Content-Disposition", `form-data; name="map"`)
+ h.Add("Content-Type", "application/json")
+ pw, _ = body.CreatePart(h)
+ if err := json.NewEncoder(pw).Encode(filemap); err != nil {
+ panic(err) // Programmer error
+ }
+
+ // Finally, add one multipart field per upload. Use the upload
+ // index as field name. The filename value is optional and can
+ // remain empty. If the content-type is not set, assume
+ // application/octet-stream as default.
+ for i, upload := range uploads {
+ h = make(textproto.MIMEHeader)
+ h.Add("Content-Disposition", fmt.Sprintf(
+ `form-data; name="%d"; filename=%q`, i, upload.Filename))
+ if upload.ContentType == "" {
+ upload.ContentType = "application/octet-stream"
+ }
+ h.Add("Content-Type", upload.ContentType)
+ pw, _ = body.CreatePart(h)
+ if _, err := io.Copy(pw, upload.File); err != nil {
+ return err
+ }
+ }
+ if err := body.Close(); err != nil {
+ return err
+ }
+ reader = &buf
+ contentType = fmt.Sprintf(
+ "multipart/form-data; boundary=%s", body.Boundary())
+ } else {
+ body, err := json.Marshal(query)
+ if err != nil {
+ panic(err) // Programmer error
+ }
+ reader = bytes.NewBuffer(body)
+ contentType = "application/json"
+ }
+
req, err := http.NewRequestWithContext(ctx,
"POST", fmt.Sprintf("%s/query", origin), reader)
if err != nil {
return err
}
- req.Header.Add("Content-Type", "application/json")
+ req.Header.Add("Content-Type", contentType)
auth := InternalAuth{
Name: username,
ClientID: config.ServiceName(ctx),