使用go搭建个人博客(三):服务端解析md文件

2,799 阅读5分钟

  上一篇文章介绍了数据库的连接和路由的配置,这一篇就开始书写比较重要的功能,接收并解析md文件,然后将重要的信息存入到数据库,最后再通过一个接口将md装换成html字符串暴露出去。

处理文件上传接口

  接收到一个文件后,就开始读取这个文件,将文件里面重要的信息存入数据库,为了以后能通过关键信息能遍历到指定的博客信息,然后返回给前端进行展示。

  首先我们在controllers目录下创建处理上传文件的控制器:

controllers/uploadFile.go :

先引入使用xorm创建的各个模板,之后使用这些将数据存入数据库

package controllers

import (
  "crypto/md5"
  "errors"
  "fmt"
  ...

  "github.com/gin-gonic/gin"
  "github.com/zachrey/blog/models"
)

const postDir = "./posts/" // 将文件方法放到一个指定的目录,如果文件过多可以放在现在各种云上面。

// UpLoadFile 上传文件的控制器
func UpLoadFile(c *gin.Context) {
  // 指定数据的字段名,从该字段获取文件流
  file, header, err := c.Request.FormFile("upload")
  // 获取上传的文件名,存储的时候使用
  filename := header.Filename
  // 将文件名md5转一下,便于存储
  md5FileName := fmt.Sprintf("%x", md5.Sum([]byte(filename)))
  // 获取文件名的扩展名 .md
  fileExt := filepath.Ext(postDir + filename)
  // 文件的存储路径
  filePath := postDir + md5FileName + fileExt
  // 打印下日志
  log.Println("[INFO] upload file: ", header.Filename)
  // 判断放文件的目录下是否已经有存在相同文件
  has := hasSameNameFile(md5FileName+fileExt, postDir)
  if has {
    c.JSON(http.StatusOK, gin.H{
      "status": 1,
      "msg":    "服务器已有相同文件名称",
    })
    return
  }

  // 根据文件名的md5值,创建服务器上的文件
  out, err := os.Create(filePath)
  if err != nil {
    log.Fatal(err)
  }
  defer out.Close()
  
  // 将信息存入数据库操作。。。。
  // 将信息存入数据库操作。。。。
  // 将信息存入数据库操作。。。。
  // 将信息存入数据库操作。。。。

  c.JSON(http.StatusOK, gin.H{
    "status": 0,
    "msg":    "上传成功",
  })
}

func hasSameNameFile(fileName, dir string) bool {
  files, _ := ioutil.ReadDir(dir)
  // 遍历目录,是否存在相同的文件,这里有两种方式来查重,可以去数据库查询是否有相同的文件名也是可以的。
  for _, f := range files {
    if fileName == f.Name() {
      return true
    }
  }
  return false
}

将文件解析的信息写入数据库

  一篇博客的主要信息包括:标题、分类、标签和文字字数等,这里就规定在md文件中怎么书写这些信息,以方便服务器来解析,然后存入数据库。规定md文件头几行应该书写信息,如下规定:

title: 标题1
categories: 分类1, 分类2, 分类三
label: JS, ES6
---------------------------------------------

信息内容与主体内容以---------------------------------------------进行分割。然后读取文件,往数据库写数据,还是在同一个文件中书写存入数据库的逻辑,我们来补全内容,上面留空的“存入数据库操作”就是下面的内容了。

controllers/uploadFile.go :

package controllers

import (
  "crypto/md5"
  "errors"
  "fmt"
  ...

  "github.com/gin-gonic/gin"
  "github.com/zachrey/blog/models"
)
...
var gr sync.WaitGroup
var isShouldRemove = false

// UpLoadFile 上传文件的控制器
func UpLoadFile(c *gin.Context) {
  ...
  // 根据文件名的md5值,创建服务器上的文件
  out, err := os.Create(filePath)
  if err != nil {
    log.Fatal(err)
  }
  // 处理完整个上传过程后,是否需要删除创建的文件,在存在错误的情况下, 解析出错就删掉刚创建的文件。
  //这个操作一定需要放在 out.close上面,因为创建的文件流,如果不关闭的话,
  //无法进行删除操作,又因为defer栈的执行顺序,所以必须放在上面声明。
  defer func() {
    if isShouldRemove {
      err = os.Remove(filePath)
      if err != nil {
        log.Println("[ERROR] ", err)
      }
    }
  }()
  // 关闭文件流,存储文件。
  defer out.Close()
  // 将上传文件的内容copy到新建的文件中,然后进行存储。
  _, err = io.Copy(out, file)
  if err != nil {
    log.Fatal(err)
  }
  // 如果读取解析文件存在错误的话,则isShouldRemove复制为true,最后由defer进行删除
  err = readMdFileInfo(filePath)
  if err != nil {
    isShouldRemove = true
    c.JSON(http.StatusOK, gin.H{
      "status": 1,
      "msg":    err.Error(),
    })
    return
  }
  ... // 返回json数据,请看上一段的内容
}

// 读取文件信息,并写入数据库
func readMdFileInfo(filePath string) error {
  // 读取文件
  fileread, _ := ioutil.ReadFile(filePath)
  // 将内容切割成每一行
  lines := strings.Split(string(fileread), "\n")
  // 文字内容以第5行往后开始数
  body := strings.Join(lines[5:], "")
  // 计算文字内容的文字个数
  textAmount := GetStrLength(body)
  log.Println(lines)
  // 信息的标识
  const (
    TITLE      = "title: "
    CATEGORIES = "categories: "
    LABEL      = "label: "
  )
  var (
    postId     int64
    postCh     chan int64
    categoryCh chan []int64
    labelCh    chan []int64
  )
  mdInfo := make(map[string]string)

  /**
  * 并发插入三组相互不特别依赖的信息
  */
  for i, lens := 0, len(lines); i < lens && i < 5; i++ { // 只查找前五行
    switch {
    case strings.HasPrefix(lines[i], TITLE):
      mdInfo[TITLE] = strings.TrimLeft(lines[i], TITLE)
      postCh = make(chan int64)
      // 存入数据库,存入成功返回插入的记录id,放入通道postCh中
      go models.InsertPost(mdInfo[TITLE], filepath.Base(filePath), int64(textAmount), postCh)
    case strings.HasPrefix(lines[i], CATEGORIES):
      mdInfo[CATEGORIES] = strings.TrimLeft(lines[i], CATEGORIES)
      categoryCh = make(chan []int64)
      // 存入数据库
      go models.InsertCategory(mdInfo[CATEGORIES], categoryCh)
    case strings.HasPrefix(lines[i], LABEL):
      mdInfo[LABEL] = strings.TrimLeft(lines[i], LABEL)
      labelCh = make(chan []int64)
      // 存入数据库
      go models.InsertLabel(mdInfo[LABEL], labelCh)
    }
  }

  /**
  * 插入相互相关的信息
  */
  postId = <-postCh //等待文章成功插入
  if postId == 0 {
    return errors.New("服务器上已有相同文章标题")
  }
  log.Println("[INFO] postId: ", postId)
  // 插入文章与分类的关联信息,文字与分类关联表
  if categoryCh != nil {
    go func() {
      categoryIds := <-categoryCh
      log.Println("[INFO] categoryIds: ", categoryIds)

      for _, v := range categoryIds {
        models.InsertPostAndCategory(postId, v)
      }
    }()
  }
  // 插入文章与标签的关联信息,文字与标签关联表
  if labelCh != nil {
    go func() {
      labels := <-labelCh
      log.Println("[INFO] labels: ", labels)

      for _, v := range labels {
        models.InsertPostAndLabel(postId, v)
      }
    }()
  }
  return nil // 全部成功插入,则返回没有错误,如果存在错误,则往上返回
}

...
// GetStrLength 返回输入的字符串的字数,汉字和中文标点算 1 个字数,英文和其他字符 2 个算 1 个字数,不足 1 个算 1个
func GetStrLength(str string) float64 {
  var total float64

  reg := regexp.MustCompile("/·|,|。|《|》|‘|’|”|“|;|:|【|】|?|(|)|、/")

  for _, r := range str {
    if unicode.Is(unicode.Scripts["Han"], r) || reg.Match([]byte(string(r))) {
      total = total + 1
    } else {
      total = total + 0.5
    }
  }
  return math.Ceil(total)
}

这里使用了goroutine通道的方法,尝试的多熟悉一下Go的特性,使用并发的去插入三组不相关的记录到数据库,然后等三组分别插入完后,串行插入相互相关的记录。

路由配置

routers/router.go:

package routers

import (
  "log"
  "net/http"

  "github.com/gin-gonic/gin"
  ctrs "github.com/zachrey/blog/controllers"
  "github.com/zachrey/blog/models"
)

...

func loadRouters(router *gin.Engine) {
  ...

  router.POST("/upload", ctrs.UpLoadFile)
  
  ...
}

此时我们使用postman来进行测试,启动服务,选择文件上传。

上传测试

最后

  怎么处理上传的md文件,是整个博客系统的重点,存好了文章数据,就可以做出各种基于这些数据的API了。下一篇文件,就来讨论下怎么实现cmd工具,上传文件。如果里面有些代码看不完全的,可以去我的github上,查看完整的源码,直接运行下。欢迎留言或者使用邮箱(zz__0123@163.com)联系我讨论,初学实践分享,有很多不足,请多多指教。