上传图片和文件服务 您所在的位置:网站首页 头条上传图片 上传图片和文件服务

上传图片和文件服务

2024-07-09 08:31| 来源: 网络整理| 查看: 265

2.8 上传图片和文件服务

在处理文章模块时,你会发现 blog_article 表中的封面图片地址(cover_image_url)是我们直接手动传入的一个虚构地址,那么在实际的应用中,一般不同的架构分层有多种处理方式,例如:由浏览器端调用前端应用,前端应用(客户端)再调用服务端进行上传,第二种是浏览器端直接调用服务端接口上传文件,再调用服务器端的其它业务接口完成业务属性填写。

那么在本章节我们将继续完善功能,实现文章的封面图片上传并用文件服务对外提供静态文件的访问服务,这样子在上传图片后,就可以通过约定的地址访问到该图片资源。

2.8.1 新增配置

首先我们打开项目下的 configs/config.yaml 配置文件,新增上传相关的配置,如下:

App: ... UploadSavePath: storage/uploads UploadServerUrl: http://127.0.0.1:8000/static UploadImageMaxSize: 5 # MB UploadImageAllowExts: - .jpg - .jpeg - .png

我们一共新增了四项上传文件所必须的配置项,分别代表的作用如下:

UploadSavePath:上传文件的最终保存目录。 UploadServerUrl:上传文件后的用于展示的文件服务地址。 UploadImageMaxSize:上传文件所允许的最大空间大小(MB)。 UploadImageAllowExts:上传文件所允许的文件后缀。

接下来我们要在对应的配置结构体上新增上传相关属性,打开项目下的 pkg/setting/section.go 新增代码如下:

type AppSettingS struct { ... UploadSavePath string UploadServerUrl string UploadImageMaxSize int UploadImageAllowExts []string } 2.8.2 上传文件

接下来我们要编写一个上传文件的工具库,它的主要功能是针对上传文件时的一些相关处理。我们在项目的 pkg 目录下新建 util 目录,并创建 md5.go 文件,写入如下代码:

func EncodeMD5(value string) string { m := md5.New() m.Write([]byte(value)) return hex.EncodeToString(m.Sum(nil)) }

该方法用于针对上传后的文件名格式化,简单来讲,将文件名 MD5 后再进行写入,防止直接把原始名称就暴露出去了。接下来我们在项目的 pkg/upload 目录下新建 file.go 文件,代码如下:

type FileType int const TypeImage FileType = iota + 1 func GetFileName(name string) string { ext := GetFileExt(name) fileName := strings.TrimSuffix(name, ext) fileName = util.EncodeMD5(fileName) return fileName + ext } func GetFileExt(name string) string { return path.Ext(name) } func GetSavePath() string { return global.AppSetting.UploadSavePath }

在上述代码中,我们用到了两个比较常见的语法,首先是我们定义了 FileType 为 int 的类型别名,并且利用 FileType 作为类别标识的基础类型,并 iota 作为了它的初始值,那么 iota 又是什么呢?

实际上,在 Go 语言中 iota 相当于是一个 const 的常量计数器,你也可以理解为枚举值,第一个声明的 iota 的值为 0,在新的一行被使用时,它的值都会自动递增。

当然了,你也可以像代码中那样,在初始的第一个声明时进行手动加一,那么它将会从 1 开始递增。那么为什么我们要在 FileType 类型中使用 iota 的枚举呢,其实本质上是为了后续有其它的需求,能标准化的进行处理,例如:

const ( TypeImage FileType = iota + 1 TypeExcel TypeTxt )

如果未来再有其它的上传文件类型支持,这么看,是不是就很清晰了呢,不再像以前,你还要手工定义 1, 2, 3, 4….非常麻烦。

另外我们还一共声明了四个文件相关的方法,其作用分别如下:

GetFileName:获取文件名称,先是通过获取文件后缀并筛出原始文件名进行 MD5 加密,最后返回经过加密处理后的文件名。 GetFileExt:获取文件后缀,主要是通过调用 path.Ext 方法进行循环查找”.“符号,最后通过切片索引返回对应的文化后缀名称。 GetSavePath:获取文件保存地址,这里直接返回配置中的文件保存目录即可,也便于后续的调整。

在完成了文件相关参数获取的方法后,接下来我们需要编写检查文件的相关方法,因为需要确保在文件写入时它已经达到了必备条件,否则要给出对应的标准错误提示,继续在文件内新增如下代码:

func CheckSavePath(dst string) bool { _, err := os.Stat(dst) return os.IsNotExist(err) } func CheckContainExt(t FileType, name string) bool { ext := GetFileExt(name) ext = strings.ToUpper(ext) switch t { case TypeImage: for _, allowExt := range global.AppSetting.UploadImageAllowExts { if strings.ToUpper(allowExt) == ext { return true } } } return false } func CheckMaxSize(t FileType, f multipart.File) bool { content, _ := ioutil.ReadAll(f) size := len(content) switch t { case TypeImage: if size >= global.AppSetting.UploadImageMaxSize*1024*1024 { return true } } return false } func CheckPermission(dst string) bool { _, err := os.Stat(dst) return os.IsPermission(err) } CheckSavePath:检查保存目录是否存在,通过调用 os.Stat 方法获取文件的描述信息 FileInfo,并调用 os.IsNotExist 方法进行判断,其原理是利用 os.Stat 方法所返回的 error 值与系统中所定义的 oserror.ErrNotExist 进行判断,以此达到校验效果。 CheckPermission:检查文件权限是否足够,与 CheckSavePath 方法原理一致,是利用 oserror.ErrPermission 进行判断。 CheckContainExt:检查文件后缀是否包含在约定的后缀配置项中,需要的是所上传的文件的后缀有可能是大写、小写、大小写等,因此我们需要调用 strings.ToUpper 方法统一转为大写(固定的格式)来进行匹配。 CheckMaxSize:检查文件大小是否超出最大大小限制。

在完成检查文件的一些必要操作后,我们就可以涉及文件写入/创建的相关操作,继续在文件内新增如下代码:

func CreateSavePath(dst string, perm os.FileMode) error { err := os.MkdirAll(dst, perm) if err != nil { return err } return nil } func SaveFile(file *multipart.FileHeader, dst string) error { src, err := file.Open() if err != nil { return err } defer src.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, src) return err } CreateSavePath:创建在上传文件时所使用的保存目录,在方法内部调用的 os.MkdirAll 方法,该方法将会以传入的 os.FileMode 权限位去递归创建所需的所有目录结构,若涉及的目录均已存在,则不会进行任何操作,直接返回 nil。 SaveFile:保存所上传的文件,该方法主要是通过调用 os.Create 方法创建目标地址的文件,再通过 file.Open 方法打开源地址的文件,结合 io.Copy 方法实现两者之间的文件内容拷贝。 2.8.3 新建 service 方法

我们将上一步所编写的上传文件工具库与我们具体的业务接口结合起来,我们在项目下的 internal/service 目录新建 upload.go 文件,写入如下代码:

type FileInfo struct { Name string AccessUrl string } func (svc *Service) UploadFile(fileType upload.FileType, file multipart.File, fileHeader *multipart.FileHeader) (*FileInfo, error) { fileName := upload.GetFileName(fileHeader.Filename) if !upload.CheckContainExt(fileType, fileName) { return nil, errors.New("file suffix is not supported.") } if upload.CheckMaxSize(fileType, file) { return nil, errors.New("exceeded maximum file limit.") } uploadSavePath := upload.GetSavePath() if upload.CheckSavePath(uploadSavePath) { if err := upload.CreateSavePath(uploadSavePath, os.ModePerm); err != nil { return nil, errors.New("failed to create save directory.") } } if upload.CheckPermission(uploadSavePath) { return nil, errors.New("insufficient file permissions.") } dst := uploadSavePath + "/" + fileName if err := upload.SaveFile(fileHeader, dst); err != nil { return nil, err } accessUrl := global.AppSetting.UploadServerUrl + "/" + fileName return &FileInfo{Name: fileName, AccessUrl: accessUrl}, nil }

我们在 UploadFile Service 方法中,主要是通过获取文件所需的基本信息,接着对其进行业务所需的文件检查(文件大小是否符合需求、文件后缀是否达到要求),并且判断在写入文件前对否具备必要的写入条件(目录是否存在、权限是否足够),最后在检查通过后再进行真正的写入文件操作。

2.8.4 新增业务错误码

在项目的 pkg/errcode 下的 module_code.go 文件,针对上传模块,新增如下错误代码:

var ( ... ErrorUploadFileFail = NewError(20030001, "上传文件失败") ) 2.8.5 新增路由方法

接下来需要编写上传文件的路由方法,将整套上传逻辑给串联起来,我们在项目的 internal/routers/api 目录下新建 upload.go 文件,代码如下:

type Upload struct{} func NewUpload() Upload { return Upload{} } func (u Upload) UploadFile(c *gin.Context) { response := app.NewResponse(c) file, fileHeader, err := c.Request.FormFile("file") if err != nil { response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error())) return } fileType := convert.StrTo(c.PostForm("type")).MustInt() if fileHeader == nil || fileType


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有