Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to upload multiple files? #85

Open
palandlom opened this issue Nov 20, 2024 · 1 comment
Open

How to upload multiple files? #85

palandlom opened this issue Nov 20, 2024 · 1 comment

Comments

@palandlom
Copy link

I try to do it in following way:

	clt := mgql.NewClient("http://0.0.0.0:7071/graphql", mgql.UseMultipartForm())

	query := `
		mutation ($files: [Upload!]!) {  
  			addLpFiles(	input: { files: $files }) 
		{success}}`
	req := mgql.NewRequest(query)

	for i, s := range []string{"aaa", "bbb"} {
		r := strings.NewReader(s)
		req.File("files."+strconv.Itoa(i), s, r)
		//req.File(strconv.Itoa(i), s, r)
	}

	err := clt.Run(ctx, req, nil)
	if err != nil {
		return err
	}

... and get error - graphql: first part must be operations.

PS: without .UseMultipartForm() usual read queries go well

@palandlom
Copy link
Author

The spec expects that there should be operations, map and file fields - e.g.:

curl localhost:3001/graphql \
  -F operations='{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }' \
  -F map='{ "0": ["variables.files.0"], "1": ["variables.files.1"] }' \
  -F [email protected] \
  -F [email protected]

Here is amended implementation of runWithPostFields

func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp interface{}) error {
	var requestBody bytes.Buffer

	writer := multipart.NewWriter(&requestBody)

	oprField, err := operationsField(req)
	if err != nil {
		return errors.Wrap(err, "encode operations")
	}
	oprWriter, err := writer.CreateFormField("operations")
	if err != nil {
		return errors.Wrap(err, "encode operations")
	}
	_, err = oprWriter.Write(oprField)
	if err != nil {
		return errors.Wrap(err, "encode operations")
	}

	mpField, err := mapField(req, err)
	if err != nil {
		return errors.Wrap(err, "encode map")
	}
	mapWriter, err := writer.CreateFormField("map")
	if err != nil {
		return errors.Wrap(err, "encode map")
	}
	_, err = mapWriter.Write(mpField)
	if err != nil {
		return errors.Wrap(err, "encode map")
	}

	// Add files fields
	for i := range req.files {
		part, err := writer.CreateFormFile(req.files[i].Field, req.files[i].Name)
		if err != nil {
			return errors.Wrap(err, "create form file")
		}
		if _, err := io.Copy(part, req.files[i].R); err != nil {
			return errors.Wrap(err, "preparing file")
		}
	}
	if err := writer.Close(); err != nil {
		return errors.Wrap(err, "close writer")
	}
	//c.logf(">> variables: %s", variablesBuf.String())
	c.logf(">> files: %d", len(req.files))
	c.logf(">> query: %s", req.q)
	gr := &graphResponse{
		Data: resp,
	}
	r, err := http.NewRequest(http.MethodPost, c.endpoint, &requestBody)
	if err != nil {
		return err
	}
	r.Close = c.closeReq
	r.Header.Set("Content-Type", writer.FormDataContentType())
	r.Header.Set("Accept", "application/json; charset=utf-8")
	for key, values := range req.Header {
		for _, value := range values {
			r.Header.Add(key, value)
		}
	}
	c.logf(">> headers: %v", r.Header)
	r = r.WithContext(ctx)
	res, err := c.httpClient.Do(r)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	var buf bytes.Buffer
	if _, err := io.Copy(&buf, res.Body); err != nil {
		return errors.Wrap(err, "reading body")
	}
	c.logf("<< %s", buf.String())
	if err := json.NewDecoder(&buf).Decode(&gr); err != nil {
		if res.StatusCode != http.StatusOK {
			return fmt.Errorf("graphql: server returned a non-200 status code: %v", res.StatusCode)
		}
		return errors.Wrap(err, "decoding response")
	}
	if len(gr.Errors) > 0 {
		// return first error
		return gr.Errors[0]
	}
	return nil
}

// mapField build map field of graphQL multipart query - returns:
//
// { "0": ["variables.files.0"], "1": ["variables.files.1"] }
func mapField(req *Request, err error) ([]byte, error) {
	mapField := map[int][]string{}
	for i := 0; i < len(req.files); i++ {
		mapField[i] = []string{fmt.Sprintf("variables.files.%d", i)}
	}
	bt, err := json.Marshal(mapField)
	if err != nil {
		return nil, err
	}
	return bt, err
}

// operationsField build operations field of graphQL multipart query returns:
//
// { "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }",
//
//	"variables": { "file": null } }'
func operationsField(req *Request) ([]byte, error) {
	opm := map[string]interface{}{}
	opm["query"] = req.q

	flsVars := make([]string, len(req.files))
	for i, _ := range flsVars {
		flsVars[i] = "null"
	}
	opm["variables"] = map[string][]string{"files": flsVars}

	bt, err := json.Marshal(opm)
	if err != nil {
		return nil, err
	}
	return bt, err
}

/ File sets a file to upload.
// Files are only supported with a Client that was created with
// the UseMultipartForm option.
//
// In accordance with https://github.com/jaydenseric/graphql-multipart-request-spec
// files should be added in the following form:
//
// "0", "fileName0.txt, fileReader0"
//
// "1", "fileName1.txt, fileReader1
//
// "N", "fileNameN.txt, fileReaderN
func (req *Request) File(fieldname, filename string, r io.Reader) {
	req.files = append(req.files, File{
		Field: fieldname, Name: filename, R: r,
	})
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant