versionBackup.go 17 KB

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