前一节,我们介绍了文章创建和修改页面编写和操作。但是并没有处理到图片的上传问题。这里我们介绍下如何配置来支持图片上传功能。
图片上传js处理
图片上传需要时候用到js,我们使用的富文本编辑器layedit默认是支持图片上传的,但是需要我们配置一下后端接收路径,我们打开app.js,修改一下layedit编辑器的初始化参数,增加图片上传的配置:
if($('#text-editor').length) {
editorIndex = layedit.build('text-editor', {
height: 450,
uploadImage: {
url: '/attachment/upload',
type: 'post'
}
});
}
好啦,我们现在增加了uploadImage
参数,这里声明提交的方式是post,并且设置了后端接收路径为/attachment/upload
。接着我们只需要根据layedit定义的指定格式将处理结果返回回来,编辑器就会自动将图片插入到编辑器中了。
图片上传后端逻辑处理
上面我们定义了图片上传接收路径为/attachment/upload
,我们根据这个路径,创建图片处理控制器,在controller下新建一个attachment.go 文件,并添加AttachmentUpload()
函数:
package controller
import (
"github.com/kataras/iris/v12"
"irisweb/config"
"irisweb/provider"
)
func AttachmentUpload(ctx iris.Context) {
file, info, err := ctx.FormFile("file")
if err != nil {
ctx.JSON(iris.Map{
"status": config.StatusFailed,
"msg": err.Error(),
})
return
}
defer file.Close()
attachment, err := provider.AttachmentUpload(file, info)
if err != nil {
ctx.JSON(iris.Map{
"status": config.StatusFailed,
"msg": err.Error(),
})
return
}
ctx.JSON(iris.Map{
"code": config.StatusOK,
"msg": "",
"data": iris.Map{
"src": attachment.Logo,
"title": attachment.FileName,
},
})
}
这个控制器,只负责接收用户提交上来的图片,判断是否是正常提交了图片,如果不是,则返回错误,如果是则将图片的文件转交给provider.AttachmentUpload()来处理。最后将处理结果返回给前端。
我们在provider文件夹中,创建一个attachment.go文件,并添加AttachmentUpload()
函数和GetAttachmentByMd5()函数
:
package provider
import (
"bufio"
"bytes"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"github.com/nfnt/resize"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"irisweb/config"
"irisweb/library"
"irisweb/model"
"log"
"mime/multipart"
"os"
"path"
"strconv"
"strings"
"time"
)
func AttachmentUpload(file multipart.File, info *multipart.FileHeader) (*model.Attachment, error) {
db := config.DB
//获取宽高
bufFile := bufio.NewReader(file)
img, imgType, err := image.Decode(bufFile)
if err != nil {
//无法获取图片尺寸
fmt.Println("无法获取图片尺寸")
return nil, err
}
imgType = strings.ToLower(imgType)
width := uint(img.Bounds().Dx())
height := uint(img.Bounds().Dy())
fmt.Println("width = ", width, " height = ", height)
//只允许上传jpg,jpeg,gif,png
if imgType != "jpg" && imgType != "jpeg" && imgType != "gif" && imgType != "png" {
return nil, errors.New(fmt.Sprintf("不支持的图片格式:%s。", imgType))
}
if imgType == "jpeg" {
imgType = "jpg"
}
fileName := strings.TrimSuffix(info.Filename, path.Ext(info.Filename))
log.Printf(fileName)
_, err = file.Seek(0, 0)
if err != nil {
return nil, err
}
//获取文件的MD5,检查数据库是否已经存在,存在则不用重复上传
md5hash := md5.New()
bufFile = bufio.NewReader(file)
_, err = io.Copy(md5hash, bufFile)
if err != nil {
return nil, err
}
md5Str := hex.EncodeToString(md5hash.Sum(nil))
_, err = file.Seek(0, 0)
if err != nil {
return nil, err
}
attachment, err := GetAttachmentByMd5(md5Str)
if err == nil {
if attachment.Status != 1 {
//更新status
attachment.Status = 1
err = attachment.Save(db)
if err != nil {
return nil, err
}
}
//直接返回
return attachment, nil
}
//如果图片宽度大于750,自动压缩到750, gif 不能处理
buff := &bytes.Buffer{}
if width > 750 && imgType != "gif" {
newImg := library.Resize(750, 0, img, resize.Lanczos3)
width = uint(newImg.Bounds().Dx())
height = uint(newImg.Bounds().Dy())
if imgType == "jpg" {
// 保存裁剪的图片
_ = jpeg.Encode(buff, newImg, nil)
} else if imgType == "png" {
// 保存裁剪的图片
_ = png.Encode(buff, newImg)
}
} else {
_, _ = io.Copy(buff, file)
}
tmpName := md5Str[8:24] + "." + imgType
filePath := strconv.Itoa(time.Now().Year()) + strconv.Itoa(int(time.Now().Month())) + "/" + strconv.Itoa(time.Now().Day()) + "/"
//将文件写入本地
basePath := config.ExecPath + "public/uploads/"
//先判断文件夹是否存在,不存在就先创建
_, err = os.Stat(basePath + filePath)
if err != nil && os.IsNotExist(err) {
err = os.MkdirAll(basePath+filePath, os.ModePerm)
if err != nil {
return nil, err
}
}
originFile, err := os.OpenFile(basePath + filePath + tmpName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
//无法创建
return nil, err
}
defer originFile.Close()
_, err = io.Copy(originFile, buff)
if err != nil {
//文件写入失败
return nil, err
}
//生成宽度为250的缩略图
thumbName := "thumb_" + tmpName
newImg := library.ThumbnailCrop(250, 250, img)
if imgType == "jpg" {
_ = jpeg.Encode(buff, newImg, nil)
} else if imgType == "png" {
_ = png.Encode(buff, newImg)
} else if imgType == "gif" {
_ = gif.Encode(buff, newImg, nil)
}
thumbFile, err := os.OpenFile(basePath + filePath + thumbName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
//无法创建
return nil, err
}
defer thumbFile.Close()
_, err = io.Copy(thumbFile, buff)
if err != nil {
//文件写入失败
return nil, err
}
//文件上传完成
attachment = &model.Attachment{
Id: 0,
FileName: fileName,
FileLocation: filePath + tmpName,
FileSize: int64(info.Size),
FileMd5: md5Str,
Width: width,
Height: height,
Status: 1,
}
attachment.GetThumb()
err = attachment.Save(db)
if err != nil {
return nil, err
}
return attachment, nil
}
func GetAttachmentByMd5(md5 string) (*model.Attachment, error) {
db := config.DB
var attach model.Attachment
if err := db.Where("`status` != 99").Where("`file_md5` = ?", md5).First(&attach).Error; err != nil {
return nil, err
}
attach.GetThumb()
return &attach, nil
}
因为是图片处理,所以这里的代码量有点多。因为我们要考虑到上传的图片有jpg、png、gif等,因此需要分别引入这些包,来解析图片。
上面通过解析图片,获取到图片的宽高和图片文件大小,也计算了图片的md5值。为了防止用户多次重复上传同一张图片,我们根据图片的md5值来判断图片是否重复,如果上传的是同一个图片,则不再进行后续处理,直接返回已经存储在服务器上的图片路径回去即可。
为了减轻服务器压力,我们在上传图片的时候,对图片尺寸大小做了判断,如果宽度大于750像素,则自动压缩到宽度为750像素的图片。再根据图片上传的日期,自动按年月和一个随机名称,存储到服务器的目录中。如果目录不存在,则先创建。
同时在处理图片的时候,也给每个图片都生成了一个宽度为250像素的缩略图,这个缩略图将会按居中裁剪方式生成。
上面我们注意到了,图片的缩放处理、裁剪处理,我们都使用了一个library/image的图片处理函数,因为图片处理相对比较复杂,我们将图片的缩放、裁剪处理,单独抽出到了library中。我们现在library创建一个image.go文件,来存放图片处理函数:
package library
import (
"fmt"
"github.com/nfnt/resize"
"github.com/oliamb/cutter"
"image"
)
func ThumbnailCrop(minWidth, minHeight uint, img image.Image) image.Image {
origBounds := img.Bounds()
origWidth := uint(origBounds.Dx())
origHeight := uint(origBounds.Dy())
newWidth, newHeight := origWidth, origHeight
// Return original image if it have same or smaller size as constraints
if minWidth >= origWidth && minHeight >= origHeight {
return img
}
if minWidth > origWidth {
minWidth = origWidth
}
if minHeight > origHeight {
minHeight = origHeight
}
// Preserve aspect ratio
if origWidth > minWidth {
newHeight = uint(origHeight * minWidth / origWidth)
if newHeight < 1 {
newHeight = 1
}
//newWidth = minWidth
}
if newHeight < minHeight {
newWidth = uint(newWidth * minHeight / newHeight)
if newWidth < 1 {
newWidth = 1
}
//newHeight = minHeight
}
if origWidth > origHeight {
newWidth = minWidth
newHeight = 0
}else {
newWidth = 0
newHeight = minHeight
}
thumbImg := resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
//return CropImg(thumbImg, int(minWidth), int(minHeight))
return thumbImg
}
func Resize(width, height uint, img image.Image, interp resize.InterpolationFunction) image.Image {
return resize.Resize(width, height, img, interp)
}
func Thumbnail(width, height uint, img image.Image, interp resize.InterpolationFunction) image.Image {
return resize.Thumbnail(width, height, img, interp)
}
func CropImg(srcImg image.Image, dstWidth, dstHeight int) image.Image {
//origBounds := srcImg.Bounds()
//origWidth := origBounds.Dx()
//origHeight := origBounds.Dy()
dstImg, err := cutter.Crop(srcImg, cutter.Config{
Height: dstHeight, // height in pixel or Y ratio(see Ratio Option below)
Width: dstWidth, // width in pixel or X ratio
Mode: cutter.Centered, // Accepted Mode: TopLeft, Centered
//Anchor: image.Point{
// origWidth / 12,
// origHeight / 8}, // Position of the top left point
Options: 0, // Accepted Option: Ratio
})
fmt.Println()
if err != nil {
fmt.Println("Cannot crop image:" + err.Error())
return srcImg
}
return dstImg
}
这个图片处理文件,是我从别人那里抄来的。它支持图片缩放、裁剪的多种形式。图片缩放我们使用了github.com/nfnt/resize
包,图片裁剪我们使用了github.com/oliamb/cutter
包。这里不做详细介绍,知道它能裁剪图片和缩放图片就行。
图片上传路由处理
上面这些逻辑处理完,我们还需要添加路由,才能真正提供给前端访问,修改route/base.go文件,添加如下代码:
attachment := app.Party("/attachment", controller.Inspect)
{
attachment.Post("/upload", controller.AttachmentUpload)
}
这里同样的,我们也定义了它为一个路由分组,是为了以后方便扩展。
测试结果验证
上面操作完成了,我们重启一下项目,打不一篇文章,在文章中添加图片,看看能不能正常上传图片。如果不出意外,我们就可以看到图片出现在编辑框中了。
完整的项目示例代码托管在GitHub上,需要查看完整的项目代码可以到github.com/fesiong/goblog 上查看,也可以直接fork一份来在上面做修改。