kratos文件http下载上传

使用kratos自带http transport实现文件下载上传功能,并且可以使用中间件。

文件的下载接口可以使用proto定义,上传目前还未找到完美方法,为了统一,采用不定义在proto中定义,自行在server中实现。

为什么上传接口不能使用proto定义?
上传接口实际上可以使用proto定义,但是在解析入参时是不能解析上传的字节类型,入参的Content-Type类型只支持: x-www-form-urlencodedjsonxmlprotoyaml,而上传文件我希望以只读字节流readCloser的方式,这样可以方便使用io.Copy()的方式实现上传,而不是将文件数据转换成[]byte进行传输

看一段官方的实现方式原代码 · GitHub,已简化。

func downloadFile(ctx http.Context) error {
    f := excelize.NewFile()
    return f.Write(ctx.Response())
}
func main() {
    var opts = []http.ServerOption{
        http.Address(":8001")
    }

    httpSrv := http.NewServer(
        opts...,
    )
    route := httpSrv.Route("/")
    route.POST("/download", downloadFile)

    app := kratos.New(
        kratos.Name("download"),
        kratos.Server(
            httpSrv,
        ),
    )
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

可以看到建议的方式是直接使用httpSrv定义路由,不再使用生成的http代码注册,但是这种方式也导致了中间件逻辑的丢失。

来看另一段可以使用中间件的关键代码原代码 · GitHub,这段代码实际也是kratos根据proto生成http接口的类似代码。

func sayHelloHandler(ctx http.Context) error {
    var in helloworld.HelloRequest
    if err := ctx.BindQuery(&in); err != nil {
        return err
    }

    // binding /hello/{name} to in.Name
    if err := ctx.BindVars(&in); err != nil {
        return err
    }

    http.SetOperation(ctx, "/helloworld.Greeter/SayHello")
    h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
        return &helloworld.HelloReply{Message: "test:" + req.(*helloworld.HelloRequest).Name}, nil
    })
    return ctx.Returns(h(ctx, &in))
}

其中下面这一段就是在http接口中使用中间件的核心代码:

h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
        return &helloworld.HelloReply{Message: "test:" + req.(*helloworld.HelloRequest).Name}, nil
    })
h(ctx, &in)

ctx.Middleware中包装service层的方法,也是实现一个中间件方法,再使用h(ctx, &in)执行。

而不能使用proto定义的接口的原因也在这里

if err := ctx.BindQuery(&in); err != nil {
    return err
}

这三行会解析请求中携带的参数,如果contentType类型不为上述中的类型,那么会造成解析失败,返回错误当前类型未注册。所以在实现上传功能时采用request自带的FormFile("file")方法获取文件流。

将这两种方式结合,下载接口代码示例,上传同理:

func downloadFile(ctx context.Context, _ *http.Request, w http.ResponseWriter) error {
    f := excelize.NewFile()
    return f.Write(w)
    // 或
    // f := io.Open("xxx")
    // _, err = io.Copy(w, f)
}

func downloadWrapper(ctx http.Context) error {  
    h := ctx.Middleware(func(cc context.Context, req interface{}) (interface{}, error) {  
       return nil, downloadFile(cc, ctx.Request(), ctx.Response())  
    })  
    _, err = h(ctx, nil)  
    if err != nil {  
       return err  
    }  
    return nil  
}

func main() {
    var opts = []http.ServerOption{
        http.Address(":8001")
        http.Middleware(// 不要忘记注册中间件
            recovery.Recovery(),  
            logging.Server(logger),
        )
    }

    httpSrv := http.NewServer(
        opts...,
    )
    route := httpSrv.Route("/")
    route.Get("/download", downloadWrapper)

    app := kratos.New(
        kratos.Name("download"),
        kratos.Server(
            httpSrv,
        ),
    )
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

在进行路由注册时,不再直接注册处理方法,而是注册route.POST("/download", downloadWrapper),由downloadWrapper内部使用中间件后调用downloadFile方法。
目前我是将main方法中注册路由部分的代码放在server层,将downloadWrapper方法放在service层,downloadFile放在biz层。
如果希望读取到参数,如果是下载接口的话,是可以使用

if err := ctx.BindVars(&in); err != nil {
    return err
}

来获取请求数据的,wrapper方法中获取数据后,在downloadFile增加一个入参传过去就可以了,当然也可以直接从request读取。
目前上传没有什么太优雅的方式获取到上传文件流,只能用request中获取数据到。