-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Handling Binary File Uploads #500
Comments
@davejohnston, thanks for the inquery. Unfortunately at this moment we don't support and don't have a plan to support file uploads. You can see some discussion of the topic here. If you were interested in giving it a go and documenting it I would love to have some notes on strategies that work/don't work for this problem. Wanna give it a try? |
2017.... why no close |
I actually get it managed by adding custom routes in runtime Mux. |
@rosspatil How did you do it? Can you share an example? |
@rosspatil can you share some details on how you did it? I'm also looking for a solution . Thanks! |
I did it like so with a custom route: ...
mux := runtime.NewServeMux()
...
// Attachment upload from http/s handled manually
if err := mux.HandlePath("POST", "/v1/publisher/attach/{type}/{identifier}", h.AttachmentHandler); err != nil {
panic(err)
} and the func (r *RequestHandler) AttachmentHandler(w http.ResponseWriter, rq *http.Request, params map[string]string) {
...
f, header, err := rq.FormFile("attachment")
if err != nil {
zap.L().Warn("Failed getting file from upload", zap.Error(err))
writeErr(http.StatusInternalServerError, err.Error(), w)
return
}
defer f.Close()
// Get type, identifier from params
ofType, ok := params["type"]
if !ok {
writeErr(http.StatusBadRequest, "Missing 'type' param", w)
return
}
identifier, ok := params["identifier"]
if !ok {
writeErr(http.StatusBadRequest, "Missing 'identifier' param", w)
return
}
err = r.store.Attach(ofType, identifier, header.Filename, f)
if err != nil {
writeErr(http.StatusInternalServerError, err.Error(), w)
return
}
w.WriteHeader(http.StatusOK)
} |
Hi @purebadger, would you be willing to contribute your solution to our docs to help users in the future? |
Sure, how do I do this? |
I think a new file in https://github.com/grpc-ecosystem/grpc-gateway/tree/master/docs/docs/mapping would be a good place to start. You can follow the general format provided by the other docs pages, and check out https://grpc-ecosystem.github.io/grpc-gateway/ to see what it looks like online. Let me know if you need anymore pointers! |
Hope this example will help you to achieve binary file uploads -
|
@johanbrandhorst If you don't mind, Should I raise a PR for the above example in the doc section -> https://github.com/grpc-ecosystem/grpc-gateway/tree/master/docs/docs/operations for binary file uploads? |
Hi Ross, thanks for sharing your solution. Lets give @purebadger a little more time to make their contribution if they wish, I did invite them first 🙂. |
Hi @johanbrandhorst, Ok no problem. Let me know if you want me to add it. Thanks 🙂 |
@jonathanbp = @purebadger ;) |
Update docs/docs/mapping/binary_file_uploads.md Co-authored-by: Johan Brandhorst-Satzkorn <[email protected]> Update docs/docs/mapping/binary_file_uploads.md Co-authored-by: Johan Brandhorst-Satzkorn <[email protected]> Fix example up from PR comments
Hi Folks, I was able to manually modify the swagger.json (added the formData param), and was able to upload the content through envoy, from swagger UI. Any chance to fix the protoc-gen-grpc-gateway to auto generate this param entry for files upload? Thanks
|
That's super cool! What exactly did you have to change, just the |
@johanbrandhorst yes just the body param section |
Hm, I can't see any obvious way in which we could infer that it should use
I don't know how we'd be able to tell that something should use Also, this feature is asking for us to support something that only works with envoy as a proxy right? I'd sooner we were able to actually support envoys behaviour in the gateway mux. |
mark |
After reading the source code, I found a way, but it's a bit ungraceful. idea:
proto file: message Stuff {
string md5 = 1;
string type = 2;
}
service XXX {
rpc CreateStuff(stream google.api.HttpBody) returns(Stuff) {
option (google.api.http) = {
post: "/stuffs"
body: "*"
};
}
}
grpc server: func (s *XXXServer) CreateStuff(css pb.XXX_CreateStuffServer) (err error) {
ctx := css.Context()
contentType := ""
if meta, ok := metadata.FromIncomingContext(ctx); ok {
if ct := meta.Get("X-Content-Type"); len(ct) > 0 {
contentType = ct[0]
}
}
hasher := md5.New()
for {
body, err := css.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
hasher.Write(body.Data)
}
return css.SendAndClose(&pb.Stuff{
Md5: hex.EncodeToString(hasher.Sum(nil)),
Type: contentType,
})
} gateway server: type RawBinaryUnmarshaler runtime.HTTPBodyMarshaler
func NewRawBinaryUnmarshaler() *RawBinaryUnmarshaler {
return &RawBinaryUnmarshaler{
// just the built-in default marshaler,
// to handle the outgoing marshalling
Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
EmitUnpopulated: true,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
},
},
}
}
func (m *RawBinaryUnmarshaler) NewDecoder(r io.Reader) runtime.Decoder {
return &BinaryDecoder{"Data", r}
}
type BinaryDecoder struct {
fieldName string
r io.Reader
}
func (d *BinaryDecoder) fn() string {
if d.fieldName == "" {
return "Data"
}
return d.fieldName
}
var typeOfBytes = reflect.TypeOf([]byte(nil))
func (d *BinaryDecoder) Decode(v interface{}) error {
rv := reflect.ValueOf(v).Elem() // assert it must be a pointer
if rv.Kind() != reflect.Struct {
return d
}
data := rv.FieldByName(d.fn())
if !data.CanSet() || data.Type() != typeOfBytes {
return d
}
// if only `google.api.HttpBody` is used, the above reflect
// actions can also be changed to the type assertion:
// httpBody, ok := v.(*httpbody.HttpBody)
p, err := io.ReadAll(d.r)
if err != nil {
return err
}
if len(p) == 0 {
return io.EOF
}
data.SetBytes(p)
return err
}
func (d *BinaryDecoder) Error() string {
d.r = nil
return "cannot set: " + d.fn()
}
func HeaderToMetadata(ctx context.Context, r *http.Request) metadata.MD {
md := metadata.New(nil)
setter := func(k string, newKey ...func(old string) string) {
if v := r.Header.Values(k); len(v) > 0 {
if len(newKey) > 0 {
k = newKey[0](k)
}
md.Set(k, v...)
}
}
setter("X-Content-Type")
setter("Content-Length", func(old string) string {
return "X-" + old
})
return md
}
// ...
muxOpts := []runtime.ServeMuxOption{
runtime.WithMetadata(HeaderToMetadata),
runtime.WithMarshalerOption(
"application/octet-stream",
NewRawBinaryUnmarshaler(),
),
}
mux := runtime.NewServeMux(muxOpts...) test by curl: [root@trial /tmp]# dd if=/dev/urandom of=./1MB.bin bs=1K count=1024
1024+0 records in
1024+0 records out
1048576 bytes (1.0 MB) copied, 0.014323 s, 73.2 MB/s
[root@trial /tmp]# md5sum 1MB.bin
e39b990d8cd32d01b1fed6ef16954c6d 1MB.bin
[root@trial /tmp]# curl -v -X POST http://localhost:8080/v1/stuffs --data-binary "@/tmp/1MB.bin" -H "Content-Type: application/octet-stream" -H "X-Content-Type: application/what-ever"
* About to connect() to localhost port 8080 (#0)
* Trying ::1...
* Connection refused
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /v1/stuffs HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> Content-Type: application/octet-stream
> X-Content-Type: application/what-ever
> Content-Length: 1048576
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Wed, 29 Mar 2023 18:26:12 GMT
< Content-Length: 73
<
* Connection #0 to host localhost left intact
{"md5":"e39b990d8cd32d01b1fed6ef16954c6d","type":"application/what-ever"}
|
👋 hello, I've solved this in my gRPC-transcoding project https://github.com/emcfarlane/larking by letting the handler access the underlying reader/writer stream. The API is: func AsHTTPBodyReader(stream grpc.ServerStream, msg proto.Message) (io.Reader, error)
func AsHTTPBodyWriter(stream grpc.ServerStream, msg proto.Message) (io.Writer, error) Which handles asserting the stream is a stream of google.api.HttpBody and correctly unmarshals the first payloads. So if you have an API like: import "google/api/httpbody.proto";
service Files {
rpc LargeUploadDownload(stream UploadFileRequest)
returns (stream google.api.HttpBody) {
option (google.api.http) = {
post : "/files/large/{filename}"
body : "file"
};
}
}
message UploadFileRequest {
string filename = 1;
google.api.HttpBody file = 2;
} You can use the // LargeUploadDownload echoes the request body as the response body with contentType.
func (s *asHTTPBodyServer) LargeUploadDownload(stream testpb.Files_LargeUploadDownloadServer) error {
var req testpb.UploadFileRequest
r, _ := larking.AsHTTPBodyReader(stream, &req)
log.Printf("got %s!", req.Filename)
rsp := httpbody.HttpBody{
ContentType: req.File.GetContentType(),
}
w, _ := larking.AsHTTPBodyWriter(stream, &rsp)
_, err := io.Copy(w, r)
return err
} |
Hey guys, I wrote a plugin for grpc-gateway. It can support file upload and download, and the api is directly defined in grpc proto file. https://github.com/black-06/grpc-gateway-file I am looking forward to your suggestion |
repo is not avail (github says 404) |
Sorry, I set it to private by mistake. You should be able to see it now. |
Hi,
I have a protobuf that defines one field
message FileUpload {
bytes fileContents = 1;
}
I want to be able to upload a file, and have the contents of that upload stored in fileContents. When sending json data, I understand that the keys map to grpc message fields. But in the case of a binary upload there is no field name, so how can I map the contents of the request body into a message?
If I use curl to make a request like this:
curl -X POST --data-binary "@/tmp/test_file.txt" http://localhost:9090/v1/files
The HTTP request looks like this:
The text was updated successfully, but these errors were encountered: