versionBackup.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. package hybridBackup
  2. import (
  3. "errors"
  4. "log"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "time"
  9. "imuslab.com/arozos/mod/disk/diskcapacity/dftool"
  10. "imuslab.com/arozos/mod/filesystem/hidden"
  11. )
  12. /*
  13. VersionBackup.go
  14. This scirpt file backup the data in the system nightly and create a restore point
  15. for the day just like BRTFS
  16. */
  17. func executeVersionBackup(backupConfig *BackupTask) (string, error) {
  18. //Check if the backup parent root is identical / within backup disk
  19. parentRootAbs, err := filepath.Abs(backupConfig.ParentPath)
  20. if err != nil {
  21. backupConfig.PanicStopped = true
  22. return "", errors.New("Unable to resolve parent disk path")
  23. }
  24. backupRootAbs, err := filepath.Abs(filepath.Join(backupConfig.DiskPath, "/version/"))
  25. if err != nil {
  26. backupConfig.PanicStopped = true
  27. return "", errors.New("Unable to resolve backup disk path")
  28. }
  29. if len(parentRootAbs) >= len(backupRootAbs) {
  30. if parentRootAbs[:len(backupRootAbs)] == backupRootAbs {
  31. //parent root is within backup root. Raise configuration error
  32. log.Println("[HyperBackup] Invalid backup cycle: Parent drive is located inside backup drive")
  33. backupConfig.PanicStopped = true
  34. return "", errors.New("Configuration Error. Skipping backup cycle.")
  35. }
  36. }
  37. backupConfig.PanicStopped = false
  38. todayFolderName := time.Now().Format("2006-01-02")
  39. lastSnapshotTime := int64(0)
  40. previousSnapshotExists := true
  41. previousSnapshotName, err := getPreviousSnapshotName(backupConfig, todayFolderName)
  42. if err != nil {
  43. previousSnapshotExists = false
  44. }
  45. snapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName)
  46. previousSnapshotLocation := ""
  47. var previousSnapshotMap *LinkFileMap
  48. if previousSnapshotExists {
  49. previousSnapshotLocation = filepath.Join(backupConfig.DiskPath, "/version/", previousSnapshotName)
  50. previousSnapshotMap, _ = readLinkFile(previousSnapshotLocation)
  51. lastSnapshotTime = lastModTime(previousSnapshotLocation)
  52. }
  53. //Create today folder if not exist
  54. if !fileExists(snapshotLocation) {
  55. os.MkdirAll(snapshotLocation, 0755)
  56. }
  57. //Read the previous snapshot datalink into a LinkFileMap and use binary search for higher performance
  58. /*
  59. Run a three pass compare logic between
  60. 1. source disk and new backup disk to check any new / modified files (created today)
  61. 2. yesterday backup and today backup to check any deleted files (created before, deleted today)
  62. 3. file in today backup disk no longer in the current source disk (created today, deleted today)
  63. */
  64. copiedFileList := []string{}
  65. linkedFileList := map[string]string{}
  66. deletedFileList := map[string]string{}
  67. //First pass: Check if there are any updated file from source and backup it to backup drive
  68. log.Println("[HybridBackup] Snapshot Stage 1 - Started " + backupConfig.JobName)
  69. rootAbs, _ := filepath.Abs(backupConfig.ParentPath)
  70. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  71. err = fastWalk(parentRootAbs, func(filename string) error {
  72. if filepath.Ext(filename) == ".db" || filepath.Ext(filename) == ".lock" {
  73. //Reserved filename, skipping
  74. return nil
  75. }
  76. if filepath.Ext(filename) == ".datalink" {
  77. //Reserved filename, skipping
  78. return nil
  79. }
  80. isHiddenFile, _ := hidden.IsHidden(filename, true)
  81. if isHiddenFile {
  82. //Do not backup hidden files
  83. return nil
  84. }
  85. //Get the target paste location
  86. fileAbs, _ := filepath.Abs(filename)
  87. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  88. relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
  89. fileBackupLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName, relPath)
  90. yesterdayBackupLocation := filepath.Join(previousSnapshotLocation, relPath)
  91. //Check if the file exists in previous snapshot folder
  92. if !fileExists(yesterdayBackupLocation) {
  93. //Not exists in snapshot folder. Search the link file
  94. //fmt.Println("File not in last snapshot", yesterdayBackupLocation, previousSnapshotLocation, relPath)
  95. fileFoundInSnapshotLinkFile, nameOfSnapshot := previousSnapshotMap.fileExists(relPath)
  96. if fileFoundInSnapshotLinkFile {
  97. //File found in the snapshot link file. Compare the one in snapshot
  98. linkedSnapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", nameOfSnapshot)
  99. linkedSnapshotOriginalFile := filepath.Join(linkedSnapshotLocation, relPath)
  100. if fileExists(linkedSnapshotOriginalFile) {
  101. //Linked file exists. Check for changes
  102. if lastModTime(fileAbs) > lastModTime(linkedSnapshotLocation) {
  103. //This has changed recently. Match their hash to see if it is identical
  104. fileHashMatch, err := fileHashIdentical(fileAbs, linkedSnapshotOriginalFile)
  105. if err != nil {
  106. return nil
  107. }
  108. if fileHashMatch {
  109. //append this record to this snapshot linkdata file
  110. linkedFileList[relPath] = nameOfSnapshot
  111. } else {
  112. //File hash mismatch. Do file copy to renew data
  113. err = copyFileToBackupLocation(backupConfig, filename, fileBackupLocation)
  114. if err != nil {
  115. return err
  116. }
  117. copiedFileList = append(copiedFileList, fileBackupLocation)
  118. }
  119. } else {
  120. //It hasn't been changed since last snapshot
  121. linkedFileList[relPath] = nameOfSnapshot
  122. }
  123. } else {
  124. //Invalid snapshot linkage. Assume new and do copy
  125. log.Println("[HybridBackup] Link lost. Cloning source file to snapshot.")
  126. err = copyFileToBackupLocation(backupConfig, filename, fileBackupLocation)
  127. if err != nil {
  128. return err
  129. }
  130. copiedFileList = append(copiedFileList, fileBackupLocation)
  131. }
  132. } else {
  133. //This file is not in snapshot link file.
  134. //This is new file. Copy it to backup
  135. err = copyFileToBackupLocation(backupConfig, filename, fileBackupLocation)
  136. if err != nil {
  137. return err
  138. }
  139. copiedFileList = append(copiedFileList, fileBackupLocation)
  140. }
  141. } else if fileExists(yesterdayBackupLocation) {
  142. //The file exists in the last snapshot
  143. if lastModTime(fileAbs) > lastSnapshotTime {
  144. //Check if their hash is the same. If no, update it
  145. fileHashMatch, err := fileHashIdentical(fileAbs, yesterdayBackupLocation)
  146. if err != nil {
  147. return nil
  148. }
  149. if !fileHashMatch {
  150. //Hash mismatch. Overwrite the file
  151. if !fileExists(filepath.Dir(fileBackupLocation)) {
  152. os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
  153. }
  154. err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
  155. if err != nil {
  156. log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  157. } else {
  158. //No problem. Add this filepath into the list
  159. copiedFileList = append(copiedFileList, fileBackupLocation)
  160. }
  161. } else {
  162. //Create a link file for this relative path
  163. linkedFileList[relPath] = previousSnapshotName
  164. }
  165. } else {
  166. //Not modified
  167. linkedFileList[relPath] = previousSnapshotName
  168. }
  169. } else {
  170. //Default case
  171. lastModTime := lastModTime(fileAbs)
  172. if lastModTime > backupConfig.LastCycleTime {
  173. //Check if hash the same
  174. srcHash, err := getFileHash(fileAbs)
  175. if err != nil {
  176. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  177. return nil
  178. }
  179. targetHash, err := getFileHash(fileBackupLocation)
  180. if err != nil {
  181. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(fileBackupLocation), err.Error(), " Skipping.")
  182. return nil
  183. }
  184. if srcHash != targetHash {
  185. //Hash mismatch. Overwrite the file
  186. if !fileExists(filepath.Dir(fileBackupLocation)) {
  187. os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
  188. }
  189. err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
  190. if err != nil {
  191. log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  192. } else {
  193. //No problem. Add this filepath into the list
  194. copiedFileList = append(copiedFileList, fileBackupLocation)
  195. }
  196. }
  197. }
  198. }
  199. return nil
  200. })
  201. if err != nil {
  202. //Copy error. Mostly because of disk fulled
  203. return err.Error(), err
  204. }
  205. //2nd pass: Check if there are anything exists in the previous backup but no longer exists in the source now
  206. //For case where the file is backed up in previous snapshot but now the file has been removed
  207. log.Println("[HybridBackup] Snapshot Stage 2 - Started " + backupConfig.JobName)
  208. if previousSnapshotExists {
  209. fastWalk(previousSnapshotLocation, func(filename string) error {
  210. if filepath.Ext(filename) == ".datalink" {
  211. //System reserved file. Skip this
  212. return nil
  213. }
  214. //Get the target paste location
  215. rootAbs, _ := filepath.Abs(previousSnapshotLocation)
  216. fileAbs, _ := filepath.Abs(filename)
  217. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  218. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  219. relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
  220. sourcAssumeLocation := filepath.Join(parentRootAbs, relPath)
  221. //todaySnapshotLocation := filepath.Join(snapshotLocation, relPath)
  222. if !fileExists(sourcAssumeLocation) {
  223. //File exists in yesterday snapshot but not in the current source
  224. //Assume it has been deleted, create a dummy indicator file
  225. deletedFileList[relPath] = todayFolderName
  226. }
  227. return nil
  228. })
  229. //Check for deleting of unchanged file as well
  230. for relPath, _ := range previousSnapshotMap.UnchangedFile {
  231. sourcAssumeLocation := filepath.Join(parentRootAbs, relPath)
  232. if !fileExists(sourcAssumeLocation) {
  233. //The source file no longer exists
  234. deletedFileList[relPath] = todayFolderName
  235. }
  236. }
  237. }
  238. //3rd pass: Check if there are anything in today backup drive that didn't exists in the source drive
  239. //For cases where the backup is applied to overwrite an eariler backup of the same day
  240. log.Println("[HybridBackup] Snapshot Stage 3 - Started " + backupConfig.JobName)
  241. fastWalk(snapshotLocation, func(filename string) error {
  242. if filepath.Ext(filename) == ".db" || filepath.Ext(filename) == ".lock" {
  243. //Reserved filename, skipping
  244. return nil
  245. }
  246. if filepath.Ext(filename) == ".datalink" {
  247. //Deleted file marker. Skip this
  248. return nil
  249. }
  250. //Get the target paste location
  251. rootAbs, _ := filepath.Abs(snapshotLocation)
  252. fileAbs, _ := filepath.Abs(filename)
  253. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  254. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  255. relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
  256. sourceAssumedLocation := filepath.Join(parentRootAbs, relPath)
  257. if !fileExists(sourceAssumedLocation) {
  258. //File removed from the source. Delete it from backup as well
  259. os.Remove(filename)
  260. }
  261. return nil
  262. })
  263. //Generate linkfile for this snapshot
  264. log.Println("[HybridBackup] Snapshot - Generating Linker File")
  265. generateLinkFile(snapshotLocation, LinkFileMap{
  266. UnchangedFile: linkedFileList,
  267. DeletedFiles: deletedFileList,
  268. })
  269. if err != nil {
  270. log.Println("[HybridBackup] Error! ", err.Error())
  271. return "", err
  272. }
  273. return "", nil
  274. }
  275. //Return the previous snapshot for the currentSnspashot
  276. func getPreviousSnapshotName(backupConfig *BackupTask, currentSnapshotName string) (string, error) {
  277. //Resolve the backup root folder
  278. backupRootAbs, err := filepath.Abs(filepath.Join(backupConfig.DiskPath, "/version/"))
  279. if err != nil {
  280. return "", errors.New("Unable to get the previous snapshot directory")
  281. }
  282. //Get the snapshot list and extract the snapshot date from foldername
  283. existingSnapshots := []string{}
  284. files, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(backupRootAbs)) + "/*")
  285. for _, file := range files {
  286. if isDir(file) && fileExists(filepath.Join(file, "snapshot.datalink")) {
  287. existingSnapshots = append(existingSnapshots, filepath.Base(file))
  288. }
  289. }
  290. if len(existingSnapshots) == 0 {
  291. return "", errors.New("No snapshot found")
  292. }
  293. //Check if the current snapshot exists, if not, return the latest one
  294. previousSnapshotName := ""
  295. if fileExists(filepath.Join(backupRootAbs, currentSnapshotName)) {
  296. //Current snapshot exists. Find the one just above it
  297. lastSnapshotName := existingSnapshots[0]
  298. for _, snapshotName := range existingSnapshots {
  299. if snapshotName == currentSnapshotName {
  300. //This is the correct snapshot name. Get the last one as previous snapshot
  301. previousSnapshotName = lastSnapshotName
  302. } else {
  303. lastSnapshotName = snapshotName
  304. }
  305. }
  306. } else {
  307. //Current snapshot not exists. Use the last item in snapshots list
  308. previousSnapshotName = existingSnapshots[len(existingSnapshots)-1]
  309. }
  310. return previousSnapshotName, nil
  311. }
  312. func copyFileToBackupLocation(task *BackupTask, filename string, fileBackupLocation string) error {
  313. //Make dir the target dir if not exists
  314. if !fileExists(filepath.Dir(fileBackupLocation)) {
  315. os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
  316. }
  317. //Check if the target disk can fit the new file
  318. capinfo, err := dftool.GetCapacityInfoFromPath(filepath.Dir(fileBackupLocation))
  319. if err == nil {
  320. //Capacity info return normally. Estimate if the file will fit
  321. srcSize := fileSize(filename)
  322. diskSpace := capinfo.Avilable
  323. if diskSpace < srcSize {
  324. //Merge older snapshots. Maxium merging is 1 week
  325. for i := 0; i < 6; i++ {
  326. //Merge the oldest snapshot
  327. err = mergeOldestSnapshots(task)
  328. if err != nil {
  329. log.Println("[HybridBackup] " + err.Error())
  330. return errors.New("No space left on device")
  331. }
  332. //Check if there are enough space again
  333. capinfo, err := dftool.GetCapacityInfoFromPath(filepath.Dir(fileBackupLocation))
  334. if err != nil {
  335. log.Println("[HybridBackup] " + err.Error())
  336. return errors.New("No space left on device")
  337. }
  338. srcSize = fileSize(filename)
  339. diskSpace = capinfo.Avilable
  340. if diskSpace > srcSize {
  341. //Space enough. Break out
  342. break
  343. }
  344. }
  345. log.Println("[HybridBackup] Error: No space left on device! Require ", srcSize, "bytes but only ", diskSpace, " bytes left")
  346. return errors.New("No space left on device")
  347. }
  348. }
  349. err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
  350. if err != nil {
  351. log.Println("[HybridBackup] Failed to copy file: ", filepath.Base(filename)+". "+err.Error())
  352. return err
  353. }
  354. return nil
  355. }
  356. func fileHashIdentical(srcFile string, matchingFile string) (bool, error) {
  357. srcHash, err := getFileHash(srcFile)
  358. if err != nil {
  359. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(srcFile), err.Error(), " Skipping.")
  360. return false, nil
  361. }
  362. targetHash, err := getFileHash(matchingFile)
  363. if err != nil {
  364. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(matchingFile), err.Error(), " Skipping.")
  365. return false, nil
  366. }
  367. if srcHash != targetHash {
  368. return false, nil
  369. } else {
  370. return true, nil
  371. }
  372. }
  373. //List all restorable for version backup
  374. func listVersionRestorables(task *BackupTask) ([]*RestorableFile, error) {
  375. //Check if mode is set correctly
  376. restorableFiles := []*RestorableFile{}
  377. if task.Mode != "version" {
  378. return restorableFiles, errors.New("This task mode is not supported by this list function")
  379. }
  380. //List directories of the restorable snapshots
  381. snapshotPath := filepath.ToSlash(filepath.Clean(filepath.Join(task.DiskPath, "/version/")))
  382. filesInSnapshotFolder, err := filepath.Glob(snapshotPath + "/*")
  383. if err != nil {
  384. return restorableFiles, err
  385. }
  386. //Check if the foler is actually a snapshot
  387. avaibleSnapshot := []string{}
  388. for _, fileObject := range filesInSnapshotFolder {
  389. possibleSnapshotDatalinkFile := filepath.Join(fileObject, "snapshot.datalink")
  390. if fileExists(possibleSnapshotDatalinkFile) {
  391. //This is a snapshot
  392. avaibleSnapshot = append(avaibleSnapshot, fileObject)
  393. }
  394. }
  395. //Build restorabe file struct for returning
  396. for _, snapshot := range avaibleSnapshot {
  397. thisFile := RestorableFile{
  398. Filename: filepath.Base(snapshot),
  399. IsHidden: false,
  400. Filesize: 0,
  401. RelpathOnDisk: filepath.Base(snapshot),
  402. RestorePoint: task.ParentUID,
  403. BackupDiskUID: task.DiskUID,
  404. RemainingTime: -1,
  405. DeleteTime: -1,
  406. IsSnapshot: true,
  407. }
  408. restorableFiles = append(restorableFiles, &thisFile)
  409. }
  410. return restorableFiles, nil
  411. }
  412. //Check if a file in snapshot relPath (start with /) belongs to a user
  413. func snapshotFileBelongsToUser(relPath string, username string) bool {
  414. relPath = filepath.ToSlash(filepath.Clean(relPath))
  415. userPath := "/users/" + username + "/"
  416. if len(relPath) > len(userPath) && relPath[:len(userPath)] == userPath {
  417. return true
  418. } else {
  419. return false
  420. }
  421. }
  422. //This function generate and return a snapshot summary. For public drive, leave username as nil
  423. func (task *BackupTask) GenerateSnapshotSummary(snapshotName string, username *string) (*SnapshotSummary, error) {
  424. //Check if the task is version
  425. if task.Mode != "version" {
  426. return nil, errors.New("Invalid backup mode. This function only support snapshot mode backup task.")
  427. }
  428. userSumamryMode := false
  429. targetUserName := ""
  430. if username != nil {
  431. targetUserName = *username
  432. userSumamryMode = true
  433. }
  434. //Check if the snapshot folder exists
  435. targetSnapshotFolder := filepath.Join(task.DiskPath, "/version/", snapshotName)
  436. if !fileExists(targetSnapshotFolder) {
  437. return nil, errors.New("Snapshot not exists")
  438. }
  439. if !fileExists(filepath.Join(targetSnapshotFolder, "snapshot.datalink")) {
  440. return nil, errors.New("Snapshot datalink file not exists")
  441. }
  442. summary := SnapshotSummary{
  443. ChangedFiles: map[string]string{},
  444. UnchangedFiles: map[string]string{},
  445. DeletedFiles: map[string]string{},
  446. }
  447. fastWalk(targetSnapshotFolder, func(filename string) error {
  448. if filepath.Base(filename) == "snapshot.datalink" {
  449. //Exceptional
  450. return nil
  451. }
  452. relPath, err := filepath.Rel(targetSnapshotFolder, filename)
  453. if err != nil {
  454. return err
  455. }
  456. //Check if user mode, check if folder owned by them
  457. if userSumamryMode == true {
  458. if snapshotFileBelongsToUser("/"+filepath.ToSlash(relPath), targetUserName) {
  459. summary.ChangedFiles["/"+filepath.ToSlash(relPath)] = snapshotName
  460. }
  461. } else {
  462. summary.ChangedFiles["/"+filepath.ToSlash(relPath)] = snapshotName
  463. }
  464. return nil
  465. })
  466. //Generate the summary
  467. linkFileMap, err := readLinkFile(targetSnapshotFolder)
  468. if err != nil {
  469. return nil, err
  470. }
  471. //Move the file map into result
  472. if userSumamryMode {
  473. //Only show the files that belongs to this user
  474. for relPath, linkTarget := range linkFileMap.UnchangedFile {
  475. if snapshotFileBelongsToUser(filepath.ToSlash(relPath), targetUserName) {
  476. summary.UnchangedFiles[filepath.ToSlash(relPath)] = linkTarget
  477. }
  478. }
  479. for relPath, linkTarget := range linkFileMap.DeletedFiles {
  480. if snapshotFileBelongsToUser(filepath.ToSlash(relPath), targetUserName) {
  481. summary.DeletedFiles[filepath.ToSlash(relPath)] = linkTarget
  482. }
  483. }
  484. } else {
  485. //Show all files (public mode)
  486. summary.UnchangedFiles = linkFileMap.UnchangedFile
  487. summary.DeletedFiles = linkFileMap.DeletedFiles
  488. }
  489. return &summary, nil
  490. }