github.com/99designs/gqlgen@v0.17.45/client/withfilesoption.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "mime/multipart" 9 "net/http" 10 "net/textproto" 11 "os" 12 "strings" 13 ) 14 15 type fileFormDataMap struct { 16 mapKey string 17 file *os.File 18 } 19 20 func findFiles(parentMapKey string, variables map[string]interface{}) []*fileFormDataMap { 21 files := []*fileFormDataMap{} 22 for key, value := range variables { 23 if v, ok := value.(map[string]interface{}); ok { 24 files = append(files, findFiles(parentMapKey+"."+key, v)...) 25 } else if v, ok := value.([]map[string]interface{}); ok { 26 for i, arr := range v { 27 files = append(files, findFiles(fmt.Sprintf(`%s.%s.%d`, parentMapKey, key, i), arr)...) 28 } 29 } else if v, ok := value.([]*os.File); ok { 30 for i, file := range v { 31 files = append(files, &fileFormDataMap{ 32 mapKey: fmt.Sprintf(`%s.%s.%d`, parentMapKey, key, i), 33 file: file, 34 }) 35 } 36 } else if v, ok := value.(*os.File); ok { 37 files = append(files, &fileFormDataMap{ 38 mapKey: parentMapKey + "." + key, 39 file: v, 40 }) 41 } 42 } 43 44 return files 45 } 46 47 // WithFiles encodes the outgoing request body as multipart form data for file variables 48 func WithFiles() Option { 49 return func(bd *Request) { 50 bodyBuf := &bytes.Buffer{} 51 bodyWriter := multipart.NewWriter(bodyBuf) 52 53 // -b7955bd2e1d17b67ac157b9e9ddb6238888caefc6f3541920a1debad284d 54 // Content-Disposition: form-data; name="operations" 55 // 56 // {"query":"mutation ($input: Input!) {}","variables":{"input":{"file":{}}} 57 requestBody, _ := json.Marshal(bd) 58 bodyWriter.WriteField("operations", string(requestBody)) 59 60 // --b7955bd2e1d17b67ac157b9e9ddb6238888caefc6f3541920a1debad284d 61 // Content-Disposition: form-data; name="map" 62 // 63 // `{ "0":["variables.input.file"] }` 64 // or 65 // `{ "0":["variables.input.files.0"], "1":["variables.input.files.1"] }` 66 // or 67 // `{ "0": ["variables.input.0.file"], "1": ["variables.input.1.file"] }` 68 // or 69 // `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` 70 mapData := "" 71 filesData := findFiles("variables", bd.Variables) 72 filesGroup := [][]*fileFormDataMap{} 73 for _, fd := range filesData { 74 foundDuplicate := false 75 for j, fg := range filesGroup { 76 f1, _ := fd.file.Stat() 77 f2, _ := fg[0].file.Stat() 78 if os.SameFile(f1, f2) { 79 foundDuplicate = true 80 filesGroup[j] = append(filesGroup[j], fd) 81 } 82 } 83 84 if !foundDuplicate { 85 filesGroup = append(filesGroup, []*fileFormDataMap{fd}) 86 } 87 } 88 if len(filesGroup) > 0 { 89 mapDataFiles := []string{} 90 91 for i, fileData := range filesGroup { 92 mapDataFiles = append( 93 mapDataFiles, 94 fmt.Sprintf(`"%d":[%s]`, i, strings.Join(collect(fileData, wrapMapKeyInQuotes), ",")), 95 ) 96 } 97 98 mapData = `{` + strings.Join(mapDataFiles, ",") + `}` 99 } 100 bodyWriter.WriteField("map", mapData) 101 102 // --b7955bd2e1d17b67ac157b9e9ddb6238888caefc6f3541920a1debad284d 103 // Content-Disposition: form-data; name="0"; filename="tempFile" 104 // Content-Type: text/plain; charset=utf-8 105 // or 106 // Content-Type: application/octet-stream 107 // 108 for i, fileData := range filesGroup { 109 h := make(textproto.MIMEHeader) 110 h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%d"; filename="%s"`, i, fileData[0].file.Name())) 111 b, _ := os.ReadFile(fileData[0].file.Name()) 112 h.Set("Content-Type", http.DetectContentType(b)) 113 ff, _ := bodyWriter.CreatePart(h) 114 ff.Write(b) 115 } 116 bodyWriter.Close() 117 118 bd.HTTP.Body = io.NopCloser(bodyBuf) 119 bd.HTTP.Header.Set("Content-Type", bodyWriter.FormDataContentType()) 120 } 121 } 122 123 func collect(strArr []*fileFormDataMap, f func(s *fileFormDataMap) string) []string { 124 result := make([]string, len(strArr)) 125 for i, str := range strArr { 126 result[i] = f(str) 127 } 128 return result 129 } 130 131 func wrapMapKeyInQuotes(s *fileFormDataMap) string { 132 return fmt.Sprintf("\"%s\"", s.mapKey) 133 }