hybridBackup.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package hybridBackup
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "errors"
  6. "io"
  7. "log"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "time"
  12. )
  13. /*
  14. Hybrid Backup
  15. This module handle backup functions from the drive with Hieracchy labeled as "backup"
  16. Backup modes suport in this module currently consists of
  17. Denote P drive as parent drive and B drive as backup drive.
  18. 1. Basic (basic):
  19. - Any new file created in P will be copied to B within 1 minutes
  20. - Any file change will be copied to B within 30 minutes
  21. - Any file removed in P will be delete from backup if it is > 24 hours old
  22. 2. Nightly (nightly):
  23. - The whole P drive will be copied to N drive every night
  24. 3. Versioning (version)
  25. - A versioning system will be introduce to this backup drive
  26. - Just like the time machine
  27. Tips when developing this module
  28. - This is a sub-module of the current file system. Do not import from arozos file system module
  29. - If you need any function from the file system, copy and paste it in this module
  30. */
  31. type BackupConfig struct {
  32. JobName string //The name used by the scheduler for executing this config
  33. CycleCounter int64 //The number of backup executed in the background
  34. LastCycleTime int64 //The execution time of the last cycle
  35. DiskUID string //The UID of the target fsandlr
  36. DiskPath string //The mount point for the disk
  37. ParentUID string //Parent virtal disk UUID
  38. ParentPath string //Parent disk path
  39. DeleteFileMarkers map[string]int64 //Markers for those files delete pending, [file path (relative)] time
  40. Mode string //Backup mode
  41. }
  42. func executeBackup(backupConfig *BackupConfig, deepBackup bool) (string, error) {
  43. copiedFileList := []string{}
  44. rootPath := filepath.ToSlash(filepath.Clean(backupConfig.ParentPath))
  45. //Add file cycles
  46. fastWalk(rootPath, func(filename string) error {
  47. if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
  48. //Reserved filename, skipping
  49. return nil
  50. }
  51. //Get the target paste location
  52. rootAbs, _ := filepath.Abs(rootPath)
  53. fileAbs, _ := filepath.Abs(filename)
  54. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  55. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  56. relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
  57. assumedTargetPosition := filepath.Join(backupConfig.DiskPath, relPath)
  58. if !deepBackup {
  59. //Shallow copy. Only do copy base on file exists or not
  60. //This is used to reduce the time for reading the file metatag
  61. if !fileExists(assumedTargetPosition) {
  62. //Target file not exists in backup disk. Make a copy
  63. if !fileExists(filepath.Dir(assumedTargetPosition)) {
  64. //Folder containing this file not exists. Create it
  65. os.MkdirAll(filepath.Dir(assumedTargetPosition), 0755)
  66. }
  67. //Copy the file to target
  68. err := BufferedLargeFileCopy(fileAbs, assumedTargetPosition, 1024)
  69. if err != nil {
  70. log.Println("*Hybrid Backup* Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  71. } else {
  72. //No problem. Add this filepath into the list
  73. copiedFileList = append(copiedFileList, assumedTargetPosition)
  74. }
  75. }
  76. } else {
  77. //Deep copy. Check and match the modtime of each file
  78. if !fileExists(assumedTargetPosition) {
  79. //Copy the file to target
  80. err := BufferedLargeFileCopy(fileAbs, assumedTargetPosition, 1024)
  81. if err != nil {
  82. log.Println("*Hybrid Backup* Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  83. return nil
  84. } else {
  85. //No problem. Add this filepath into the list
  86. copiedFileList = append(copiedFileList, assumedTargetPosition)
  87. }
  88. } else {
  89. //Target file already exists. Check if their hash matches
  90. srcHash, err := getFileHash(fileAbs)
  91. if err != nil {
  92. log.Println("*Hybrid Backup* Hash calculation failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  93. return nil
  94. }
  95. targetHash, err := getFileHash(assumedTargetPosition)
  96. if err != nil {
  97. log.Println("*Hybrid Backup* Hash calculation failed for file "+filepath.Base(assumedTargetPosition), err.Error(), " Skipping.")
  98. return nil
  99. }
  100. if srcHash != targetHash {
  101. log.Println("[Debug] Hash mismatch. Copying ", fileAbs)
  102. //This file has been recently changed. Copy it to new location
  103. err = BufferedLargeFileCopy(fileAbs, assumedTargetPosition, 1024)
  104. if err != nil {
  105. log.Println("*Hybrid Backup* Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  106. } else {
  107. //No problem. Add this filepath into the list
  108. copiedFileList = append(copiedFileList, assumedTargetPosition)
  109. }
  110. }
  111. }
  112. }
  113. ///Remove file cycle
  114. backupDriveRootPath := filepath.ToSlash(filepath.Clean(backupConfig.DiskPath))
  115. fastWalk(backupConfig.DiskPath, func(filename string) error {
  116. if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
  117. //Reserved filename, skipping
  118. return nil
  119. }
  120. //Get the target paste location
  121. rootAbs, _ := filepath.Abs(backupDriveRootPath)
  122. fileAbs, _ := filepath.Abs(filename)
  123. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  124. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  125. thisFileRel := filename[len(backupDriveRootPath):]
  126. originalFileOnDiskPath := filepath.ToSlash(filepath.Clean(filepath.Join(backupConfig.ParentPath, thisFileRel)))
  127. //Check if the taget file not exists and this file has been here for more than 24h
  128. if !fileExists(originalFileOnDiskPath) {
  129. //This file not exists. Check if it is in the delete file marker for more than 24 hours
  130. val, ok := backupConfig.DeleteFileMarkers[thisFileRel]
  131. if !ok {
  132. //This file is newly deleted. Push into the marker map
  133. backupConfig.DeleteFileMarkers[thisFileRel] = time.Now().Unix()
  134. log.Println("[Debug] Adding " + filename + " to delete marker")
  135. } else {
  136. //This file has been marked. Check if it is time to delete
  137. if time.Now().Unix()-val > 120 {
  138. log.Println("[Debug] Deleting " + filename)
  139. //Remove the backup file
  140. os.RemoveAll(filename)
  141. //Remove file from delete file markers
  142. delete(backupConfig.DeleteFileMarkers, thisFileRel)
  143. }
  144. }
  145. }
  146. return nil
  147. })
  148. return nil
  149. })
  150. return "", nil
  151. }
  152. //Main handler function for hybrid backup
  153. func HandleBackupProcess(backupConfig *BackupConfig) (string, error) {
  154. log.Println(">>>>>> [Debug] Running backup process: ", backupConfig)
  155. //Check if the target disk is writable and mounted
  156. if fileExists(filepath.Join(backupConfig.ParentPath, "aofs.db")) && fileExists(filepath.Join(backupConfig.ParentPath, "aofs.db.lock")) {
  157. //This parent filesystem is mounted
  158. } else {
  159. //File system not mounted even after 3 backup cycle. Terminate backup scheduler
  160. log.Println("*HybridBackup* Skipping backup cycle for " + backupConfig.ParentUID + ":/")
  161. return "Parent drive (" + backupConfig.ParentUID + ":/) not mounted", nil
  162. }
  163. //Check if the backup disk is mounted. If no, stop the scheulder
  164. if backupConfig.CycleCounter > 3 && !(fileExists(filepath.Join(backupConfig.DiskPath, "aofs.db")) && fileExists(filepath.Join(backupConfig.DiskPath, "aofs.db.lock"))) {
  165. log.Println("*HybridBackup* Backup schedule stopped for " + backupConfig.DiskUID + ":/")
  166. return "Backup drive (" + backupConfig.DiskUID + ":/) not mounted", errors.New("Backup File System Handler not mounted")
  167. }
  168. deepBackup := true //Default perform deep backup
  169. if backupConfig.Mode == "basic" {
  170. if backupConfig.CycleCounter%30 == 0 {
  171. //Perform deep backup, use walk function
  172. deepBackup = true
  173. } else {
  174. deepBackup = false
  175. }
  176. backupConfig.LastCycleTime = time.Now().Unix()
  177. return executeBackup(backupConfig, deepBackup)
  178. } else if backupConfig.Mode == "nightly" {
  179. if time.Now().Unix()-backupConfig.LastCycleTime >= 86400 {
  180. //24 hours from last backup. Execute deep backup now
  181. executeBackup(backupConfig, true)
  182. backupConfig.LastCycleTime = time.Now().Unix()
  183. }
  184. } else if backupConfig.Mode == "version" {
  185. //Do a versioning backup
  186. //WIP
  187. }
  188. //Add one to the cycle counter
  189. backupConfig.CycleCounter++
  190. //Return the log information
  191. return "", nil
  192. }
  193. //Get and return the file hash for a file
  194. func getFileHash(filename string) (string, error) {
  195. f, err := os.Open(filename)
  196. if err != nil {
  197. return "", err
  198. }
  199. defer f.Close()
  200. h := sha256.New()
  201. if _, err := io.Copy(h, f); err != nil {
  202. return "", err
  203. }
  204. return hex.EncodeToString(h.Sum(nil)), nil
  205. }