最近阴差阳错的三个项目都涉及到了前后端文件传输,特别是对图片文件的处理这一技术点,结合最近上传博客经常卡壳,速度也很慢这一现状,干脆用 go 给自己的博客搭一个图床吧。

一些约定 链接到标题

系统: Ubuntu 20.04

图片存储格式: webp

图片文件名称生成格式: UUID

图片支持上传的格式: jpeg、png

基础框架 链接到标题

我选择 gin 作为前后端对接的框架,redis 简单储存一些需要用到的数据,搭好一个简单的登录与鉴权框架即可。

业务实现 链接到标题

图片接收与存储 链接到标题

前端会将图片以 form-data 的形式进行传输,后端只需调用 gin 框架对文件的接受与保存方法即可实现。

func UploadImg(c *gin.Context) {  
   form, _ := c.MultipartForm()  
   img := form.File["img"][0]  
   imgName := img.Filename  
   _ = c.SaveUploadedFile(img, "./tmp/"+imgName)
}

将 jpeg 格式的图片转换为 webp 格式存储 链接到标题

这一步因为 go 的标准库内只有 webp 格式的 decode 方法,没有我想要的 encode 方法,只能选择第三方库 go-webp 来实现这一功能。

func UploadImg(c *gin.Context) {  
   // 上文的图片存储代码,略
   file, _ := os.Open("./tmp/" + imgName)  
   defer file.Close()
   imgNew, _ := jpeg.Decode(file)  
   fileName := strings.TrimSuffix(img.Filename, path.Ext(path.Base(img.Filename))) + ".webp"  
   output, _ := os.Create("./img/" + fileName)  
   defer output.Close()  
   options, err := encoder.NewLossyEncoderOptions(encoder.PresetDefault, 75)  
   if err != nil {  
      _ = c.AbortWithError(200, apiException.ImgTypeError)  
      return  
   }  
   err = webp.Encode(output, imgNew, options)  
   if err != nil {  
      _ = c.AbortWithError(200, apiException.ImgTypeError)  
      return  
   } 
   utils.JsonSuccessResponse(c, config.GetWebpUrlKey()+fileName) 
}

处理 png 格式的问题 链接到标题

因为我没找到 png 直接转换为 webp 的库,因此选择先理由标准库把 png 转换为 jpeg,再利用我刚刚实现的 jpegwebp 的代码处理。

func UploadImg(c *gin.Context) {  
   // 上文的图片存储与打开文件代码,略
   buffer := make([]byte, 512)
	_, _ = file.Read(buffer)
	contentType := http.DetectContentType(buffer)
	file.Close()
	file, _ = os.Open("./tmp/" + imgName)
	defer file.Close()
	if contentType == "image/png" {
		newTypeName := "./tmp/" + strings.TrimSuffix(img.Filename, path.Ext(path.Base(img.Filename))) + ".png"
		_ = os.Rename("./tmp/"+imgName, newTypeName)
		imgNew, err := png.Decode(file)
		if err != nil {
			fmt.Println(err)
			_ = c.AbortWithError(200, apiException.ImgTypeError)
			return
		}
		out, err := os.Create("./tmp/" + strings.TrimSuffix(img.Filename, path.Ext(path.Base(img.Filename))) + ".jpg")
		if err != nil {
			fmt.Println(err)
			_ = c.AbortWithError(200, apiException.ImgTypeError)
			return
		}
		defer out.Close()
		err = jpeg.Encode(out, imgNew, &jpeg.Options{Quality: 95})
		if err != nil {
			fmt.Println(err)
			_ = c.AbortWithError(200, apiException.ImgTypeError)
			return
		}
		_ = os.Remove(newTypeName)
		imgName = strings.TrimSuffix(img.Filename, path.Ext(path.Base(img.Filename))) + ".jpg"
		file.Close()
		file, _ = os.Open("./tmp/" + imgName)
	}
   // 上文 jpeg 转 webp 格式并存储的代码,略
}

其他处理 链接到标题

转换后的图片如果仍使用原文件名可能会遇到冲突的问题,因此我选择生成一个 UUID 来命名新文件,确保独一,且在转换存储成功后加了一条语句删除了之前存储的临时文件。

最终代码如下:

func UploadImg(c *gin.Context) {  
  form, _ := c.MultipartForm()
	img := form.File["img"][0]
	imgName := img.Filename
	_ = c.SaveUploadedFile(img, "./tmp/"+imgName)
	file, _ := os.Open("./tmp/" + imgName)
	buffer := make([]byte, 512)
	_, _ = file.Read(buffer)
	contentType := http.DetectContentType(buffer)
	file.Close()
	file, _ = os.Open("./tmp/" + imgName)
	defer file.Close()
	if contentType == "image/png" {
		newTypeName := "./tmp/" + strings.TrimSuffix(img.Filename, path.Ext(path.Base(img.Filename))) + ".png"
		_ = os.Rename("./tmp/"+imgName, newTypeName)
		imgNew, err := png.Decode(file)
		if err != nil {
			fmt.Println(err)
			_ = c.AbortWithError(200, apiException.ImgTypeError)
			return
		}
		out, err := os.Create("./tmp/" + strings.TrimSuffix(img.Filename, path.Ext(path.Base(img.Filename))) + ".jpg")
		if err != nil {
			fmt.Println(err)
			_ = c.AbortWithError(200, apiException.ImgTypeError)
			return
		}
		defer out.Close()
		err = jpeg.Encode(out, imgNew, &jpeg.Options{Quality: 95})
		if err != nil {
			fmt.Println(err)
			_ = c.AbortWithError(200, apiException.ImgTypeError)
			return
		}
		_ = os.Remove(newTypeName)
		imgName = strings.TrimSuffix(img.Filename, path.Ext(path.Base(img.Filename))) + ".jpg"
		file.Close()
		file, _ = os.Open("./tmp/" + imgName)
	}
	imgNew, err := jpeg.Decode(file)
	if err != nil {
		fmt.Println(err)
		_ = c.AbortWithError(200, apiException.ImgTypeError)
		return
	}
	fileName := uuid.NewString() + ".webp"
	output, _ := os.Create("./img/" + fileName)
	defer output.Close()
	options, err := encoder.NewLossyEncoderOptions(encoder.PresetDefault, 75)
	if err != nil {
		_ = c.AbortWithError(200, apiException.ImgTypeError)
		return
	}
	err = webp.Encode(output, imgNew, options)
	if err != nil {
		_ = c.AbortWithError(200, apiException.ImgTypeError)
		return
	}
	_ = os.Remove("./tmp/" + imgName)
	utils.JsonSuccessResponse(c, config.GetWebpUrlKey()+fileName)
}