| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- package main
- import (
- "flag"
- "fmt"
- "github.com/fsnotify/fsnotify"
- "github.com/labstack/echo"
- "github.com/labstack/echo/middleware"
- "github.com/sergi/go-diff/diffmatchpatch"
- "github.com/zeebo/xxh3"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "time"
- )
- type fileInfo struct {
- Hash uint64 `json:"hash"`
- Path string `json:"-"`
- Name string `json:"name"`
- Size int64 `json:"size"`
- ModTime time.Time `json:"mod_time"`
- }
- func newFileInfo(name string) *fileInfo {
- finfo := new(fileInfo)
- finfo.Hash = xxh3.HashString(name)
- finfo.Path = name
- finfo.Name, _ = filepath.Rel(docDir, name)
- return finfo
- }
- func (f *fileInfo) update(info *os.FileInfo) {
- f.Size = (*info).Size()
- f.ModTime = (*info).ModTime()
- }
- type fileCache struct {
- Path string `json:"-"`
- Hash uint64 `json:"-"`
- Editing map[*wsCli]bool `json:"-"`
- File string `json:"-"`
- }
- func newFileCache(info *fileInfo) *fileCache {
- c := new(fileCache)
- c.Editing = make(map[*wsCli]bool)
- if info != nil {
- data, _ := ioutil.ReadFile(info.Path)
- c.File = string(data)
- c.Path = info.Path
- c.Hash = info.Hash
- }
- return c
- }
- func (c *fileCache) Name() string {
- name, _ := filepath.Rel(docDir, c.Path)
- return name
- }
- func (c *fileCache) AddEditor(cli *wsCli) {
- c.Editing[cli] = true
- c.SendWriters(cli, map[string]interface{}{"editors": len(c.Editing)})
- }
- func (c *fileCache) SendWriters(cli *wsCli, data interface{}) {
- msg := wsMessage{
- cli: cli,
- hash: c.Hash,
- CMD: wsWritersCMD,
- Seq: 0,
- Data: data,
- }
- hub.broadcast<-&msg
- }
- func (c *fileCache) GetUpdate() map[string]interface{} {
- return map[string]interface{}{
- "editors": len(c.Editing),
- }
- }
- func (c *fileCache) RemoveEditor(cli *wsCli) {
- delete(c.Editing, cli)
- if len(c.Editing) == 0 {
- c.Flush()
- delete(fileCaches, c.Hash)
- } else {
- c.SendWriters(nil, map[string]interface{}{"editors": len(c.Editing)})
- }
- }
- func (c *fileCache) Flush() {
- var err error
- if len(strings.Trim(c.File, " \n\r\t")) == 0 {
- return
- }
- fmt.Printf("Flushing file: %s\n", c.Path)
- var f *os.File
- err = os.MkdirAll(filepath.Dir(c.Path), 0775)
- if err != nil {
- e.Logger.Errorf("Failed to create directory" + err.Error())
- return
- }
- _ = watcher.Add(filepath.Dir(c.Path))
- f, err = os.OpenFile(c.Path, os.O_CREATE|os.O_WRONLY, os.ModePerm)
- if err != nil {
- e.Logger.Errorf("Failed to create/open file" + err.Error())
- return
- }
- _, err = f.Write([]byte(c.File))
- if err != nil {
- _ = f.Close()
- e.Logger.Errorf("Failed to create/open file" + err.Error())
- return
- }
- //if _, ok = fileTree[c.Hash]; !ok {
- // finfo := newFileInfo(c.Path)
- // fileTree[c.Hash] = finfo
- // if info, err := f.Stat(); err != nil {
- // finfo.update(&info)
- // }
- //}
- _ = f.Close()
- }
- var (
- e *echo.Echo
- watcher *fsnotify.Watcher
- fileTree map[uint64]*fileInfo
- docDir string
- fileCaches map[uint64]*fileCache
- dmp = diffmatchpatch.New()
- )
- func init() {
- fileTree = make(map[uint64]*fileInfo)
- fileCaches = make(map[uint64]*fileCache)
- }
- func updateFileTree() {
- _ = filepath.Walk(docDir, func(path string, info os.FileInfo, err error) error {
- if err != nil { return err }
- if info.IsDir() {
- _ = watcher.Add(path)
- return nil
- }
- if filepath.Ext(path) != ".md" { return nil }
- var finfo *fileInfo
- var ok bool
- hash := xxh3.HashString(path)
- if finfo, ok = fileTree[hash]; !ok {
- finfo = newFileInfo(path)
- fileTree[hash] = finfo
- }
- finfo.update(&info)
- return nil
- })
- }
- func fsWatcher() {
- for {
- select {
- case event, ok := <-watcher.Events:
- if !ok {
- return
- }
- if filepath.Ext(event.Name) != ".md" { continue }
- hash := xxh3.HashString(event.Name)
- var finfo *fileInfo
- var name string
- if event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename {
- if finfo, ok = fileTree[hash]; ok {
- name = finfo.Name
- delete(fileTree, hash)
- }
- goto annouceUpdate
- }
- // on create or write
- if finfo, ok = fileTree[hash]; !ok {
- finfo = newFileInfo(event.Name)
- fileTree[hash] = finfo
- }
- name = finfo.Name
- if event.Op&fsnotify.Write == fsnotify.Write {
- info, err := os.Stat(event.Name)
- if err != nil { continue }
- finfo.update(&info)
- }
- annouceUpdate:
- if len(hub.clients) > 0 {
- msg := wsMessage{
- CMD: wsFileUpdateCMD,
- Data: name,
- }
- hub.broadcast<-&msg
- }
- case err, ok := <-watcher.Errors:
- if !ok {
- return
- }
- log.Println("error:", err)
- }
- }
- }
- func fixName(name string) string {
- name = strings.ToLower(name)
- if strings.HasSuffix(name, ".md") {
- name = name[:len(name)-3]
- }
- name = strings.ReplaceAll(name, " ", "_")
- name = strings.ReplaceAll(name, ".", "_")
- return name + ".md"
- }
- func calcHash(name string) (uint64, string) {
- path := filepath.Join(docDir, fixName(name))
- hash := xxh3.HashString(path)
- return hash, path
- }
- func main() {
- adderss := flag.String("b", "127.0.0.1:8288", "Bind address")
- flag.StringVar(&docDir, "d", "documents", "Document directory")
- distDir := flag.String("w", "dist", "Web dist directory")
- flag.Parse()
- var err error
- e = echo.New()
- //e.Use(middleware.Logger())
- e.Use(middleware.Recover())
- defer func() {
- for _, cached := range fileCaches {
- cached.Flush()
- }
- }()
- watcher, err = fsnotify.NewWatcher()
- if err != nil {
- e.Logger.Fatal(err)
- }
- defer watcher.Close()
- go fsWatcher()
- err = watcher.Add(docDir)
- if err != nil {
- e.Logger.Fatal(err)
- }
- updateFileTree()
- e.Use(middleware.Recover())
- e.GET("/_doc", func(c echo.Context) error {
- return c.JSON(http.StatusOK, fileTree)
- })
- e.GET("/_ws", handleWS)
- docs := e.Group("/_doc/")
- docs.Use(middleware.GzipWithConfig(middleware.GzipConfig{Level: 5}))
- docs.GET("*", func(c echo.Context) error {
- hash, fname := calcHash(c.Param("*"))
- if cached, ok := fileCaches[hash]; ok {
- fmt.Println("Serving cached: " + fname)
- return c.Blob(http.StatusOK, "text/markdown", []byte(cached.File))
- }
- if _, ok := fileTree[hash]; !ok {
- return c.Blob(http.StatusOK, "text/markdown", nil)
- }
- fmt.Println("Serving from file: " + fname)
- return c.File(fname)
- })
- docs.DELETE("*", func(c echo.Context) error {
- hash, fname := calcHash(c.Param("*"))
- if _, ok := fileTree[hash]; ok {
- err = os.Remove(fname)
- if err != nil {
- e.Logger.Errorf("Failed to remove file %s: %v", fname, err)
- }
- }
- return c.JSON(http.StatusOK, true)
- })
- e.Static("/js", filepath.Join(*distDir, "js"))
- e.Static("/css", filepath.Join(*distDir, "css"))
- e.Static("/fonts", filepath.Join(*distDir, "fonts"))
- e.Static("/img", filepath.Join(*distDir, "img"))
- e.File("/favicon.png", filepath.Join(*distDir, "favicon.png"))
- e.File("/favicon.ico", filepath.Join(*distDir, "favicon.png"))
- e.GET("/*", func(c echo.Context) error {
- return c.File(filepath.Join(*distDir, "index.html"))
- })
- go hub.run()
- e.Logger.Fatal(e.Start(*adderss))
- }
|