上一篇文章介绍了数据库的连接和路由的配置,这一篇就开始书写比较重要的功能,接收并解析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)联系我讨论,初学实践分享,有很多不足,请多多指教。