versionBackup.go 18 KB

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