versionBackup.go 18 KB

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