backup.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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. "crypto/md5"
  20. "crypto/sha256"
  21. "encoding/hex"
  22. "errors"
  23. "fmt"
  24. "io"
  25. "io/fs"
  26. "os"
  27. "path/filepath"
  28. "strings"
  29. "time"
  30. "github.com/88250/gulu"
  31. "github.com/dustin/go-humanize"
  32. "github.com/siyuan-note/encryption"
  33. "github.com/siyuan-note/siyuan/kernel/filesys"
  34. "github.com/siyuan-note/siyuan/kernel/util"
  35. )
  36. type Backup struct {
  37. Size int64 `json:"size"`
  38. HSize string `json:"hSize"`
  39. Updated string `json:"updated"`
  40. SaveDir string `json:"saveDir"` // 本地备份数据存放目录路径
  41. }
  42. type Sync struct {
  43. Size int64 `json:"size"`
  44. HSize string `json:"hSize"`
  45. Updated string `json:"updated"`
  46. CloudName string `json:"cloudName"` // 云端同步数据存放目录名
  47. SaveDir string `json:"saveDir"` // 本地同步数据存放目录路径
  48. }
  49. func RemoveCloudBackup() (err error) {
  50. err = removeCloudDirPath("backup")
  51. return
  52. }
  53. func getCloudAvailableBackupSize() (size int64, err error) {
  54. var sync map[string]interface{}
  55. var assetSize int64
  56. sync, _, assetSize, err = getCloudSpaceOSS()
  57. if nil != err {
  58. return
  59. }
  60. var syncSize int64
  61. if nil != sync {
  62. syncSize = int64(sync["size"].(float64))
  63. }
  64. size = int64(Conf.User.UserSiYuanRepoSize) - syncSize - assetSize
  65. return
  66. }
  67. func GetCloudSpace() (s *Sync, b *Backup, hSize, hAssetSize, hTotalSize string, err error) {
  68. var sync, backup map[string]interface{}
  69. var assetSize int64
  70. sync, backup, assetSize, err = getCloudSpaceOSS()
  71. if nil != err {
  72. return nil, nil, "", "", "", errors.New(Conf.Language(30) + " " + err.Error())
  73. }
  74. var totalSize, syncSize, backupSize int64
  75. var syncUpdated, backupUpdated string
  76. if nil != sync {
  77. syncSize = int64(sync["size"].(float64))
  78. syncUpdated = sync["updated"].(string)
  79. }
  80. s = &Sync{
  81. Size: syncSize,
  82. HSize: humanize.Bytes(uint64(syncSize)),
  83. Updated: syncUpdated,
  84. }
  85. if nil != backup {
  86. backupSize = int64(backup["size"].(float64))
  87. backupUpdated = backup["updated"].(string)
  88. }
  89. b = &Backup{
  90. Size: backupSize,
  91. HSize: humanize.Bytes(uint64(backupSize)),
  92. Updated: backupUpdated,
  93. }
  94. totalSize = syncSize + backupSize + assetSize
  95. hAssetSize = humanize.Bytes(uint64(assetSize))
  96. hSize = humanize.Bytes(uint64(totalSize))
  97. hTotalSize = byteCountSI(int64(Conf.User.UserSiYuanRepoSize))
  98. return
  99. }
  100. func byteCountSI(b int64) string {
  101. const unit = 1000
  102. if b < unit {
  103. return fmt.Sprintf("%d B", b)
  104. }
  105. div, exp := int64(unit), 0
  106. for n := b / unit; n >= unit; n /= unit {
  107. div *= unit
  108. exp++
  109. }
  110. return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
  111. }
  112. func GetLocalBackup() (ret *Backup, err error) {
  113. backupDir := Conf.Backup.GetSaveDir()
  114. if err = os.MkdirAll(backupDir, 0755); nil != err {
  115. return
  116. }
  117. backup, err := os.Stat(backupDir)
  118. ret = &Backup{
  119. Updated: backup.ModTime().Format("2006-01-02 15:04:05"),
  120. SaveDir: Conf.Backup.GetSaveDir(),
  121. }
  122. return
  123. }
  124. func RecoverLocalBackup() (err error) {
  125. if "" == Conf.E2EEPasswd {
  126. return errors.New(Conf.Language(11))
  127. }
  128. data := util.AESDecrypt(Conf.E2EEPasswd)
  129. data, _ = hex.DecodeString(string(data))
  130. passwd := string(data)
  131. CloseWatchAssets()
  132. defer WatchAssets()
  133. // 使用备份恢复时自动暂停同步,避免刚刚恢复后的数据又被同步覆盖 https://github.com/siyuan-note/siyuan/issues/4773
  134. syncEnabled := Conf.Sync.Enabled
  135. Conf.Sync.Enabled = false
  136. Conf.Save()
  137. filesys.ReleaseAllFileLocks()
  138. util.PushEndlessProgress(Conf.Language(63))
  139. util.LogInfof("starting recovery...")
  140. start := time.Now()
  141. decryptedDataDir, err := decryptDataDir(passwd)
  142. if nil != err {
  143. return
  144. }
  145. newDataDir := filepath.Join(util.WorkspaceDir, "data.new")
  146. os.RemoveAll(newDataDir)
  147. if err = os.MkdirAll(newDataDir, 0755); nil != err {
  148. util.ClearPushProgress(100)
  149. return
  150. }
  151. if err = stableCopy(decryptedDataDir, newDataDir); nil != err {
  152. util.ClearPushProgress(100)
  153. return
  154. }
  155. oldDataDir := filepath.Join(util.WorkspaceDir, "data.old")
  156. if err = os.RemoveAll(oldDataDir); nil != err {
  157. util.ClearPushProgress(100)
  158. return
  159. }
  160. // 备份恢复时生成历史 https://github.com/siyuan-note/siyuan/issues/4752
  161. if gulu.File.IsExist(util.DataDir) {
  162. var historyDir string
  163. historyDir, err = util.GetHistoryDir("backup")
  164. if nil != err {
  165. util.LogErrorf("get history dir failed: %s", err)
  166. util.ClearPushProgress(100)
  167. return
  168. }
  169. var dirs []os.DirEntry
  170. dirs, err = os.ReadDir(util.DataDir)
  171. if nil != err {
  172. util.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
  173. util.ClearPushProgress(100)
  174. return
  175. }
  176. for _, dir := range dirs {
  177. from := filepath.Join(util.DataDir, dir.Name())
  178. to := filepath.Join(historyDir, dir.Name())
  179. if err = os.Rename(from, to); nil != err {
  180. util.LogErrorf("rename [%s] to [%s] failed: %s", from, to, err)
  181. util.ClearPushProgress(100)
  182. return
  183. }
  184. }
  185. }
  186. if gulu.File.IsExist(util.DataDir) {
  187. if err = os.RemoveAll(util.DataDir); nil != err {
  188. util.LogErrorf("remove [%s] failed: %s", util.DataDir, err)
  189. util.ClearPushProgress(100)
  190. return
  191. }
  192. }
  193. if err = os.Rename(newDataDir, util.DataDir); nil != err {
  194. util.ClearPushProgress(100)
  195. util.LogErrorf("rename data dir from [%s] to [%s] failed: %s", newDataDir, util.DataDir, err)
  196. return
  197. }
  198. elapsed := time.Now().Sub(start).Seconds()
  199. size, _ := util.SizeOfDirectory(util.DataDir, false)
  200. sizeStr := humanize.Bytes(uint64(size))
  201. util.LogInfof("recovered backup [size=%s] in [%.2fs]", sizeStr, elapsed)
  202. util.PushEndlessProgress(Conf.Language(62))
  203. time.Sleep(2 * time.Second)
  204. RefreshFileTree()
  205. if syncEnabled {
  206. func() {
  207. time.Sleep(5 * time.Second)
  208. util.PushMsg(Conf.Language(134), 0)
  209. }()
  210. }
  211. return
  212. }
  213. func CreateLocalBackup() (err error) {
  214. if "" == Conf.E2EEPasswd {
  215. return errors.New(Conf.Language(11))
  216. }
  217. defer util.ClearPushProgress(100)
  218. util.PushEndlessProgress(Conf.Language(22))
  219. WaitForWritingFiles()
  220. filesys.ReleaseAllFileLocks()
  221. util.LogInfof("creating backup...")
  222. start := time.Now()
  223. data := util.AESDecrypt(Conf.E2EEPasswd)
  224. data, _ = hex.DecodeString(string(data))
  225. passwd := string(data)
  226. encryptedDataDir, err := encryptDataDir(passwd)
  227. if nil != err {
  228. util.LogErrorf("encrypt data dir failed: %s", err)
  229. err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
  230. return
  231. }
  232. newBackupDir := Conf.Backup.GetSaveDir() + ".new"
  233. os.RemoveAll(newBackupDir)
  234. if err = os.MkdirAll(newBackupDir, 0755); nil != err {
  235. err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
  236. return
  237. }
  238. if err = stableCopy(encryptedDataDir, newBackupDir); nil != err {
  239. util.LogErrorf("copy encrypted data dir from [%s] to [%s] failed: %s", encryptedDataDir, newBackupDir, err)
  240. err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
  241. return
  242. }
  243. _, err = genCloudIndex(newBackupDir, map[string]bool{}, true)
  244. if nil != err {
  245. return
  246. }
  247. conf := map[string]interface{}{"updated": time.Now().UnixMilli()}
  248. data, err = gulu.JSON.MarshalJSON(conf)
  249. if nil != err {
  250. util.LogErrorf("marshal backup conf.json failed: %s", err)
  251. } else {
  252. confPath := filepath.Join(newBackupDir, "conf.json")
  253. if err = os.WriteFile(confPath, data, 0644); nil != err {
  254. util.LogErrorf("write backup conf.json [%s] failed: %s", confPath, err)
  255. }
  256. }
  257. oldBackupDir := Conf.Backup.GetSaveDir() + ".old"
  258. os.RemoveAll(oldBackupDir)
  259. backupDir := Conf.Backup.GetSaveDir()
  260. if gulu.File.IsExist(backupDir) {
  261. if err = os.Rename(backupDir, oldBackupDir); nil != err {
  262. util.LogErrorf("rename backup dir from [%s] to [%s] failed: %s", backupDir, oldBackupDir, err)
  263. err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
  264. return
  265. }
  266. }
  267. if err = os.Rename(newBackupDir, backupDir); nil != err {
  268. util.LogErrorf("rename backup dir from [%s] to [%s] failed: %s", newBackupDir, backupDir, err)
  269. err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
  270. return
  271. }
  272. os.RemoveAll(oldBackupDir)
  273. elapsed := time.Now().Sub(start).Seconds()
  274. size, _ := util.SizeOfDirectory(backupDir, false)
  275. sizeStr := humanize.Bytes(uint64(size))
  276. util.LogInfof("created backup [size=%s] in [%.2fs]", sizeStr, elapsed)
  277. util.PushEndlessProgress(Conf.Language(21))
  278. time.Sleep(2 * time.Second)
  279. return
  280. }
  281. func DownloadBackup() (err error) {
  282. // 使用路径映射文件进行解密验证 https://github.com/siyuan-note/siyuan/issues/3789
  283. var tmpFetchedFiles int
  284. var tmpTransferSize uint64
  285. err = ossDownload0(util.TempDir+"/backup", "backup", "/"+pathJSON, &tmpFetchedFiles, &tmpTransferSize, false)
  286. if nil != err {
  287. return
  288. }
  289. data, err := os.ReadFile(filepath.Join(util.TempDir, "/backup/"+pathJSON))
  290. if nil != err {
  291. return
  292. }
  293. passwdData, _ := hex.DecodeString(string(util.AESDecrypt(Conf.E2EEPasswd)))
  294. passwd := string(passwdData)
  295. data, err = encryption.AESGCMDecryptBinBytes(data, passwd)
  296. if nil != err {
  297. err = errors.New(Conf.Language(28))
  298. return
  299. }
  300. localDirPath := Conf.Backup.GetSaveDir()
  301. util.PushEndlessProgress(Conf.Language(68))
  302. start := time.Now()
  303. fetchedFilesCount, transferSize, _, err := ossDownload(localDirPath, "backup", false)
  304. if nil == err {
  305. elapsed := time.Now().Sub(start).Seconds()
  306. util.LogInfof("downloaded backup [fetchedFiles=%d, transferSize=%s] in [%.2fs]", fetchedFilesCount, humanize.Bytes(transferSize), elapsed)
  307. util.PushEndlessProgress(Conf.Language(69))
  308. }
  309. return
  310. }
  311. func UploadBackup() (err error) {
  312. defer util.ClearPushProgress(100)
  313. if err = checkUploadBackup(); nil != err {
  314. return
  315. }
  316. localDirPath := Conf.Backup.GetSaveDir()
  317. util.PushEndlessProgress(Conf.Language(61))
  318. util.LogInfof("uploading backup...")
  319. start := time.Now()
  320. wroteFiles, transferSize, err := ossUpload(true, localDirPath, "backup", "not exist", false)
  321. if nil == err {
  322. elapsed := time.Now().Sub(start).Seconds()
  323. util.LogInfof("uploaded backup [wroteFiles=%d, transferSize=%s] in [%.2fs]", wroteFiles, humanize.Bytes(transferSize), elapsed)
  324. util.PushEndlessProgress(Conf.Language(41))
  325. time.Sleep(2 * time.Second)
  326. return
  327. }
  328. err = errors.New(formatErrorMsg(err))
  329. return
  330. }
  331. var pathJSON = fmt.Sprintf("%x", md5.Sum([]byte("paths.json"))) // 6952277a5a37c17aa6a7c6d86cd507b1
  332. func encryptDataDir(passwd string) (encryptedDataDir string, err error) {
  333. encryptedDataDir = filepath.Join(util.TempDir, "incremental", "backup-encrypt")
  334. if err = os.RemoveAll(encryptedDataDir); nil != err {
  335. return
  336. }
  337. if err = os.MkdirAll(encryptedDataDir, 0755); nil != err {
  338. return
  339. }
  340. ctime := map[string]time.Time{}
  341. metaJSON := map[string]string{}
  342. filepath.Walk(util.DataDir, func(path string, info fs.FileInfo, _ error) error {
  343. if util.DataDir == path {
  344. return nil
  345. }
  346. if isCloudSkipFile(path, info) {
  347. if info.IsDir() {
  348. return filepath.SkipDir
  349. }
  350. return nil
  351. }
  352. plainP := strings.TrimPrefix(path, util.DataDir+string(os.PathSeparator))
  353. p := plainP
  354. parts := strings.Split(p, string(os.PathSeparator))
  355. buf := bytes.Buffer{}
  356. for i, part := range parts {
  357. buf.WriteString(fmt.Sprintf("%x", sha256.Sum256([]byte(part)))[:7])
  358. if i < len(parts)-1 {
  359. buf.WriteString(string(os.PathSeparator))
  360. }
  361. }
  362. p = buf.String()
  363. metaJSON[filepath.ToSlash(p)] = filepath.ToSlash(plainP)
  364. p = encryptedDataDir + string(os.PathSeparator) + p
  365. if info.IsDir() {
  366. if err = os.MkdirAll(p, 0755); nil != err {
  367. return io.EOF
  368. }
  369. if fi, err0 := os.Stat(path); nil == err0 {
  370. ctime[p] = fi.ModTime()
  371. }
  372. } else {
  373. if err = os.MkdirAll(filepath.Dir(p), 0755); nil != err {
  374. return io.EOF
  375. }
  376. f, err0 := os.Create(p)
  377. if nil != err0 {
  378. util.LogErrorf("create file [%s] failed: %s", p, err0)
  379. err = err0
  380. return io.EOF
  381. }
  382. data, err0 := os.ReadFile(path)
  383. if nil != err0 {
  384. util.LogErrorf("read file [%s] failed: %s", path, err0)
  385. err = err0
  386. return io.EOF
  387. }
  388. data, err0 = encryption.AESGCMEncryptBinBytes(data, passwd)
  389. if nil != err0 {
  390. util.LogErrorf("encrypt file [%s] failed: %s", path, err0)
  391. err = errors.New("encrypt file failed")
  392. return io.EOF
  393. }
  394. if _, err0 = f.Write(data); nil != err0 {
  395. util.LogErrorf("write file [%s] failed: %s", p, err0)
  396. err = err0
  397. return io.EOF
  398. }
  399. if err0 = f.Close(); nil != err0 {
  400. util.LogErrorf("close file [%s] failed: %s", p, err0)
  401. err = err0
  402. return io.EOF
  403. }
  404. fi, err0 := os.Stat(path)
  405. if nil != err0 {
  406. util.LogErrorf("stat file [%s] failed: %s", path, err0)
  407. err = err0
  408. return io.EOF
  409. }
  410. ctime[p] = fi.ModTime()
  411. }
  412. return nil
  413. })
  414. if nil != err {
  415. return
  416. }
  417. for p, t := range ctime {
  418. if err = os.Chtimes(p, t, t); nil != err {
  419. return
  420. }
  421. }
  422. // 检查文件是否全部已经编入索引
  423. err = filepath.Walk(encryptedDataDir, func(path string, info fs.FileInfo, _ error) error {
  424. if encryptedDataDir == path {
  425. return nil
  426. }
  427. path = strings.TrimPrefix(path, encryptedDataDir+string(os.PathSeparator))
  428. path = filepath.ToSlash(path)
  429. if _, ok := metaJSON[path]; !ok {
  430. util.LogErrorf("not found backup path in meta [%s]", path)
  431. return errors.New(Conf.Language(27))
  432. }
  433. return nil
  434. })
  435. if nil != err {
  436. return
  437. }
  438. data, err := gulu.JSON.MarshalJSON(metaJSON)
  439. if nil != err {
  440. return
  441. }
  442. data, err = encryption.AESGCMEncryptBinBytes(data, passwd)
  443. if nil != err {
  444. return "", errors.New("encrypt file failed")
  445. }
  446. meta := filepath.Join(encryptedDataDir, pathJSON)
  447. if err = gulu.File.WriteFileSafer(meta, data, 0644); nil != err {
  448. return
  449. }
  450. return
  451. }
  452. func decryptDataDir(passwd string) (decryptedDataDir string, err error) {
  453. decryptedDataDir = filepath.Join(util.TempDir, "incremental", "backup-decrypt")
  454. if err = os.RemoveAll(decryptedDataDir); nil != err {
  455. return
  456. }
  457. backupDir := Conf.Backup.GetSaveDir()
  458. meta := filepath.Join(util.TempDir, "backup", pathJSON)
  459. data, err := os.ReadFile(meta)
  460. if nil != err {
  461. return
  462. }
  463. data, err = encryption.AESGCMDecryptBinBytes(data, passwd)
  464. if nil != err {
  465. return "", errors.New(Conf.Language(40))
  466. }
  467. metaJSON := map[string]string{}
  468. if err = gulu.JSON.UnmarshalJSON(data, &metaJSON); nil != err {
  469. return
  470. }
  471. index := map[string]*CloudIndex{}
  472. data, err = os.ReadFile(filepath.Join(backupDir, "index.json"))
  473. if nil != err {
  474. return
  475. }
  476. if err = gulu.JSON.UnmarshalJSON(data, &index); nil != err {
  477. return
  478. }
  479. err = filepath.Walk(backupDir, func(path string, info fs.FileInfo, _ error) error {
  480. if backupDir == path || pathJSON == info.Name() || strings.HasSuffix(info.Name(), ".json") {
  481. return nil
  482. }
  483. encryptedP := strings.TrimPrefix(path, backupDir+string(os.PathSeparator))
  484. encryptedP = filepath.ToSlash(encryptedP)
  485. decryptedP := metaJSON[encryptedP]
  486. if "" == decryptedP {
  487. if gulu.File.IsDir(path) {
  488. return filepath.SkipDir
  489. }
  490. return nil
  491. }
  492. plainP := filepath.Join(decryptedDataDir, decryptedP)
  493. plainP = filepath.FromSlash(plainP)
  494. if info.IsDir() {
  495. if err = os.MkdirAll(plainP, 0755); nil != err {
  496. return io.EOF
  497. }
  498. } else {
  499. if err = os.MkdirAll(filepath.Dir(plainP), 0755); nil != err {
  500. return io.EOF
  501. }
  502. var err0 error
  503. data, err0 = os.ReadFile(path)
  504. if nil != err0 {
  505. util.LogErrorf("read file [%s] failed: %s", path, err0)
  506. err = err0
  507. return io.EOF
  508. }
  509. data, err0 = encryption.AESGCMDecryptBinBytes(data, passwd)
  510. if nil != err0 {
  511. util.LogErrorf("decrypt file [%s] failed: %s", path, err0)
  512. err = errors.New(Conf.Language(40))
  513. return io.EOF
  514. }
  515. if err0 = os.WriteFile(plainP, data, 0644); nil != err0 {
  516. util.LogErrorf("write file [%s] failed: %s", plainP, err0)
  517. err = err0
  518. return io.EOF
  519. }
  520. var modTime int64
  521. idx := index["/"+encryptedP]
  522. if nil == idx {
  523. util.LogErrorf("index file [%s] not found", encryptedP)
  524. modTime = info.ModTime().Unix()
  525. } else {
  526. modTime = idx.Updated
  527. }
  528. if err0 = os.Chtimes(plainP, time.Unix(modTime, 0), time.Unix(modTime, 0)); nil != err0 {
  529. util.LogErrorf("change file [%s] time failed: %s", plainP, err0)
  530. }
  531. }
  532. return nil
  533. })
  534. return
  535. }