filetree.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. // SiYuan - Build Your Eternal Digital Garden
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package api
  17. import (
  18. "fmt"
  19. "net/http"
  20. "path"
  21. "regexp"
  22. "strings"
  23. "unicode/utf8"
  24. "github.com/88250/gulu"
  25. "github.com/gin-gonic/gin"
  26. "github.com/siyuan-note/siyuan/kernel/filesys"
  27. "github.com/siyuan-note/siyuan/kernel/model"
  28. "github.com/siyuan-note/siyuan/kernel/util"
  29. )
  30. func refreshFiletree(c *gin.Context) {
  31. ret := gulu.Ret.NewResult()
  32. defer c.JSON(http.StatusOK, ret)
  33. model.RefreshFileTree()
  34. }
  35. func doc2Heading(c *gin.Context) {
  36. ret := gulu.Ret.NewResult()
  37. defer c.JSON(http.StatusOK, ret)
  38. arg, ok := util.JsonArg(c, ret)
  39. if !ok {
  40. return
  41. }
  42. srcID := arg["srcID"].(string)
  43. targetID := arg["targetID"].(string)
  44. after := arg["after"].(bool)
  45. srcTreeBox, srcTreePath, err := model.Doc2Heading(srcID, targetID, after)
  46. if nil != err {
  47. ret.Code = -1
  48. ret.Msg = err.Error()
  49. ret.Data = map[string]interface{}{"closeTimeout": 5000}
  50. return
  51. }
  52. ret.Data = map[string]interface{}{
  53. "srcTreeBox": srcTreeBox,
  54. "srcTreePath": srcTreePath,
  55. }
  56. }
  57. func heading2Doc(c *gin.Context) {
  58. ret := gulu.Ret.NewResult()
  59. defer c.JSON(http.StatusOK, ret)
  60. arg, ok := util.JsonArg(c, ret)
  61. if !ok {
  62. return
  63. }
  64. srcHeadingID := arg["srcHeadingID"].(string)
  65. targetNotebook := arg["targetNoteBook"].(string)
  66. targetPath := arg["targetPath"].(string)
  67. srcRootBlockID, targetPath, err := model.Heading2Doc(srcHeadingID, targetNotebook, targetPath)
  68. if nil != err {
  69. ret.Code = -1
  70. ret.Msg = err.Error()
  71. ret.Data = map[string]interface{}{"closeTimeout": 5000}
  72. return
  73. }
  74. model.WaitForWritingFiles()
  75. tree, err := model.LoadTree(targetNotebook, targetPath)
  76. if nil != err {
  77. ret.Code = -1
  78. ret.Msg = err.Error()
  79. return
  80. }
  81. name := path.Base(targetPath)
  82. box := model.Conf.Box(targetNotebook)
  83. files, _, _ := model.ListDocTree(targetNotebook, path.Dir(targetPath), model.Conf.FileTree.Sort)
  84. evt := util.NewCmdResult("heading2doc", 0, util.PushModeBroadcast, util.PushModeNone)
  85. evt.Data = map[string]interface{}{
  86. "box": box,
  87. "path": targetPath,
  88. "files": files,
  89. "name": name,
  90. "id": tree.Root.ID,
  91. "srcRootBlockID": srcRootBlockID,
  92. }
  93. evt.Callback = arg["callback"]
  94. util.PushEvent(evt)
  95. }
  96. func li2Doc(c *gin.Context) {
  97. ret := gulu.Ret.NewResult()
  98. defer c.JSON(http.StatusOK, ret)
  99. arg, ok := util.JsonArg(c, ret)
  100. if !ok {
  101. return
  102. }
  103. srcListItemID := arg["srcListItemID"].(string)
  104. targetNotebook := arg["targetNoteBook"].(string)
  105. targetPath := arg["targetPath"].(string)
  106. srcRootBlockID, targetPath, err := model.ListItem2Doc(srcListItemID, targetNotebook, targetPath)
  107. if nil != err {
  108. ret.Code = -1
  109. ret.Msg = err.Error()
  110. ret.Data = map[string]interface{}{"closeTimeout": 5000}
  111. return
  112. }
  113. model.WaitForWritingFiles()
  114. tree, err := model.LoadTree(targetNotebook, targetPath)
  115. if nil != err {
  116. ret.Code = -1
  117. ret.Msg = err.Error()
  118. return
  119. }
  120. name := path.Base(targetPath)
  121. box := model.Conf.Box(targetNotebook)
  122. files, _, _ := model.ListDocTree(targetNotebook, path.Dir(targetPath), model.Conf.FileTree.Sort)
  123. evt := util.NewCmdResult("li2doc", 0, util.PushModeBroadcast, util.PushModeNone)
  124. evt.Data = map[string]interface{}{
  125. "box": box,
  126. "path": targetPath,
  127. "files": files,
  128. "name": name,
  129. "id": tree.Root.ID,
  130. "srcRootBlockID": srcRootBlockID,
  131. }
  132. evt.Callback = arg["callback"]
  133. util.PushEvent(evt)
  134. }
  135. func getHPathByPath(c *gin.Context) {
  136. ret := gulu.Ret.NewResult()
  137. defer c.JSON(http.StatusOK, ret)
  138. arg, ok := util.JsonArg(c, ret)
  139. if !ok {
  140. return
  141. }
  142. notebook := arg["notebook"].(string)
  143. p := arg["path"].(string)
  144. hPath, err := model.GetHPathByPath(notebook, p)
  145. if nil != err {
  146. ret.Code = -1
  147. ret.Msg = err.Error()
  148. return
  149. }
  150. ret.Data = hPath
  151. }
  152. func getHPathByID(c *gin.Context) {
  153. ret := gulu.Ret.NewResult()
  154. defer c.JSON(http.StatusOK, ret)
  155. arg, ok := util.JsonArg(c, ret)
  156. if !ok {
  157. return
  158. }
  159. id := arg["id"].(string)
  160. hPath, err := model.GetHPathByID(id)
  161. if nil != err {
  162. ret.Code = -1
  163. ret.Msg = err.Error()
  164. return
  165. }
  166. ret.Data = hPath
  167. }
  168. func getFullHPathByID(c *gin.Context) {
  169. ret := gulu.Ret.NewResult()
  170. defer c.JSON(http.StatusOK, ret)
  171. arg, ok := util.JsonArg(c, ret)
  172. if !ok {
  173. return
  174. }
  175. if nil == arg["id"] {
  176. return
  177. }
  178. id := arg["id"].(string)
  179. hPath, err := model.GetFullHPathByID(id)
  180. if nil != err {
  181. ret.Code = -1
  182. ret.Msg = err.Error()
  183. return
  184. }
  185. ret.Data = hPath
  186. }
  187. func moveDoc(c *gin.Context) {
  188. ret := gulu.Ret.NewResult()
  189. defer c.JSON(http.StatusOK, ret)
  190. arg, ok := util.JsonArg(c, ret)
  191. if !ok {
  192. return
  193. }
  194. fromNotebook := arg["fromNotebook"].(string)
  195. toNotebook := arg["toNotebook"].(string)
  196. fromPath := arg["fromPath"].(string)
  197. toPath := arg["toPath"].(string)
  198. newPath, err := model.MoveDoc(fromNotebook, fromPath, toNotebook, toPath)
  199. if nil != err {
  200. ret.Code = -1
  201. ret.Msg = err.Error()
  202. ret.Data = map[string]interface{}{"closeTimeout": 7000}
  203. return
  204. }
  205. evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast, util.PushModeNone)
  206. evt.Data = map[string]interface{}{
  207. "fromNotebook": fromNotebook,
  208. "toNotebook": toNotebook,
  209. "fromPath": fromPath,
  210. "toPath": toPath,
  211. "newPath": newPath,
  212. }
  213. util.PushEvent(evt)
  214. }
  215. func removeDoc(c *gin.Context) {
  216. ret := gulu.Ret.NewResult()
  217. defer c.JSON(http.StatusOK, ret)
  218. arg, ok := util.JsonArg(c, ret)
  219. if !ok {
  220. return
  221. }
  222. notebook := arg["notebook"].(string)
  223. p := arg["path"].(string)
  224. err := model.RemoveDoc(notebook, p)
  225. if nil != err {
  226. ret.Code = -1
  227. ret.Msg = err.Error()
  228. return
  229. }
  230. evt := util.NewCmdResult("remove", 0, util.PushModeBroadcast, util.PushModeNone)
  231. evt.Data = map[string]interface{}{
  232. "box": notebook,
  233. "path": p,
  234. }
  235. util.PushEvent(evt)
  236. }
  237. func renameDoc(c *gin.Context) {
  238. ret := gulu.Ret.NewResult()
  239. defer c.JSON(http.StatusOK, ret)
  240. arg, ok := util.JsonArg(c, ret)
  241. if !ok {
  242. return
  243. }
  244. notebook := arg["notebook"].(string)
  245. p := arg["path"].(string)
  246. title := arg["title"].(string)
  247. err := model.RenameDoc(notebook, p, title)
  248. if nil != err {
  249. ret.Code = -1
  250. ret.Msg = err.Error()
  251. return
  252. }
  253. return
  254. }
  255. func duplicateDoc(c *gin.Context) {
  256. ret := gulu.Ret.NewResult()
  257. defer c.JSON(http.StatusOK, ret)
  258. arg, ok := util.JsonArg(c, ret)
  259. if !ok {
  260. return
  261. }
  262. id := arg["id"].(string)
  263. err := model.DuplicateDoc(id)
  264. if nil != err {
  265. ret.Code = -1
  266. ret.Msg = err.Error()
  267. ret.Data = map[string]interface{}{"closeTimeout": 7000}
  268. return
  269. }
  270. block, _ := model.GetBlock(id)
  271. p := block.Path
  272. notebook := block.Box
  273. box := model.Conf.Box(notebook)
  274. tree, err := model.LoadTree(box.ID, p)
  275. if nil != err {
  276. ret.Code = -1
  277. ret.Msg = err.Error()
  278. return
  279. }
  280. pushCreate(box, p, tree.Root.ID, arg)
  281. }
  282. func createDoc(c *gin.Context) {
  283. ret := gulu.Ret.NewResult()
  284. defer c.JSON(http.StatusOK, ret)
  285. arg, ok := util.JsonArg(c, ret)
  286. if !ok {
  287. return
  288. }
  289. notebook := arg["notebook"].(string)
  290. p := arg["path"].(string)
  291. title := arg["title"].(string)
  292. md := arg["md"].(string)
  293. err := model.CreateDocByMd(notebook, p, title, md)
  294. if nil != err {
  295. ret.Code = -1
  296. ret.Msg = err.Error()
  297. ret.Data = map[string]interface{}{"closeTimeout": 7000}
  298. return
  299. }
  300. box := model.Conf.Box(notebook)
  301. tree, err := model.LoadTree(box.ID, p)
  302. if nil != err {
  303. ret.Code = -1
  304. ret.Msg = err.Error()
  305. return
  306. }
  307. pushCreate(box, p, tree.Root.ID, arg)
  308. }
  309. func createDailyNote(c *gin.Context) {
  310. ret := gulu.Ret.NewResult()
  311. defer c.JSON(http.StatusOK, ret)
  312. arg, ok := util.JsonArg(c, ret)
  313. if !ok {
  314. return
  315. }
  316. notebook := arg["notebook"].(string)
  317. p, err := model.CreateDailyNote(notebook)
  318. if nil != err {
  319. if model.ErrBoxNotFound == err {
  320. ret.Code = 1
  321. } else {
  322. ret.Code = -1
  323. }
  324. ret.Msg = err.Error()
  325. return
  326. }
  327. box := model.Conf.Box(notebook)
  328. model.WaitForWritingFiles()
  329. tree, err := model.LoadTree(box.ID, p)
  330. if nil != err {
  331. ret.Code = -1
  332. ret.Msg = err.Error()
  333. return
  334. }
  335. evt := util.NewCmdResult("createdailynote", 0, util.PushModeBroadcast, util.PushModeNone)
  336. name := path.Base(p)
  337. files, _, _ := model.ListDocTree(box.ID, path.Dir(p), model.Conf.FileTree.Sort)
  338. evt.Data = map[string]interface{}{
  339. "box": box,
  340. "path": p,
  341. "files": files,
  342. "name": name,
  343. "id": tree.Root.ID,
  344. }
  345. evt.Callback = arg["callback"]
  346. util.PushEvent(evt)
  347. }
  348. func createDocWithMd(c *gin.Context) {
  349. ret := gulu.Ret.NewResult()
  350. defer c.JSON(http.StatusOK, ret)
  351. arg, ok := util.JsonArg(c, ret)
  352. if !ok {
  353. return
  354. }
  355. notebook := arg["notebook"].(string)
  356. hPath := arg["path"].(string)
  357. markdown := arg["markdown"].(string)
  358. baseName := path.Base(hPath)
  359. dir := path.Dir(hPath)
  360. r, _ := regexp.Compile("\r\n|\r|\n|\u2028|\u2029|\t|/")
  361. baseName = r.ReplaceAllString(baseName, "")
  362. if 512 < utf8.RuneCountInString(baseName) {
  363. baseName = gulu.Str.SubStr(baseName, 512)
  364. }
  365. hPath = path.Join(dir, baseName)
  366. if !strings.HasPrefix(hPath, "/") {
  367. hPath = "/" + hPath
  368. }
  369. id, err := model.CreateWithMarkdown(notebook, hPath, markdown)
  370. if nil != err {
  371. ret.Code = -1
  372. ret.Msg = err.Error()
  373. return
  374. }
  375. ret.Data = id
  376. box := model.Conf.Box(notebook)
  377. b, _ := model.GetBlock(id)
  378. p := b.Path
  379. pushCreate(box, p, id, arg)
  380. }
  381. func lockFile(c *gin.Context) {
  382. ret := gulu.Ret.NewResult()
  383. defer c.JSON(http.StatusOK, ret)
  384. arg, ok := util.JsonArg(c, ret)
  385. if !ok {
  386. return
  387. }
  388. id := arg["id"].(string)
  389. locked, filePath := model.LockFileByBlockID(id)
  390. if !locked {
  391. ret.Code = -1
  392. ret.Msg = fmt.Sprintf(model.Conf.Language(75), filePath)
  393. ret.Data = map[string]interface{}{"closeTimeout": 5000}
  394. }
  395. }
  396. func getDocNameTemplate(c *gin.Context) {
  397. ret := gulu.Ret.NewResult()
  398. defer c.JSON(http.StatusOK, ret)
  399. arg, ok := util.JsonArg(c, ret)
  400. if !ok {
  401. return
  402. }
  403. notebook := arg["notebook"].(string)
  404. box := model.Conf.Box(notebook)
  405. nameTemplate := model.Conf.FileTree.CreateDocNameTemplate
  406. if nil != box {
  407. nameTemplate = box.GetConf().CreateDocNameTemplate
  408. }
  409. if "" == nameTemplate {
  410. nameTemplate = model.Conf.FileTree.CreateDocNameTemplate
  411. }
  412. name, err := model.RenderCreateDocNameTemplate(nameTemplate)
  413. if nil != err {
  414. ret.Code = -1
  415. ret.Msg = err.Error()
  416. return
  417. }
  418. ret.Data = map[string]interface{}{
  419. "name": name,
  420. }
  421. }
  422. func changeSort(c *gin.Context) {
  423. ret := gulu.Ret.NewResult()
  424. defer c.JSON(http.StatusOK, ret)
  425. arg, ok := util.JsonArg(c, ret)
  426. if !ok {
  427. return
  428. }
  429. notebook := arg["notebook"].(string)
  430. pathsArg := arg["paths"].([]interface{})
  431. var paths []string
  432. for _, p := range pathsArg {
  433. paths = append(paths, p.(string))
  434. }
  435. model.ChangeFileTreeSort(notebook, paths)
  436. }
  437. func searchDocs(c *gin.Context) {
  438. ret := gulu.Ret.NewResult()
  439. defer c.JSON(http.StatusOK, ret)
  440. arg, ok := util.JsonArg(c, ret)
  441. if !ok {
  442. return
  443. }
  444. k := arg["k"].(string)
  445. ret.Data = model.SearchDocsByKeyword(k)
  446. }
  447. func listDocsByPath(c *gin.Context) {
  448. ret := gulu.Ret.NewResult()
  449. defer c.JSON(http.StatusOK, ret)
  450. arg, ok := util.JsonArg(c, ret)
  451. if !ok {
  452. return
  453. }
  454. notebook := arg["notebook"].(string)
  455. p := arg["path"].(string)
  456. sortParam := arg["sort"]
  457. sortMode := model.Conf.FileTree.Sort
  458. if nil != sortParam {
  459. sortMode = int(sortParam.(float64))
  460. }
  461. files, totals, err := model.ListDocTree(notebook, p, sortMode)
  462. if nil != err {
  463. ret.Code = -1
  464. ret.Msg = err.Error()
  465. return
  466. }
  467. if model.Conf.FileTree.MaxListCount < totals {
  468. util.PushMsg(fmt.Sprintf(model.Conf.Language(48), len(files)), 7000)
  469. }
  470. ret.Data = map[string]interface{}{
  471. "box": notebook,
  472. "path": p,
  473. "files": files,
  474. }
  475. // 持久化文档面板排序
  476. model.Conf.FileTree.Sort = sortMode
  477. model.Conf.Save()
  478. }
  479. func getDoc(c *gin.Context) {
  480. ret := gulu.Ret.NewResult()
  481. defer c.JSON(http.StatusOK, ret)
  482. arg, ok := util.JsonArg(c, ret)
  483. if !ok {
  484. return
  485. }
  486. id := arg["id"].(string)
  487. idx := arg["index"]
  488. index := 0
  489. if nil != idx {
  490. index = int(idx.(float64))
  491. }
  492. k := arg["k"]
  493. var keyword string
  494. if nil != k {
  495. keyword = k.(string)
  496. }
  497. m := arg["mode"] // 0: 仅当前 ID,1:向上 2:向下,3:上下都加载,4:加载末尾
  498. mode := 0
  499. if nil != m {
  500. mode = int(m.(float64))
  501. }
  502. s := arg["size"]
  503. size := 102400 // 默认最大加载块数
  504. if nil != s {
  505. size = int(s.(float64))
  506. }
  507. blockCount, content, parentID, parent2ID, rootID, typ, eof, boxID, docPath, err := model.GetDoc(id, index, keyword, mode, size)
  508. if filesys.ErrUnableLockFile == err {
  509. ret.Code = 2
  510. ret.Data = id
  511. return
  512. }
  513. if model.ErrBlockNotFound == err {
  514. ret.Code = 3
  515. return
  516. }
  517. if nil != err {
  518. ret.Code = 1
  519. ret.Msg = err.Error()
  520. return
  521. }
  522. ret.Data = map[string]interface{}{
  523. "id": id,
  524. "mode": mode,
  525. "parentID": parentID,
  526. "parent2ID": parent2ID,
  527. "rootID": rootID,
  528. "type": typ,
  529. "content": content,
  530. "blockCount": blockCount,
  531. "eof": eof,
  532. "box": boxID,
  533. "path": docPath,
  534. }
  535. }
  536. func pushCreate(box *model.Box, p, treeID string, arg map[string]interface{}) {
  537. evt := util.NewCmdResult("create", 0, util.PushModeBroadcast, util.PushModeNone)
  538. name := path.Base(p)
  539. files, _, _ := model.ListDocTree(box.ID, path.Dir(p), model.Conf.FileTree.Sort)
  540. evt.Data = map[string]interface{}{
  541. "box": box,
  542. "path": p,
  543. "files": files,
  544. "name": name,
  545. "id": treeID,
  546. }
  547. evt.Callback = arg["callback"]
  548. util.PushEvent(evt)
  549. }