~bigbes/core-go

562662dcaf9ec186bb20458d9ed07d3d4ab61b3e — Robin Jarry 1 year, 8 months ago 2624a28
graphql: add support for multipart file uploads

Inspire from the python implementation in core.sr.ht.

Link: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/7bcbc8ba8f4e/item/srht/graphql/client.py
Signed-off-by: Robin Jarry <robin@jarry.cc>
1 files changed, 95 insertions(+), 11 deletions(-)

M client/graphql.go
M client/graphql.go => client/graphql.go +95 -11
@@ 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),