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

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

处理文件上传接口

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

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

controllers/uploadFile.go :

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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文件头几行应该书写信息,如下规定:

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

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

controllers/uploadFile.go :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)联系我讨论,初学实践分享,有很多不足,请多多指教。