main.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "github.com/fsnotify/fsnotify"
  6. "github.com/labstack/echo"
  7. "github.com/labstack/echo/middleware"
  8. "github.com/sergi/go-diff/diffmatchpatch"
  9. "github.com/zeebo/xxh3"
  10. "io/ioutil"
  11. "log"
  12. "net/http"
  13. "os"
  14. "path/filepath"
  15. "strings"
  16. "time"
  17. )
  18. type fileInfo struct {
  19. Hash uint64 `json:"hash"`
  20. Path string `json:"-"`
  21. Name string `json:"name"`
  22. Size int64 `json:"size"`
  23. ModTime time.Time `json:"mod_time"`
  24. }
  25. func newFileInfo(name string) *fileInfo {
  26. finfo := new(fileInfo)
  27. finfo.Hash = xxh3.HashString(name)
  28. finfo.Path = name
  29. finfo.Name, _ = filepath.Rel(docDir, name)
  30. return finfo
  31. }
  32. func (f *fileInfo) update(info *os.FileInfo) {
  33. f.Size = (*info).Size()
  34. f.ModTime = (*info).ModTime()
  35. }
  36. type fileCache struct {
  37. Path string `json:"-"`
  38. Hash uint64 `json:"-"`
  39. Editing map[*wsCli]bool `json:"-"`
  40. File string `json:"-"`
  41. }
  42. func newFileCache(info *fileInfo) *fileCache {
  43. c := new(fileCache)
  44. c.Editing = make(map[*wsCli]bool)
  45. if info != nil {
  46. data, _ := ioutil.ReadFile(info.Path)
  47. c.File = string(data)
  48. c.Path = info.Path
  49. c.Hash = info.Hash
  50. }
  51. return c
  52. }
  53. func (c *fileCache) Name() string {
  54. name, _ := filepath.Rel(docDir, c.Path)
  55. return name
  56. }
  57. func (c *fileCache) AddEditor(cli *wsCli) {
  58. c.Editing[cli] = true
  59. c.SendWriters(cli, map[string]interface{}{"editors": len(c.Editing)})
  60. }
  61. func (c *fileCache) SendWriters(cli *wsCli, data interface{}) {
  62. msg := wsMessage{
  63. cli: cli,
  64. hash: c.Hash,
  65. CMD: wsWritersCMD,
  66. Seq: 0,
  67. Data: data,
  68. }
  69. hub.broadcast<-&msg
  70. }
  71. func (c *fileCache) GetUpdate() map[string]interface{} {
  72. return map[string]interface{}{
  73. "editors": len(c.Editing),
  74. }
  75. }
  76. func (c *fileCache) RemoveEditor(cli *wsCli) {
  77. delete(c.Editing, cli)
  78. if len(c.Editing) == 0 {
  79. c.Flush()
  80. delete(fileCaches, c.Hash)
  81. } else {
  82. c.SendWriters(nil, map[string]interface{}{"editors": len(c.Editing)})
  83. }
  84. }
  85. func (c *fileCache) Flush() {
  86. var err error
  87. if len(strings.Trim(c.File, " \n\r\t")) == 0 {
  88. return
  89. }
  90. fmt.Printf("Flushing file: %s\n", c.Path)
  91. var f *os.File
  92. err = os.MkdirAll(filepath.Dir(c.Path), 0775)
  93. if err != nil {
  94. e.Logger.Errorf("Failed to create directory" + err.Error())
  95. return
  96. }
  97. _ = watcher.Add(filepath.Dir(c.Path))
  98. f, err = os.OpenFile(c.Path, os.O_CREATE|os.O_WRONLY, os.ModePerm)
  99. if err != nil {
  100. e.Logger.Errorf("Failed to create/open file" + err.Error())
  101. return
  102. }
  103. _, err = f.Write([]byte(c.File))
  104. if err != nil {
  105. _ = f.Close()
  106. e.Logger.Errorf("Failed to create/open file" + err.Error())
  107. return
  108. }
  109. //if _, ok = fileTree[c.Hash]; !ok {
  110. // finfo := newFileInfo(c.Path)
  111. // fileTree[c.Hash] = finfo
  112. // if info, err := f.Stat(); err != nil {
  113. // finfo.update(&info)
  114. // }
  115. //}
  116. _ = f.Close()
  117. }
  118. var (
  119. e *echo.Echo
  120. watcher *fsnotify.Watcher
  121. fileTree map[uint64]*fileInfo
  122. docDir string
  123. fileCaches map[uint64]*fileCache
  124. dmp = diffmatchpatch.New()
  125. )
  126. func init() {
  127. fileTree = make(map[uint64]*fileInfo)
  128. fileCaches = make(map[uint64]*fileCache)
  129. }
  130. func updateFileTree() {
  131. _ = filepath.Walk(docDir, func(path string, info os.FileInfo, err error) error {
  132. if err != nil { return err }
  133. if info.IsDir() {
  134. _ = watcher.Add(path)
  135. return nil
  136. }
  137. if filepath.Ext(path) != ".md" { return nil }
  138. var finfo *fileInfo
  139. var ok bool
  140. hash := xxh3.HashString(path)
  141. if finfo, ok = fileTree[hash]; !ok {
  142. finfo = newFileInfo(path)
  143. fileTree[hash] = finfo
  144. }
  145. finfo.update(&info)
  146. return nil
  147. })
  148. }
  149. func fsWatcher() {
  150. for {
  151. select {
  152. case event, ok := <-watcher.Events:
  153. if !ok {
  154. return
  155. }
  156. if filepath.Ext(event.Name) != ".md" { continue }
  157. hash := xxh3.HashString(event.Name)
  158. var finfo *fileInfo
  159. var name string
  160. if event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename {
  161. if finfo, ok = fileTree[hash]; ok {
  162. name = finfo.Name
  163. delete(fileTree, hash)
  164. }
  165. goto annouceUpdate
  166. }
  167. // on create or write
  168. if finfo, ok = fileTree[hash]; !ok {
  169. finfo = newFileInfo(event.Name)
  170. fileTree[hash] = finfo
  171. }
  172. name = finfo.Name
  173. if event.Op&fsnotify.Write == fsnotify.Write {
  174. info, err := os.Stat(event.Name)
  175. if err != nil { continue }
  176. finfo.update(&info)
  177. }
  178. annouceUpdate:
  179. if len(hub.clients) > 0 {
  180. msg := wsMessage{
  181. CMD: wsFileUpdateCMD,
  182. Data: name,
  183. }
  184. hub.broadcast<-&msg
  185. }
  186. case err, ok := <-watcher.Errors:
  187. if !ok {
  188. return
  189. }
  190. log.Println("error:", err)
  191. }
  192. }
  193. }
  194. func fixName(name string) string {
  195. name = strings.ToLower(name)
  196. if strings.HasSuffix(name, ".md") {
  197. name = name[:len(name)-3]
  198. }
  199. name = strings.ReplaceAll(name, " ", "_")
  200. name = strings.ReplaceAll(name, ".", "_")
  201. return name + ".md"
  202. }
  203. func calcHash(name string) (uint64, string) {
  204. path := filepath.Join(docDir, fixName(name))
  205. hash := xxh3.HashString(path)
  206. return hash, path
  207. }
  208. func main() {
  209. adderss := flag.String("b", "127.0.0.1:8288", "Bind address")
  210. flag.StringVar(&docDir, "d", "documents", "Document directory")
  211. distDir := flag.String("w", "dist", "Web dist directory")
  212. flag.Parse()
  213. var err error
  214. e = echo.New()
  215. //e.Use(middleware.Logger())
  216. e.Use(middleware.Recover())
  217. defer func() {
  218. for _, cached := range fileCaches {
  219. cached.Flush()
  220. }
  221. }()
  222. watcher, err = fsnotify.NewWatcher()
  223. if err != nil {
  224. e.Logger.Fatal(err)
  225. }
  226. defer watcher.Close()
  227. go fsWatcher()
  228. err = watcher.Add(docDir)
  229. if err != nil {
  230. e.Logger.Fatal(err)
  231. }
  232. updateFileTree()
  233. e.Use(middleware.Recover())
  234. e.GET("/_doc", func(c echo.Context) error {
  235. return c.JSON(http.StatusOK, fileTree)
  236. })
  237. e.GET("/_ws", handleWS)
  238. docs := e.Group("/_doc/")
  239. docs.Use(middleware.GzipWithConfig(middleware.GzipConfig{Level: 5}))
  240. docs.GET("*", func(c echo.Context) error {
  241. hash, fname := calcHash(c.Param("*"))
  242. if cached, ok := fileCaches[hash]; ok {
  243. fmt.Println("Serving cached: " + fname)
  244. return c.Blob(http.StatusOK, "text/markdown", []byte(cached.File))
  245. }
  246. if _, ok := fileTree[hash]; !ok {
  247. return c.Blob(http.StatusOK, "text/markdown", nil)
  248. }
  249. fmt.Println("Serving from file: " + fname)
  250. return c.File(fname)
  251. })
  252. docs.DELETE("*", func(c echo.Context) error {
  253. hash, fname := calcHash(c.Param("*"))
  254. if _, ok := fileTree[hash]; ok {
  255. err = os.Remove(fname)
  256. if err != nil {
  257. e.Logger.Errorf("Failed to remove file %s: %v", fname, err)
  258. }
  259. }
  260. return c.JSON(http.StatusOK, true)
  261. })
  262. e.Static("/js", filepath.Join(*distDir, "js"))
  263. e.Static("/css", filepath.Join(*distDir, "css"))
  264. e.Static("/fonts", filepath.Join(*distDir, "fonts"))
  265. e.Static("/img", filepath.Join(*distDir, "img"))
  266. e.File("/favicon.png", filepath.Join(*distDir, "favicon.png"))
  267. e.File("/favicon.ico", filepath.Join(*distDir, "favicon.png"))
  268. e.GET("/*", func(c echo.Context) error {
  269. return c.File(filepath.Join(*distDir, "index.html"))
  270. })
  271. go hub.run()
  272. e.Logger.Fatal(e.Start(*adderss))
  273. }