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

Support of File Uploads #342

Closed
dtrinh100 opened this issue Sep 13, 2018 · 18 comments · Fixed by #655
Closed

Support of File Uploads #342

dtrinh100 opened this issue Sep 13, 2018 · 18 comments · Fixed by #655
Labels
enhancement New feature or request

Comments

@dtrinh100
Copy link

Are there any plans to support file uploads via: https://github.com/jaydenseric/graphql-multipart-request-spec?

@vetcher
Copy link
Contributor

vetcher commented Sep 24, 2018

Why you want to upload files via graphql?

@dtrinh100
Copy link
Author

@vetcher I would like an easy way to do file uploads in GraphQL, using a spec that works with the Apollo Client.

@vektah vektah added the enhancement New feature or request label Oct 2, 2018
@icco
Copy link
Contributor

icco commented Nov 19, 2018

I too would like this, just to keep my server only having one endpoint, and to save on duplication of things like authentication logic.

@sneko
Copy link

sneko commented Dec 6, 2018

I'm also interested 😄

Does anyone have seen a Go implementation of this specification?

Thanks,

cc @vektah @vvakame

@MtthwBrwng
Copy link

MtthwBrwng commented Dec 6, 2018

I'm interested as-well.

@sneko The closest thing I've seen would be this project graphql-upload. Though it seems to be made for graphql-go.

@dehypnosis
Copy link

dehypnosis commented Feb 22, 2019

I made a hacky workaround to use gqlgen graphql handler.... while handling multipart/form-data request. i am using gin-gonic web server framework and this workaround works with apollo-client well.
i am waiting for this feature..!

A middleware factory which parse multipart/form-data request and remember the file streams, and then transform the original HTTP request of multipart/form-data as application/json content type.

package handler

import (
    "bytes"
    "context"
    "encoding/json"
    "github.com/99designs/gqlgen/graphql"
    "github.com/gin-gonic/gin"
    "io/ioutil"
    "log"
    "strings"
   ...
)

/* ref: GraphQL multipart file upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
this function transform multipart/form-data post request into application/json request and extract file streams
example of multipart/form-data request:

// single operation
curl localhost:3001/graphql \
  -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \
  -F map='{ "0": ["variables.file"] }' \
  -F 0=@a.txt

// batched operations
curl localhost:3001/graphql \
  -F operations='[{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }, { "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id } }", "variables": { "files": [null, null] } }]' \
  -F map='{ "0": ["0.variables.file"], "1": ["1.variables.files.0"], "2": ["1.variables.files.1"] }' \
  -F 0=@a.txt \
  -F 1=@b.txt \
  -F 2=@c.txt
*/

type operation struct {
    Query         string                 `json:"query"`
    OperationName string                 `json:"operationName"`
    Variables     map[string]interface{} `json:"variables"`
}

func parseMultipartRequest(c *gin.Context) (f graphql.RequestMiddleware) {
    f = graphql.DefaultRequestMiddleware

    if c.ContentType() != "multipart/form-data" {
        return
    }
    form, err := c.MultipartForm()
    if err != nil {
        log.Println(err.Error())
        return
    }

    operations := make([]operation, 0)
    for _, operationJSON := range form.Value["operations"] {
        op := operation{}
        err := json.Unmarshal([]byte(operationJSON), &op)
        if err != nil {
            log.Println(err.Error())
            continue
        }
        operations = append(operations, op)
    }

    fileMap := make(map[string][]string)
    err = json.Unmarshal([]byte(form.Value["map"][0]), &fileMap)
    if err != nil {
        log.Println(err.Error())
        return
    }

    files := make(map[string]*model.UploadingFile)
    for fileKey, _ := range fileMap {
        fileHeader, err := c.FormFile(fileKey)
        if err != nil {
            log.Println(fileKey, err.Error())
            continue
        }
        fileReader, err := fileHeader.Open()
        if err != nil {
            log.Println(err.Error())
            continue
        }
        files[fileKey] = &model.UploadingFile{
            Content: fileReader,
            Name: fileHeader.Filename,
            Size: fileHeader.Size,
        }
    }

    // now change the body and content-type
    // it is hacky-way until gqlgen supports multipart/form-data request by self
    var doc interface{} = operations
    if len(operations) == 1 {
        doc = operations[0]
    }
    fakeBody, err := json.Marshal(doc)
    if err != nil {
        log.Println(err.Error())
        return
    }
    c.Request.Header.Set("content-type", "application/json")
    c.Request.Body = ioutil.NopCloser(bytes.NewReader(fakeBody))

    f = func(ctx context.Context, next func(ctx context.Context) []byte) []byte {
        req := graphql.GetRequestContext(ctx)
        variables := req.Variables
        for fileKey, variableFields := range fileMap {
            for _, variableField := range variableFields {
                fields := strings.Split(variableField, ".")
                if len(fields) <= 1 || fields[0] != "variables" { // respect spec: https://github.com/jaydenseric/graphql-multipart-request-spec
                    log.Println("invalid variable field in map", variableField)
                    continue
                }
                var obj = variables
                lastIndex := len(fields) - 2
                for index, path := range fields[1:] {
                    if _, ok := obj[path]; ok {
                        if index == lastIndex {
                            // set file
                            if obj[path], ok = files[fileKey]; ok {
                                log.Println("set file", variableField, files[fileKey])
                            }
                            break
                        } else if objInObj, ok := obj[path].(map[string]interface{}); ok {
                            obj = objInObj
                            continue
                        }
                    }

                    log.Println("invalid variable field in map", variableField)
                    break
                }
            }
        }
        return next(ctx)
    }
    return
}

A scalar type which represent file uploading stream

package model

import (
    "io"
)

// scalar type
type UploadingFile struct {
    Content io.Reader
    Name string
    Size int64
}

// GraphQL JSON -> UploadingFile
func (f *UploadingFile) UnmarshalGQL(gql interface{}) (err error) {
    // this scalar type will be unmarshaled while parsing multipart/form-data body
    // ref: ../handler/multipart.go
    if v, ok := gql.(*UploadingFile); ok {
        if v != nil {
            *f = *v
        }
    }
    return
}

// UploadingFile -> GraphQL JSON (RFC3339)
func (f UploadingFile) MarshalGQL(w io.Writer) {
    w.Write([]byte("null"))
}

Now attach the middleware into graphql handler

package handler

import (
    "context"
    "github.com/99designs/gqlgen/graphql"
    graphqlHandler "github.com/99designs/gqlgen/handler"
    ...
)


...

        gqlHandler := graphqlHandler.GraphQL(
            // create root schema with context derived values
            schema.NewExecutableSchema(schema.Config{
                Resolvers:  schema.NewResolverRoot(viewer, locale),
                Directives: schema.NewDirectiveRoot(viewer),
            }),

            ....

            // multipart/form-data parsing middleware
            graphqlHandler.RequestMiddleware(parseMultipartRequest(c)),
           ...

           // other options
        )

       gqlHandler.ServeHTTP(c.Writer, c.Request)

...

@MShoaei
Copy link

MShoaei commented Feb 24, 2019

@dehypnosis would you try to make a pull request for this?

@hantonelli
Copy link

Hi @MShoaei I hope to be submiting a pull request this week. I already have it working, but I need to add some test to it. I also added support to submit the upload with a request mutation payload, so you can submit some extra fields with the file.

@mathewbyrne
Copy link
Contributor

@hantonelli if you're planning a PR can you please ensure it's against next? We aren't accepting new features against master currently.

@hantonelli
Copy link

@mathewbyrne Sure!

@mathewbyrne
Copy link
Contributor

Also, for a feature of this size it would be really great if you could write up a proposal as a separate issue with your planned approach for the development team to look at. It's really difficult to accept large PRs without a bit of context beforehand.

Sorry we're working on getting some contribution guidelines in place soon that will codify some of this.

@hantonelli
Copy link

@mathewbyrne Sure, I will.

@robert-zaremba
Copy link

Really looking forward for this feature!

@hantonelli
Copy link

@robert-zaremba @mathewbyrne I'm still working on the PR and proposal, hope to finish it soon and, that they would be good enough or at least a good start to add this feature :)

@smithaitufe
Copy link

I have been using this for about a year now.
It is just a middleware.

You can try it too.

@ghost
Copy link

ghost commented Mar 27, 2019

@smithaitufe thanks for that !

@mathewbyrne Is this planned to be integrated into the gqlgen lib ?

@mathewbyrne
Copy link
Contributor

It's not on our roadmap currently, but I think it makes sense to include at some point yes.

This was referenced Mar 31, 2019
@hantonelli
Copy link

hantonelli commented Apr 1, 2019

@mathewbyrne I finally find the time to make the proposal and the PR :)

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

Successfully merging a pull request may close this issue.