开箱即用的GO后台管理系统 Kratos Admin - 如何上传文件

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
MSE Nacos/ZooKeeper 企业版试用,1600元额度,限量50份
云原生网关 MSE Higress,422元/月
简介: Kratos Admin 是一个开箱即用的 Go 语言后台管理系统,支持通过 MinIO 实现高效文件上传。系统提供两种上传方式:一是通过预签名 URL 直接上传至 MinIO,减轻服务压力;二是通过 Kratos 微服务中转上传。前者推荐使用,具备安全、高效、易扩展等优点。文章详细介绍了两种方式的实现代码及前后端交互流程,并附有完整项目源码供参考。

开箱即用的GO后台管理系统 Kratos Admin - 如何上传文件

在一个CMS和Admin系统里面,文件上传是一个极其重要的功能之一。

在Kraots-Admin里面,我们把所有的文件都落地到MinIO。MinIO是一个非常优秀的分布式文件管理系统。

通常,后端可用的有两种上传方式:

  1. 通过Kratos的服务向MinIO申请预签名URL,然后通过预签名URL向MinIO上传文件。
  2. 直接向Kratos的服务上传文件,然后,微服务再将文件落地到MinIO。

方式一,这是最优的解决方案,因为文件不会经过微服务,直接上传到MinIO,减轻了微服务的压力。并且,MinIO支持分布式部署,可以很好的扩展。

方式二,是最简单的解决方案,但是不推荐,因为文件需要微服务经手,这显然增加了微服务的压力。

向MinIO预签名URL上传文件

该方法的原理就是,微服务向MinIO发出请求,让MinIO生成一个预签名(Presigned)的链接,你可以理解成为一个有时效性的上传链接,在一定的时间内,你有权上传一个文件。在FTP时代里,我们需要向客户端/前端暴露用户名和密码,这是极其不安全的。而预签名机制则是一个安全的机制。

让我们来一步步的实现该功能。

MinIO提供了两种预签名的上传方式:

  1. PresignedPutObject 提供一个临时的HTTP PUT 操作预签名上传链接以供上传
  2. PresignedPostPolicy 提供一个临时的HTTP POST 操作预签名上传链接以供上传

我们接着就将这两个方法封装起来:

package minio

import (
    "context"
    "log"
    "net/url"
    "time"

    "github.com/minio/minio-go/v7"
    "github.com/minio/minio-go/v7/pkg/credentials"
)

const (
    defaultExpiryTime = time.Second * 24 * 60 * 60 // 1 day

    endpoint        string = "localhost:9000"
    accessKeyID     string = "root"
    secretAccessKey string = "123456789"
    useSSL          bool   = false
)

type Client struct {
   
    cli *minio.Client
}

func NewMinioClient() *Client {
   
    cli, err := minio.New(endpoint, &minio.Options{
   
        Creds:  credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
        Secure: useSSL,
    })
    if err != nil {
   
        log.Fatalln(err)
    }

    return &Client{
   
        cli: cli,
    }
}

func (c *Client) PostPresignedUrl(ctx context.Context, bucketName, objectName string) (string, map[string]string, error) {
   
    expiry := defaultExpiryTime

    policy := minio.NewPostPolicy()
    _ = policy.SetBucket(bucketName)
    _ = policy.SetKey(objectName)
    _ = policy.SetExpires(time.Now().UTC().Add(expiry))

    presignedURL, formData, err := c.cli.PresignedPostPolicy(ctx, policy)
    if err != nil {
   
        log.Fatalln(err)
        return "", map[string]string{
   }, err
    }

    return presignedURL.String(), formData, nil
}

func (c *Client) PutPresignedUrl(ctx context.Context, bucketName, objectName string) (string, error) {
   
    expiry := defaultExpiryTime

    presignedURL, err := c.cli.PresignedPutObject(ctx, bucketName, objectName, expiry)
    if err != nil {
   
        log.Fatalln(err)
        return "", err
    }

    return presignedURL.String(), nil
}

现在,我们需要定义Protobuf的API:

syntax = "proto3";

package file.service.v1;

import "gnostic/openapi/v3/annotations.proto";
import "google/api/annotations.proto";

// 文件服务
service FileService {
  // 获取对象存储(OSS)上传链接
  rpc OssUploadUrl (OssUploadUrlRequest) returns (OssUploadUrlResponse) {
    option (google.api.http) = {
      post: "/admin/v1/file:upload-url"
      body: "*"
    };
  }
}

// 前端上传文件所用的HTTP方法
enum UploadMethod {
  Put = 0;
  Post = 1;
}

// 获取对象存储上传链接 - 请求
message OssUploadUrlRequest {
  UploadMethod method = 1 [
    json_name = "method",
    (gnostic.openapi.v3.property) = { description: "上传文件所用的HTTP方法,支持POST和PUT" }
  ];  // 上传文件所用的HTTP方法

  optional string content_type = 2 [
    json_name = "contentType",
    (gnostic.openapi.v3.property) = { description: "文件的MIME类型" }
  ];  // 文件的MIME类型

  optional string bucket_name = 3 [
    json_name = "bucketName",
    (gnostic.openapi.v3.property) = { description: "文件桶名称,如果不填写,将会根据文件名或者MIME类型进行自动解析" }
  ]; // 文件桶名称,如果不填写,将会根据文件名或者MIME类型进行自动解析。

  optional string file_path = 4 [
    json_name = "filePath",
    (gnostic.openapi.v3.property) = { description: "远端的文件路径,可以不填写" }
  ]; // 远端的文件路径,可以不填写。

  optional string file_name = 5 [
    json_name = "fileName",
    (gnostic.openapi.v3.property) = { description: "文件名,如果不填写,则会生成UUID,有同名文件也会改为UUID" }
  ]; // 文件名,如果不填写,则会生成UUID,有同名文件也会改为UUID。
}

// 获取对象存储上传链接 - 回应
message OssUploadUrlResponse {
  string upload_url = 1 [
    json_name = "uploadUrl",
    (gnostic.openapi.v3.property) = { description: "文件的上传链接,默认1个小时的过期时间" }
  ]; // 文件的上传链接,默认1个小时的过期时间。

  string download_url = 2 [
    json_name = "downloadUrl",
    (gnostic.openapi.v3.property) = { description: "文件的下载链接" }
  ]; // 文件的下载链接

  optional string bucket_name = 3 [
    json_name = "bucketName",
    (gnostic.openapi.v3.property) = { description: "文件桶名称" }
  ]; // 文件桶名称

  string object_name = 4 [
    json_name = "objectName",
    (gnostic.openapi.v3.property) = { description: "文件名" }
  ];  // 文件名

  map<string, string> form_data = 5 [
    json_name = "formData",
    (gnostic.openapi.v3.property) = { description: "表单数据,使用POST方法时填写" }
  ];
}

写好了API之后,接着来实现服务:

package service

import (
    "context"

    "github.com/go-kratos/kratos/v2/log"
    "github.com/tx7do/go-utils/trans"

    "kratos-admin/app/admin/service/internal/data"

    adminV1 "kratos-admin/api/gen/go/admin/service/v1"
    fileV1 "kratos-admin/api/gen/go/file/service/v1"
)

type FileService struct {
   
    adminV1.FileServiceHTTPServer

    log *log.Helper

    mc *data.MinIOClient
}

func NewFileService(logger log.Logger, mc *data.MinIOClient) *FileService {
   
    l := log.NewHelper(log.With(logger, "module", "file/service/admin-service"))
    return &FileService{
   
        log: l,
        mc:  mc,
    }
}

func (s *FileService) OssUploadUrl(ctx context.Context, req *fileV1.OssUploadUrlRequest) (*fileV1.OssUploadUrlResponse, error) {
   
    return s.mc.OssUploadUrl(ctx, req)
}

到这里,服务的逻辑就实现好了。

前端的调用流程是:

  1. 前端向/admin/v1/file:upload-url这个API申请MinIO的预签名链接;
  2. 前端拿到了预签名的上传链接,向该链接上传文件。

直接向Kratos的服务上传文件

该方法的核心要点就是把文件打进FormData。后端服务解析FormData即可。

需要注意的是,Kratos的代码生成器不能够将Protobuf上传文件的API生成成go代码。这就是我在上面提到的需要手工代码的地方。

让我们先定义API:

syntax = "proto3";

package admin.service.v1;

import "gnostic/openapi/v3/annotations.proto";
import "google/api/annotations.proto";

// 文件服务
service FileService {
  // POST方法上传文件
  rpc PostUploadFile (stream UploadFileRequest) returns (UploadFileResponse) {
    option (google.api.http) = {
      post: "/admin/v1/file:upload"
      body: "*"
    };
  }

  // PUT方法上传文件
  rpc PutUploadFile (stream UploadFileRequest) returns (UploadFileResponse) {
    option (google.api.http) = {
      put: "/admin/v1/file:upload"
      body: "*"
    };
  }
}

message UploadFileRequest {
  optional string bucket_name = 1 [
    json_name = "bucketName",
    (gnostic.openapi.v3.property) = { description: "文件桶名称" }
  ]; // 文件桶名称

  optional string object_name = 2 [
    json_name = "objectName",
    (gnostic.openapi.v3.property) = { description: "文件名" }
  ]; // 文件名

  optional bytes file = 3 [
    json_name = "file",
    (gnostic.openapi.v3.property) = { description: "文件内容" }
  ]; // 文件内容
}

message UploadFileResponse {
  string url = 1;
}

当你生成了API的代码之后,你可以查看i_file_http.pb.go这个生成代码,你会发现,哦吼,里边并没有这两个接口的处理代码。于是,我们下面就需要手工搓一个,我们把它放到微服务的server包下:

package server

import (
    "context"
    "io"
    "strings"

    "github.com/go-kratos/kratos/v2/transport/http"

    "kratos-admin/app/admin/service/internal/service"

    fileV1 "kratos-admin/api/gen/go/file/service/v1"
)

func registerFileUploadHandler(srv *http.Server, svc *service.FileService) {
   
    r := srv.Route("/")
    r.POST("admin/v1/file:upload", _FileService_PostUploadFile_HTTP_Handler(svc))
    r.PUT("admin/v1/file:upload", _FileService_PutUploadFile_HTTP_Handler(svc))
}

const OperationFileServicePostUploadFile = "/admin.service.v1.FileService/PostUploadFile"
const OperationFileServicePutUploadFile = "/admin.service.v1.FileService/PutUploadFile"

func _FileService_PostUploadFile_HTTP_Handler(svc *service.FileService) func(ctx http.Context) error {
   
    return func(ctx http.Context) error {
   
        http.SetOperation(ctx, OperationFileServicePostUploadFile)

        var in fileV1.UploadFileRequest
        var err error

        var aFile *fileV1.File

        file, header, err := ctx.Request().FormFile("file")
        if err == nil {
   
            defer file.Close()

            b := new(strings.Builder)
            _, err = io.Copy(b, file)

            aFile = &fileV1.File{
   
                FileName: header.Filename,
                Mime:     header.Header.Get("Content-Type"),
                Content:  []byte(b.String()),
            }
        }

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

        h := ctx.Middleware(func(ctx context.Context, req interface{
   }) (interface{
   }, error) {
   
            return svc.PostUploadFile(ctx, req.(*fileV1.UploadFileRequest), aFile)
        })

        // 逻辑处理,取数据
        out, err := h(ctx, &in)
        if err != nil {
   
            return err
        }

        reply := out.(*fileV1.UploadFileResponse)

        return ctx.Result(200, reply)
    }
}

func _FileService_PutUploadFile_HTTP_Handler(svc *service.FileService) func(ctx http.Context) error {
   
    return func(ctx http.Context) error {
   
        http.SetOperation(ctx, OperationFileServicePutUploadFile)

        var in fileV1.UploadFileRequest
        var err error

        var aFile *fileV1.File

        file, header, err := ctx.Request().FormFile("file")
        if err == nil {
   
            defer file.Close()

            b := new(strings.Builder)
            _, err = io.Copy(b, file)

            aFile = &fileV1.File{
   
                FileName: header.Filename,
                Mime:     header.Header.Get("Content-Type"),
                Content:  []byte(b.String()),
            }
        }

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

        h := ctx.Middleware(func(ctx context.Context, req interface{
   }) (interface{
   }, error) {
   
            return svc.PutUploadFile(ctx, req.(*fileV1.UploadFileRequest), aFile)
        })

        // 逻辑处理,取数据
        out, err := h(ctx, &in)
        if err != nil {
   
            return err
        }

        reply := out.(*fileV1.UploadFileResponse)

        return ctx.Result(200, reply)
    }
}

然后,把它注册进HTTP服务器:

// NewRESTServer new an HTTP server.
func NewRESTServer(
    cfg *conf.Bootstrap,
    logger log.Logger,

    fileSvc *service.FileService,
) *http.Server {
   
    adminV1.RegisterFileServiceHTTPServer(srv, fileSvc)
    registerFileUploadHandler(srv, fileSvc)
}

在这个时候,我们才真正的拥有了这两个接口。

直接向MinIO上传文件,MinIO提供了两个接口:

  1. putObject 从流上传
  2. fPutObject 从文件上传

我们这里使用的是流式上传,所以使用的是putObject,将之封装一下,以供服务调用:


func (c *MinIOClient) UploadFile(ctx context.Context, bucketName string, objectName string, file []byte) (string, error) {
   
    reader := bytes.NewReader(file)

    _, err := c.mc.PutObject(
        ctx,
        bucketName,
        objectName,
        reader, reader.Size(),
        minio.PutObjectOptions{
   },
    )
    if err != nil {
   
        return "", err
    }

    downloadUrl := "/" + bucketName + "/" + objectName

    return downloadUrl, nil
}

这时候就可以实现服务的实现代码了:


func (s *FileService) PostUploadFile(ctx context.Context, req *fileV1.UploadFileRequest, file *fileV1.File) (*fileV1.UploadFileResponse, error) {
   
    if file == nil {
   
        return nil, fileV1.ErrorUploadFailed("unknown file")
    }

    if req.BucketName == nil {
   
        req.BucketName = trans.Ptr(s.mc.ContentTypeToBucketName(file.Mime))
    }
    if req.ObjectName == nil {
   
        req.ObjectName = trans.Ptr(file.FileName)
    }

    downloadUrl, err := s.mc.UploadFile(ctx, req.GetBucketName(), req.GetObjectName(), file.Content)
    return &fileV1.UploadFileResponse{
   
        Url: downloadUrl,
    }, err
}

func (s *FileService) PutUploadFile(ctx context.Context, req *fileV1.UploadFileRequest, file *fileV1.File) (*fileV1.UploadFileResponse, error) {
   
    if file == nil {
   
        return nil, fileV1.ErrorUploadFailed("unknown file")
    }

    if req.BucketName == nil {
   
        req.BucketName = trans.Ptr(s.mc.ContentTypeToBucketName(file.Mime))
    }
    if req.ObjectName == nil {
   
        req.ObjectName = trans.Ptr(file.FileName)
    }

    downloadUrl, err := s.mc.UploadFile(ctx, req.GetBucketName(), req.GetObjectName(), file.Content)
    return &fileV1.UploadFileResponse{
   
        Url: downloadUrl,
    }, err
}

前端只需要直接向这两个接口上传文件即可。

项目代码

目录
相关文章
|
5月前
|
JSON Cloud Native Go
开箱即用的GO后台管理系统 Kratos Admin - 后端权限控制
后端的权限控制主要分为两种: API权限控制; 数据权限控制。 在本文,我们不讨论数据权限的控制,主要讲API的权限控制。
384 1
|
5月前
|
前端开发 JavaScript Go
开箱即用的GO后台管理系统 Kratos Admin - 介绍
这是一个前后端分离的中台、后台,后端基于go、go-kratos、ent、gorm等,前端基于vue3、ts、Antdv、Vben开发。支持多租户、数据权限、动态Api、任务调度、OSS文件上传、滑块拼图验证、国内外主流数据库自由切换和动态高级查询。集成统一认证授权、事件总线、国际化、数据验证、分布式缓存、分布式事务、Ip限流、全Api鉴权、集成测试、性能分析、健康检查、接口文档等。
583 1
开箱即用的GO后台管理系统 Kratos Admin - 介绍
|
前端开发 JavaScript 数据安全/隐私保护
推荐6款超级好看的开源中后台前端框架
推荐6款超级好看的开源中后台前端框架
1926 0
|
5月前
|
SQL 中间件 Go
开箱即用的GO后台管理系统 Kratos Admin - 后端项目结构说明
Kratos Admin 是一个开箱即用的 Go 语言后台管理系统,采用 Kratos 框架构建,提供清晰的项目结构与模块化设计。目录包含 API 定义、服务代码、配置文件、数据库初始化脚本及部署工具,支持 Docker 部署与自动化构建,便于快速开发和维护企业级应用。
219 1
|
3月前
|
存储 运维 安全
金融级 ZooKeeper 来袭:性能提升100%,SLA 99.99%,数据防护升级
阿里云微服务引擎 MSE ZooKeeper 企业版正式发布,提供比专业版更高的稳定性与安全能力,SLA 达 99.99%,整体服务性能提升 100%。针对关键业务,企业版通过独享资源池实现更高规格配额,满足大规模需求。此外新增数据备份容灾、容量管理反脆弱限流等功能,提升整体企业级特性,助力企业应对复杂业务挑战。
341 137
金融级 ZooKeeper 来袭:性能提升100%,SLA 99.99%,数据防护升级
|
4月前
|
人工智能 运维 API
Dify开发者必看:如何破解MCP集成与Prompt迭代难题?
Dify 是一个面向AI时代的开源大语言模型(LLM)应用开发平台,致力于让复杂的人工智能应用构建变得简单高效,目前已在全球范围内形成显著影响力,其 GitHub 仓库 Star 数截至 2025 年 6 月已突破 100,000+,目前,Dify 已经成为 LLMOps 领域增长最快的开源项目之一。
|
4月前
|
运维 Cloud Native 应用服务中间件
阿里云微服务引擎 MSE 及 API 网关 2025 年 6 月产品动态
阿里云微服务引擎 MSE 面向业界主流开源微服务项目, 提供注册配置中心和分布式协调(原生支持 Nacos/ZooKeeper/Eureka )、云原生网关(原生支持Higress/Nginx/Envoy,遵循Ingress标准)、微服务治理(原生支持 Spring Cloud/Dubbo/Sentinel,遵循 OpenSergo 服务治理规范)能力。API 网关 (API Gateway),提供 APl 托管服务,覆盖设计、开发、测试、发布、售卖、运维监测、安全管控、下线等 API 生命周期阶段。帮助您快速构建以 API 为核心的系统架构.满足新技术引入、系统集成、业务中台等诸多场景需要。
|
5月前
|
人工智能 安全 API
Agent 工程师绕不开的必修课:API 网关 vs API 管理
本文探讨了“API管理”与“API网关”的起源、发展及差异,二者分别服务于API生命周期的不同阶段。API网关从流量网关演进至AI网关,承担运行时请求控制;API管理则从接口文档化发展到商业化平台,关注全生命周期治理。两者在实际应用中协同工作,通过分层架构和策略联动实现高效运营。未来,随着大模型应用的兴起,AI网关和MCP Server管理将成为新趋势,推动API技术迈入智能化和服务化的新阶段。
Agent 工程师绕不开的必修课:API 网关 vs API 管理
|
5月前
|
JSON 前端开发 Java
开箱即用的GO后台管理系统 Kratos Admin - 交互式API文档 Swagger UI
Kratos Admin 集成 Swagger UI,实现交互式 API 文档。通过 Buf 生成 OpenAPI 规范,并内嵌至服务,自动同步接口变动,提升调试与协作效率。
273 0
开箱即用的GO后台管理系统 Kratos Admin - 交互式API文档 Swagger UI
|
3月前
|
运维 监控 测试技术
2025年微服务架构关键知识点(一):核心原则与演进趋势
微服务架构凭借其高可用性、灵活扩展等优势,已成为2025年主流软件开发范式。本文深入解析微服务的核心原则、演进趋势及实践要点,助力开发者夯实基础,应对挑战,构建高效、稳定的系统架构。

热门文章

最新文章