import.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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 model
  17. import (
  18. "bytes"
  19. "encoding/json"
  20. "errors"
  21. "fmt"
  22. "io"
  23. "io/fs"
  24. "math/rand"
  25. "os"
  26. "path"
  27. "path/filepath"
  28. "runtime/debug"
  29. "sort"
  30. "strconv"
  31. "strings"
  32. "time"
  33. "github.com/88250/gulu"
  34. "github.com/88250/lute/ast"
  35. "github.com/88250/lute/html"
  36. "github.com/88250/lute/parse"
  37. "github.com/88250/protyle"
  38. "github.com/mattn/go-zglob"
  39. "github.com/siyuan-note/siyuan/kernel/filesys"
  40. "github.com/siyuan-note/siyuan/kernel/sql"
  41. "github.com/siyuan-note/siyuan/kernel/treenode"
  42. "github.com/siyuan-note/siyuan/kernel/util"
  43. )
  44. func ImportSY(zipPath, boxID, toPath string) (err error) {
  45. util.PushEndlessProgress(Conf.Language(73))
  46. defer util.ClearPushProgress(100)
  47. baseName := filepath.Base(zipPath)
  48. ext := filepath.Ext(baseName)
  49. baseName = strings.TrimSuffix(baseName, ext)
  50. unzipPath := filepath.Join(filepath.Dir(zipPath), baseName+"-"+gulu.Rand.String(7))
  51. err = gulu.Zip.Unzip(zipPath, unzipPath)
  52. if nil != err {
  53. return
  54. }
  55. defer os.RemoveAll(unzipPath)
  56. var syPaths []string
  57. filepath.Walk(unzipPath, func(path string, info fs.FileInfo, err error) error {
  58. if nil != err {
  59. return err
  60. }
  61. if !info.IsDir() && strings.HasSuffix(info.Name(), ".sy") {
  62. syPaths = append(syPaths, path)
  63. }
  64. return nil
  65. })
  66. unzipRootPaths, err := filepath.Glob(unzipPath + "/*")
  67. if nil != err {
  68. return
  69. }
  70. if 1 != len(unzipRootPaths) {
  71. util.LogErrorf("invalid .sy.zip")
  72. return errors.New("invalid .sy.zip")
  73. }
  74. unzipRootPath := unzipRootPaths[0]
  75. luteEngine := util.NewLute()
  76. blockIDs := map[string]string{}
  77. trees := map[string]*parse.Tree{}
  78. // 重新生成块 ID
  79. for _, syPath := range syPaths {
  80. data, readErr := os.ReadFile(syPath)
  81. if nil != readErr {
  82. util.LogErrorf("read .sy [%s] failed: %s", syPath, readErr)
  83. err = readErr
  84. return
  85. }
  86. tree, _, parseErr := protyle.ParseJSON(luteEngine, data)
  87. if nil != parseErr {
  88. util.LogErrorf("parse .sy [%s] failed: %s", syPath, parseErr)
  89. err = parseErr
  90. return
  91. }
  92. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  93. if !entering {
  94. return ast.WalkContinue
  95. }
  96. if "" != n.ID {
  97. newNodeID := ast.NewNodeID()
  98. blockIDs[n.ID] = newNodeID
  99. n.ID = newNodeID
  100. n.SetIALAttr("id", newNodeID)
  101. }
  102. return ast.WalkContinue
  103. })
  104. tree.ID = tree.Root.ID
  105. tree.Path = filepath.ToSlash(strings.TrimPrefix(syPath, unzipRootPath))
  106. trees[tree.ID] = tree
  107. }
  108. // 引用指向重新生成的块 ID
  109. for _, tree := range trees {
  110. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  111. if !entering {
  112. return ast.WalkContinue
  113. }
  114. if ast.NodeBlockRefID == n.Type {
  115. newDefID := blockIDs[n.TokensStr()]
  116. if "" != newDefID {
  117. n.Tokens = gulu.Str.ToBytes(newDefID)
  118. } else {
  119. util.LogWarnf("not found def [" + n.TokensStr() + "]")
  120. }
  121. }
  122. return ast.WalkContinue
  123. })
  124. }
  125. // 写回 .sy
  126. for _, tree := range trees {
  127. syPath := filepath.Join(unzipRootPath, tree.Path)
  128. renderer := protyle.NewJSONRenderer(tree, luteEngine.RenderOptions)
  129. data := renderer.Render()
  130. buf := bytes.Buffer{}
  131. buf.Grow(4096)
  132. if err = json.Indent(&buf, data, "", "\t"); nil != err {
  133. return
  134. }
  135. data = buf.Bytes()
  136. if err = os.WriteFile(syPath, data, 0644); nil != err {
  137. util.LogErrorf("write .sy [%s] failed: %s", syPath, err)
  138. return
  139. }
  140. newSyPath := filepath.Join(filepath.Dir(syPath), tree.ID+".sy")
  141. if err = os.Rename(syPath, newSyPath); nil != err {
  142. util.LogErrorf("rename .sy from [%s] to [%s] failed: %s", syPath, newSyPath, err)
  143. return
  144. }
  145. }
  146. // 重命名文件路径
  147. renamePaths := map[string]string{}
  148. filepath.Walk(unzipRootPath, func(path string, info fs.FileInfo, err error) error {
  149. if nil != err {
  150. return err
  151. }
  152. if info.IsDir() && util.IsIDPattern(info.Name()) {
  153. renamePaths[path] = path
  154. }
  155. return nil
  156. })
  157. for p, _ := range renamePaths {
  158. originalPath := p
  159. p = strings.TrimPrefix(p, unzipRootPath)
  160. p = filepath.ToSlash(p)
  161. parts := strings.Split(p, "/")
  162. buf := bytes.Buffer{}
  163. buf.WriteString("/")
  164. for i, part := range parts {
  165. if "" == part {
  166. continue
  167. }
  168. newNodeID := blockIDs[part]
  169. if "" != newNodeID {
  170. buf.WriteString(newNodeID)
  171. } else {
  172. buf.WriteString(part)
  173. }
  174. if i < len(parts)-1 {
  175. buf.WriteString("/")
  176. }
  177. }
  178. newPath := buf.String()
  179. renamePaths[originalPath] = filepath.Join(unzipRootPath, newPath)
  180. }
  181. var oldPaths []string
  182. for oldPath, _ := range renamePaths {
  183. oldPaths = append(oldPaths, oldPath)
  184. }
  185. sort.Slice(oldPaths, func(i, j int) bool {
  186. return strings.Count(oldPaths[i], string(os.PathSeparator)) < strings.Count(oldPaths[j], string(os.PathSeparator))
  187. })
  188. for i, oldPath := range oldPaths {
  189. newPath := renamePaths[oldPath]
  190. if err = os.Rename(oldPath, newPath); nil != err {
  191. util.LogErrorf("rename path from [%s] to [%s] failed: %s", oldPath, renamePaths[oldPath], err)
  192. return errors.New("rename path failed")
  193. }
  194. delete(renamePaths, oldPath)
  195. var toRemoves []string
  196. newRenamedPaths := map[string]string{}
  197. for oldP, newP := range renamePaths {
  198. if strings.HasPrefix(oldP, oldPath) {
  199. renamedOldP := strings.Replace(oldP, oldPath, newPath, 1)
  200. newRenamedPaths[renamedOldP] = newP
  201. toRemoves = append(toRemoves, oldPath)
  202. }
  203. }
  204. for _, toRemove := range toRemoves {
  205. delete(renamePaths, toRemove)
  206. }
  207. for oldP, newP := range newRenamedPaths {
  208. renamePaths[oldP] = newP
  209. }
  210. for j := i + 1; j < len(oldPaths); j++ {
  211. if strings.HasPrefix(oldPaths[j], oldPath) {
  212. renamedOldP := strings.Replace(oldPaths[j], oldPath, newPath, 1)
  213. oldPaths[j] = renamedOldP
  214. }
  215. }
  216. }
  217. assetsDirs, err := zglob.Glob(unzipRootPath + "/**/assets")
  218. if nil != err {
  219. return
  220. }
  221. if 0 < len(assetsDirs) {
  222. for _, assets := range assetsDirs {
  223. if gulu.File.IsDir(assets) {
  224. dataAssets := filepath.Join(util.DataDir, "assets")
  225. if err = gulu.File.Copy(assets, dataAssets); nil != err {
  226. util.LogErrorf("copy assets from [%s] to [%s] failed: %s", assets, dataAssets, err)
  227. return
  228. }
  229. }
  230. os.RemoveAll(assets)
  231. }
  232. }
  233. writingDataLock.Lock()
  234. defer writingDataLock.Unlock()
  235. filesys.ReleaseAllFileLocks()
  236. var baseTargetPath string
  237. if "/" == toPath {
  238. baseTargetPath = "/"
  239. } else {
  240. block := treenode.GetBlockTreeRootByPath(boxID, toPath)
  241. if nil == block {
  242. util.LogErrorf("not found block by path [%s]", toPath)
  243. return nil
  244. }
  245. baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
  246. }
  247. targetDir := filepath.Join(util.DataDir, boxID, baseTargetPath)
  248. if err = os.MkdirAll(targetDir, 0755); nil != err {
  249. return
  250. }
  251. if err = stableCopy(unzipRootPath, targetDir); nil != err {
  252. util.LogErrorf("copy data dir from [%s] to [%s] failed: %s", unzipRootPath, util.DataDir, err)
  253. err = errors.New("copy data failed")
  254. return
  255. }
  256. IncWorkspaceDataVer()
  257. RefreshFileTree()
  258. return
  259. }
  260. func ImportData(zipPath string) (err error) {
  261. util.PushEndlessProgress(Conf.Language(73))
  262. defer util.ClearPushProgress(100)
  263. baseName := filepath.Base(zipPath)
  264. ext := filepath.Ext(baseName)
  265. baseName = strings.TrimSuffix(baseName, ext)
  266. unzipPath := filepath.Join(filepath.Dir(zipPath), baseName)
  267. err = gulu.Zip.Unzip(zipPath, unzipPath)
  268. if nil != err {
  269. return
  270. }
  271. defer os.RemoveAll(unzipPath)
  272. files, err := filepath.Glob(filepath.Join(unzipPath, "*/.siyuan/conf.json"))
  273. if nil != err {
  274. util.LogErrorf("glob conf.json failed: %s", err)
  275. return errors.New("not found conf.json")
  276. }
  277. if 1 > len(files) {
  278. return errors.New("not found conf.json")
  279. }
  280. confPath := files[0]
  281. confData, err := os.ReadFile(confPath)
  282. if nil != err {
  283. return errors.New("read conf.json failed")
  284. }
  285. dataConf := &filesys.DataConf{}
  286. if err = gulu.JSON.UnmarshalJSON(confData, dataConf); nil != err {
  287. util.LogErrorf("unmarshal conf.json failed: %s", err)
  288. return errors.New("unmarshal conf.json failed")
  289. }
  290. dataConf.Device = util.GetDeviceID()
  291. confData, err = gulu.JSON.MarshalJSON(dataConf)
  292. if nil != err {
  293. util.LogErrorf("marshal conf.json failed: %s", err)
  294. return errors.New("marshal conf.json failed")
  295. }
  296. if err = os.WriteFile(confPath, confData, 0644); nil != err {
  297. util.LogErrorf("write conf.json failed: %s", err)
  298. return errors.New("write conf.json failed")
  299. }
  300. writingDataLock.Lock()
  301. defer writingDataLock.Unlock()
  302. filesys.ReleaseAllFileLocks()
  303. tmpDataPath := filepath.Dir(filepath.Dir(confPath))
  304. if err = stableCopy(tmpDataPath, util.DataDir); nil != err {
  305. util.LogErrorf("copy data dir from [%s] to [%s] failed: %s", tmpDataPath, util.DataDir, err)
  306. err = errors.New("copy data failed")
  307. return
  308. }
  309. IncWorkspaceDataVer()
  310. RefreshFileTree()
  311. return
  312. }
  313. func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
  314. util.PushEndlessProgress(Conf.Language(73))
  315. WaitForWritingFiles()
  316. writingDataLock.Lock()
  317. defer writingDataLock.Unlock()
  318. box := Conf.Box(boxID)
  319. var baseHPath, baseTargetPath, boxLocalPath string
  320. if "/" == toPath {
  321. baseHPath = "/"
  322. baseTargetPath = "/"
  323. } else {
  324. block := treenode.GetBlockTreeRootByPath(boxID, toPath)
  325. if nil == block {
  326. util.LogErrorf("not found block by path [%s]", toPath)
  327. return nil
  328. }
  329. baseHPath = block.HPath
  330. baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
  331. }
  332. boxLocalPath = filepath.Join(util.DataDir, boxID)
  333. if gulu.File.IsDir(localPath) {
  334. folderName := filepath.Base(localPath)
  335. p := path.Join(toPath, folderName)
  336. if box.Exist(p) {
  337. return errors.New(Conf.Language(1))
  338. }
  339. // 收集所有资源文件
  340. assets := map[string]string{}
  341. filepath.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
  342. if localPath == currentPath {
  343. return nil
  344. }
  345. if strings.HasPrefix(info.Name(), ".") {
  346. if info.IsDir() {
  347. return filepath.SkipDir
  348. }
  349. return nil
  350. }
  351. if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
  352. dest := currentPath
  353. assets[dest] = currentPath
  354. return nil
  355. }
  356. return nil
  357. })
  358. targetPaths := map[string]string{}
  359. // md 转换 sy
  360. i := 0
  361. filepath.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
  362. if strings.HasPrefix(info.Name(), ".") {
  363. if info.IsDir() {
  364. return filepath.SkipDir
  365. }
  366. return nil
  367. }
  368. var tree *parse.Tree
  369. ext := path.Ext(info.Name())
  370. title := strings.TrimSuffix(info.Name(), ext)
  371. id := ast.NewNodeID()
  372. curRelPath := filepath.ToSlash(strings.TrimPrefix(currentPath, localPath))
  373. targetPath := path.Join(baseTargetPath, id)
  374. if "" == curRelPath {
  375. curRelPath = "/"
  376. } else {
  377. dirPath := targetPaths[path.Dir(curRelPath)]
  378. targetPath = path.Join(dirPath, id)
  379. }
  380. targetPath = strings.ReplaceAll(targetPath, ".sy/", "/")
  381. targetPath += ".sy"
  382. targetPaths[curRelPath] = targetPath
  383. hPath := path.Join(baseHPath, filepath.ToSlash(strings.TrimPrefix(currentPath, localPath)))
  384. hPath = strings.TrimSuffix(hPath, ext)
  385. if info.IsDir() {
  386. tree = treenode.NewTree(boxID, targetPath, hPath, title)
  387. if err = filesys.WriteTree(tree); nil != err {
  388. return io.EOF
  389. }
  390. return nil
  391. }
  392. if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
  393. return nil
  394. }
  395. data, readErr := os.ReadFile(currentPath)
  396. if nil != readErr {
  397. err = readErr
  398. return io.EOF
  399. }
  400. tree = parseKTree(data)
  401. if nil == tree {
  402. util.LogErrorf("parse tree [%s] failed", currentPath)
  403. return nil
  404. }
  405. tree.ID = id
  406. tree.Root.ID = id
  407. tree.Root.SetIALAttr("id", tree.Root.ID)
  408. tree.Root.SetIALAttr("title", title)
  409. tree.Box = boxID
  410. targetPath = path.Join(path.Dir(targetPath), tree.Root.ID+".sy")
  411. tree.Path = targetPath
  412. targetPaths[curRelPath] = targetPath
  413. tree.HPath = hPath
  414. docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
  415. assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
  416. currentDir := filepath.Dir(currentPath)
  417. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  418. if !entering || ast.NodeLinkDest != n.Type {
  419. return ast.WalkContinue
  420. }
  421. dest := n.TokensStr()
  422. if !util.IsRelativePath(dest) || "" == dest {
  423. return ast.WalkContinue
  424. }
  425. absDest := filepath.Join(currentDir, dest)
  426. fullPath, exist := assets[absDest]
  427. if !exist {
  428. absDest = filepath.Join(currentDir, string(html.DecodeDestination([]byte(dest))))
  429. }
  430. fullPath, exist = assets[absDest]
  431. if exist {
  432. name := filepath.Base(fullPath)
  433. ext := filepath.Ext(name)
  434. name = strings.TrimSuffix(name, ext)
  435. name += "-" + ast.NewNodeID() + ext
  436. assetTargetPath := filepath.Join(assetDirPath, name)
  437. delete(assets, absDest)
  438. if err = gulu.File.Copy(fullPath, assetTargetPath); nil != err {
  439. util.LogErrorf("copy asset from [%s] to [%s] failed: %s", fullPath, assetTargetPath, err)
  440. return ast.WalkContinue
  441. }
  442. n.Tokens = gulu.Str.ToBytes("assets/" + name)
  443. }
  444. return ast.WalkContinue
  445. })
  446. reassignIDUpdated(tree)
  447. if err = filesys.WriteTree(tree); nil != err {
  448. return io.EOF
  449. }
  450. i++
  451. if 0 == i%4 {
  452. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(66), util.ShortPathForBootingDisplay(tree.Path)))
  453. }
  454. return nil
  455. })
  456. if nil != err {
  457. return err
  458. }
  459. IncWorkspaceDataVer()
  460. RefreshFileTree()
  461. } else { // 导入单个文件
  462. fileName := filepath.Base(localPath)
  463. if !strings.HasSuffix(fileName, ".md") && !strings.HasSuffix(fileName, ".markdown") {
  464. return errors.New(Conf.Language(79))
  465. }
  466. title := strings.TrimSuffix(fileName, ".markdown")
  467. title = strings.TrimSuffix(title, ".md")
  468. targetPath := strings.TrimSuffix(toPath, ".sy")
  469. id := ast.NewNodeID()
  470. targetPath = path.Join(targetPath, id+".sy")
  471. var data []byte
  472. data, err = os.ReadFile(localPath)
  473. if nil != err {
  474. return err
  475. }
  476. tree := parseKTree(data)
  477. if nil == tree {
  478. msg := fmt.Sprintf("parse tree [%s] failed", localPath)
  479. util.LogErrorf(msg)
  480. return errors.New(msg)
  481. }
  482. tree.ID = id
  483. tree.Root.ID = id
  484. tree.Root.SetIALAttr("id", tree.Root.ID)
  485. tree.Root.SetIALAttr("title", title)
  486. tree.Box = boxID
  487. tree.Path = targetPath
  488. tree.HPath = path.Join(baseHPath, title)
  489. docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
  490. assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
  491. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  492. if !entering || ast.NodeLinkDest != n.Type {
  493. return ast.WalkContinue
  494. }
  495. dest := n.TokensStr()
  496. if !util.IsRelativePath(dest) {
  497. return ast.WalkContinue
  498. }
  499. dest = filepath.ToSlash(dest)
  500. if "" == dest {
  501. return ast.WalkContinue
  502. }
  503. absolutePath := filepath.Join(filepath.Dir(localPath), dest)
  504. exist := gulu.File.IsExist(absolutePath)
  505. if !exist {
  506. absolutePath = filepath.Join(filepath.Dir(localPath), string(html.DecodeDestination([]byte(dest))))
  507. exist = gulu.File.IsExist(absolutePath)
  508. }
  509. if exist {
  510. name := filepath.Base(absolutePath)
  511. ext := filepath.Ext(name)
  512. name = strings.TrimSuffix(name, ext)
  513. name += "-" + ast.NewNodeID() + ext
  514. assetTargetPath := filepath.Join(assetDirPath, name)
  515. if err = gulu.File.CopyFile(absolutePath, assetTargetPath); nil != err {
  516. util.LogErrorf("copy asset from [%s] to [%s] failed: %s", absolutePath, assetTargetPath, err)
  517. return ast.WalkContinue
  518. }
  519. n.Tokens = gulu.Str.ToBytes("assets/" + name)
  520. }
  521. return ast.WalkContinue
  522. })
  523. reassignIDUpdated(tree)
  524. if err = indexWriteJSONQueue(tree); nil != err {
  525. return
  526. }
  527. IncWorkspaceDataVer()
  528. sql.WaitForWritingDatabase()
  529. util.PushEndlessProgress(Conf.Language(58))
  530. go func() {
  531. time.Sleep(2 * time.Second)
  532. util.ReloadUI()
  533. }()
  534. }
  535. debug.FreeOSMemory()
  536. IncWorkspaceDataVer()
  537. return
  538. }
  539. func reassignIDUpdated(tree *parse.Tree) {
  540. var blockCount int
  541. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  542. if !entering || "" == n.ID {
  543. return ast.WalkContinue
  544. }
  545. blockCount++
  546. return ast.WalkContinue
  547. })
  548. ids := make([]string, blockCount)
  549. min, _ := strconv.ParseInt(time.Now().Add(-1*time.Duration(blockCount)*time.Second).Format("20060102150405"), 10, 64)
  550. for i := 0; i < blockCount; i++ {
  551. ids[i] = newID(fmt.Sprintf("%d", min))
  552. min++
  553. }
  554. var i int
  555. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  556. if !entering || "" == n.ID {
  557. return ast.WalkContinue
  558. }
  559. n.ID = ids[i]
  560. n.SetIALAttr("id", n.ID)
  561. n.SetIALAttr("updated", util.TimeFromID(n.ID))
  562. i++
  563. return ast.WalkContinue
  564. })
  565. tree.ID = tree.Root.ID
  566. tree.Path = path.Join(path.Dir(tree.Path), tree.ID+".sy")
  567. tree.Root.SetIALAttr("id", tree.Root.ID)
  568. }
  569. func newID(t string) string {
  570. return t + "-" + randStr(7)
  571. }
  572. func randStr(length int) string {
  573. letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
  574. b := make([]rune, length)
  575. for i := range b {
  576. b[i] = letter[rand.Intn(len(letter))]
  577. }
  578. return string(b)
  579. }