前言
前一个简单版本简单的markdown在线解析服务
说明
这个版本之所以加一个pro的后缀,是因为增加了一下功能。
-
增加了/update接口锁,保证同一时间只会有一个groutine在更新文件列表
-
增加了markdown解析后数据的缓存,避免重复解析
-
增加了单个markdown文件的读取锁,避免同一个文件正在解析,重复提交解析任务
文件结构
--manager
manager.go
--markdowns
--templates
index.html
main.go
dependences
"github.com/LK4D4/trylock"
"github.com/microcosm-cc/bluemonday"
"gopkg.in/russross/blackfriday.v2"
实现
manager.go
markdows路径在New()时传入,一般情况是在main运行的根目录建立一个专门用来存放markdown文件的文件夹。
package manager
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"github.com/LK4D4/trylock"
"github.com/microcosm-cc/bluemonday"
blackfriday "gopkg.in/russross/blackfriday.v2"
)
// MarkdownFile :markdown file and html byte code
type MarkdownFile struct {
path string
data []byte
updateLock *trylock.Mutex
}
// MarkdownsManeger to update and get markdown file
type MarkdownsManeger struct {
rwLock *sync.RWMutex
updateLock *trylock.Mutex
data map[string]*MarkdownFile
markdownPath string
}
func (m *MarkdownFile) isExist() bool {
return m.data != nil
}
func (m *MarkdownFile) readMarkdown() ([]byte, error) {
if m.updateLock.TryLock() {
defer m.updateLock.Unlock()
if fileread, err := ioutil.ReadFile(m.path); err == nil {
unsafe := blackfriday.Run(fileread)
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
m.data = html
return html, nil
} else {
return nil, fmt.Errorf("file(%s)ReadFail", m.path)
}
} else {
m.updateLock.Lock()
m.updateLock.Unlock()
if m.isExist() {
return m.data, nil
} else {
return nil, fmt.Errorf("file(%s)ReadFail", m.path)
}
}
}
func (s *MarkdownsManeger) Reflesh() bool {
if s.updateLock.TryLock() {
s.rwLock.Lock()
defer s.updateLock.Unlock()
defer s.rwLock.Unlock()
s.data = make(map[string]*MarkdownFile)
files, _ := filepath.Glob(fmt.Sprintf("./%s/*", s.markdownPath))
for _, f := range files {
fileName := f[strings.LastIndex(f, string(os.PathSeparator))+1 : len(f)-3]
s.data[fileName] = &MarkdownFile{f, nil, new(trylock.Mutex)}
}
return true
}
return false
}
func (s *MarkdownsManeger) GetFileList() []string {
keys := make([]string, 0, len(s.data))
for k := range s.data {
keys = append(keys, k)
}
return keys
}
func (s *MarkdownsManeger) GetFile(fileName string) ([]byte, error) {
s.rwLock.RLock()
defer s.rwLock.RUnlock()
markdownFile, ok := s.data[fileName]
if !ok {
return nil, fmt.Errorf("file(%s)NotExist", fileName)
}
if markdownFile.isExist() {
return markdownFile.data, nil
}
return markdownFile.readMarkdown()
}
//New MarkdownsManeger
func New(markdownPath string) MarkdownsManeger {
return MarkdownsManeger{
new(sync.RWMutex),
new(trylock.Mutex),
map[string]*MarkdownFile{},
markdownPath,
}
}
main.go
package main
import (
"fmt"
"html/template"
"net/http"
"os"
"./manager"
)
type MarkdownsHandler struct {
markdownsManeger *manager.MarkdownsManeger
templatesPath string
}
func (h *MarkdownsHandler) Index(w http.ResponseWriter, req *http.Request) {
t := template.New("index.html")
t, _ = t.ParseFiles(fmt.Sprintf("%s%sindex.html", h.templatesPath, string(os.PathSeparator)))
t.Execute(w, h.markdownsManeger.GetFileList())
}
func (h *MarkdownsHandler) ReaderHander(w http.ResponseWriter, r *http.Request) {
fileName := r.URL.Query().Get("file")
body, err := h.markdownsManeger.GetFile(fileName)
if err != nil {
w.WriteHeader(http.StatusNotFound)
} else {
w.Write(body)
}
}
func (h *MarkdownsHandler) UpdateHandler(w http.ResponseWriter, req *http.Request) {
if h.markdownsManeger.Reflesh() {
w.Write([]byte("success"))
} else {
w.Write([]byte("under refleshing"))
}
}
var (
hander *MarkdownsHandler
)
func init() {
m := manager.New("markdowns")
hander = &MarkdownsHandler{
&m,
"templates",
}
hander.markdownsManeger.Reflesh()
}
func main() {
http.HandleFunc("/", hander.Index)
http.HandleFunc("/read", hander.ReaderHander)
http.HandleFunc("/update", hander.UpdateHandler)
http.ListenAndServe(":8000", nil)
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>markdownlist</h1>
{{range $key := .}}
<a href="/read?file={{$key}}">{{$key}}</a><br/>
{{end}}
</body>
</html>
小结
-
这次项目主要用到的就是tryLock(自然是直接用现成的第三方包),更新文件列表时回去尝试获得锁,失败时说明已经有groutine在更新列表,那自己就不必更新了;
-
markdownFile的读取锁比上面稍微复杂了一点点。因为在尝试失败后不能直接跳出(还是需要返回数据的),所以就在尝试获取所失败了一次后,通过Lock方法来等待实际读取文件的groutine退出;
-
做缓存时,文件列表中就不能直接保存markdownFile的实例了。如果这样的话,从列表中获取的就是实力的copy,自然缓存也无从谈起,所以只能存引用。
ps: 前端样式美化我就不搞了
ps: 这个版本比上一个高级了一些,其实对于这个项目的实际运用场景,并没有创造额外价值,毕竟实际访问量就那么几个。
ennn
以下是代码地址,增加了一些易用性相关的东西,可执行文件可以直接运行,template都打包到文件里了release下载地址