versionBackup.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package hybridBackup
  2. import (
  3. "errors"
  4. "log"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "time"
  9. )
  10. /*
  11. VersionBackup.go
  12. This scirpt file backup the data in the system nightly and create a restore point
  13. for the day just like BRTFS
  14. */
  15. func executeVersionBackup(backupConfig *BackupTask) (string, error) {
  16. //Check if the backup parent root is identical / within backup disk
  17. parentRootAbs, err := filepath.Abs(backupConfig.ParentPath)
  18. if err != nil {
  19. return "", errors.New("Unable to resolve parent disk path")
  20. }
  21. backupRootAbs, err := filepath.Abs(filepath.Join(backupConfig.DiskPath, "/version/"))
  22. if err != nil {
  23. return "", errors.New("Unable to resolve backup disk path")
  24. }
  25. if len(parentRootAbs) >= len(backupRootAbs) {
  26. if parentRootAbs[:len(backupRootAbs)] == backupRootAbs {
  27. //parent root is within backup root. Raise configuration error
  28. log.Println("*HyperBackup* Invalid backup cycle: Parent drive is located inside backup drive")
  29. return "", errors.New("Configuration Error. Skipping backup cycle.")
  30. }
  31. }
  32. todayFolderName := time.Now().Format("2006-01-02")
  33. previousSnapshotExists := true
  34. previousSnapshotName, err := getPreviousSnapshotName(backupConfig, todayFolderName)
  35. if err != nil {
  36. previousSnapshotExists = false
  37. }
  38. snapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName)
  39. previousSnapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", previousSnapshotName)
  40. //Create today folder if not exist
  41. if !fileExists(snapshotLocation) {
  42. os.MkdirAll(snapshotLocation, 0755)
  43. }
  44. //Read the previous snapshot datalink into a LinkFileMap and use binary search for higher performance
  45. previousSnapshotMap, _ := readLinkFile(previousSnapshotLocation)
  46. /*
  47. Run a three pass compare logic between
  48. 1. source disk and new backup disk to check any new / modified files (created today)
  49. 2. yesterday backup and today backup to check any deleted files (created before, deleted today)
  50. 3. file in today backup disk no longer in the current source disk (created today, deleted today)
  51. */
  52. copiedFileList := []string{}
  53. linkedFileList := map[string]string{}
  54. deletedFileList := map[string]string{}
  55. //First pass: Check if there are any updated file from source and backup it to backup drive
  56. fastWalk(parentRootAbs, func(filename string) error {
  57. if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
  58. //Reserved filename, skipping
  59. return nil
  60. }
  61. //Get the target paste location
  62. rootAbs, _ := filepath.Abs(backupConfig.ParentPath)
  63. fileAbs, _ := filepath.Abs(filename)
  64. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  65. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  66. relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
  67. fileBackupLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName, relPath)
  68. yesterdayBackupLocation := filepath.Join(previousSnapshotLocation, relPath)
  69. //Check if the file exists
  70. if !fileExists(yesterdayBackupLocation) {
  71. //This file not in last snapshot location.
  72. //Check if it is in previous snapshot map
  73. fileFoundInSnapshotLinkFile, nameOfSnapshot := previousSnapshotMap.fileExists(relPath)
  74. if fileFoundInSnapshotLinkFile {
  75. //File found in the snapshot link file. Compare the one in snapshot
  76. linkedSnapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", nameOfSnapshot)
  77. linkedSnapshotOriginalFile := filepath.Join(linkedSnapshotLocation, relPath)
  78. if fileExists(linkedSnapshotOriginalFile) {
  79. //Linked file exists. Compare hash
  80. fileHashMatch, err := fileHashIdentical(fileAbs, linkedSnapshotOriginalFile)
  81. if err != nil {
  82. return nil
  83. }
  84. if fileHashMatch {
  85. //append this record to this snapshot linkdata file
  86. linkedFileList[relPath] = nameOfSnapshot
  87. } else {
  88. //File hash mismatch. Do file copy to renew data
  89. copyFileToBackupLocation(filename, fileBackupLocation)
  90. copiedFileList = append(copiedFileList, fileBackupLocation)
  91. }
  92. } else {
  93. //Invalid snapshot linkage. Assume new and do copy
  94. log.Println("[HybridBackup] Link lost. Cloning source file to snapshot.")
  95. copyFileToBackupLocation(filename, fileBackupLocation)
  96. copiedFileList = append(copiedFileList, fileBackupLocation)
  97. }
  98. } else {
  99. //This file is not in snapshot link file.
  100. //This is new file. Copy it to backup
  101. copyFileToBackupLocation(filename, fileBackupLocation)
  102. copiedFileList = append(copiedFileList, fileBackupLocation)
  103. }
  104. } else if fileExists(yesterdayBackupLocation) {
  105. //The file exists in the last snapshot
  106. //Check if their hash is the same. If no, update it
  107. fileHashMatch, err := fileHashIdentical(fileAbs, yesterdayBackupLocation)
  108. if err != nil {
  109. return nil
  110. }
  111. if !fileHashMatch {
  112. //Hash mismatch. Overwrite the file
  113. if !fileExists(filepath.Dir(fileBackupLocation)) {
  114. os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
  115. }
  116. err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
  117. if err != nil {
  118. log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  119. } else {
  120. //No problem. Add this filepath into the list
  121. copiedFileList = append(copiedFileList, fileBackupLocation)
  122. }
  123. } else {
  124. //Create a link file for this relative path
  125. linkedFileList[relPath] = previousSnapshotName
  126. }
  127. } else {
  128. //Default case
  129. lastModTime := lastModTime(fileAbs)
  130. if lastModTime > backupConfig.LastCycleTime {
  131. //Check if hash the same
  132. srcHash, err := getFileHash(fileAbs)
  133. if err != nil {
  134. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  135. return nil
  136. }
  137. targetHash, err := getFileHash(fileBackupLocation)
  138. if err != nil {
  139. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(fileBackupLocation), err.Error(), " Skipping.")
  140. return nil
  141. }
  142. if srcHash != targetHash {
  143. //Hash mismatch. Overwrite the file
  144. if !fileExists(filepath.Dir(fileBackupLocation)) {
  145. os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
  146. }
  147. err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
  148. if err != nil {
  149. log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
  150. } else {
  151. //No problem. Add this filepath into the list
  152. copiedFileList = append(copiedFileList, fileBackupLocation)
  153. }
  154. }
  155. }
  156. }
  157. return nil
  158. })
  159. //2nd pass: Check if there are anything exists in the previous backup but no longer exists in the source now
  160. //For case where the file is backed up in previous snapshot but now the file has been removed
  161. if previousSnapshotExists {
  162. fastWalk(previousSnapshotLocation, func(filename string) error {
  163. if filepath.Base(filename) == "snapshot.datalink" {
  164. //System reserved file. Skip this
  165. return nil
  166. }
  167. //Get the target paste location
  168. rootAbs, _ := filepath.Abs(previousSnapshotLocation)
  169. fileAbs, _ := filepath.Abs(filename)
  170. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  171. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  172. relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
  173. sourcAssumeLocation := filepath.Join(parentRootAbs, relPath)
  174. //todaySnapshotLocation := filepath.Join(snapshotLocation, relPath)
  175. if !fileExists(sourcAssumeLocation) {
  176. //File exists in yesterday snapshot but not in the current source
  177. //Assume it has been deleted, create a dummy indicator file
  178. //ioutil.WriteFile(todaySnapshotLocation+".deleted", []byte(""), 0755)
  179. deletedFileList[relPath] = todayFolderName
  180. }
  181. return nil
  182. })
  183. //Check for deleting of unchanged file as well
  184. for relPath, _ := range previousSnapshotMap.UnchangedFile {
  185. sourcAssumeLocation := filepath.Join(parentRootAbs, relPath)
  186. if !fileExists(sourcAssumeLocation) {
  187. //The source file no longer exists
  188. deletedFileList[relPath] = todayFolderName
  189. }
  190. }
  191. }
  192. //3rd pass: Check if there are anything (except file with .deleted) in today backup drive that didn't exists in the source drive
  193. //For cases where the backup is applied to overwrite an eariler backup of the same day
  194. fastWalk(snapshotLocation, func(filename string) error {
  195. if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
  196. //Reserved filename, skipping
  197. return nil
  198. }
  199. if filepath.Ext(filename) == ".datalink" {
  200. //Deleted file marker. Skip this
  201. return nil
  202. }
  203. //Get the target paste location
  204. rootAbs, _ := filepath.Abs(snapshotLocation)
  205. fileAbs, _ := filepath.Abs(filename)
  206. rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
  207. fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
  208. relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
  209. sourceAssumedLocation := filepath.Join(parentRootAbs, relPath)
  210. if !fileExists(sourceAssumedLocation) {
  211. //File removed from the source. Delete it from backup as well
  212. os.Remove(filename)
  213. }
  214. return nil
  215. })
  216. //Generate linkfile for this snapshot
  217. generateLinkFile(snapshotLocation, LinkFileMap{
  218. UnchangedFile: linkedFileList,
  219. DeletedFiles: deletedFileList,
  220. })
  221. if err != nil {
  222. return "", err
  223. }
  224. return "", nil
  225. }
  226. //Return the previous snapshot for the currentSnspashot
  227. func getPreviousSnapshotName(backupConfig *BackupTask, currentSnapshotName string) (string, error) {
  228. //Resolve the backup root folder
  229. backupRootAbs, err := filepath.Abs(filepath.Join(backupConfig.DiskPath, "/version/"))
  230. if err != nil {
  231. return "", errors.New("Unable to get the previous snapshot directory")
  232. }
  233. //Get the snapshot list and extract the snapshot date from foldername
  234. existingSnapshots := []string{}
  235. files, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(backupRootAbs)) + "/*")
  236. for _, file := range files {
  237. if isDir(file) && fileExists(filepath.Join(file, "snapshot.datalink")) {
  238. existingSnapshots = append(existingSnapshots, filepath.Base(file))
  239. }
  240. }
  241. if len(existingSnapshots) == 0 {
  242. return "", errors.New("No snapshot found")
  243. }
  244. //Check if the current snapshot exists, if not, return the latest one
  245. previousSnapshotName := ""
  246. if fileExists(filepath.Join(backupRootAbs, currentSnapshotName)) {
  247. //Current snapshot exists. Find the one just above it
  248. lastSnapshotName := existingSnapshots[0]
  249. for _, snapshotName := range existingSnapshots {
  250. if snapshotName == currentSnapshotName {
  251. //This is the correct snapshot name. Get the last one as previous snapshot
  252. previousSnapshotName = lastSnapshotName
  253. } else {
  254. lastSnapshotName = snapshotName
  255. }
  256. }
  257. } else {
  258. //Current snapshot not exists. Use the last item in snapshots list
  259. previousSnapshotName = existingSnapshots[len(existingSnapshots)-1]
  260. }
  261. return previousSnapshotName, nil
  262. }
  263. func copyFileToBackupLocation(filename string, fileBackupLocation string) error {
  264. if !fileExists(filepath.Dir(fileBackupLocation)) {
  265. os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
  266. }
  267. err := BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
  268. if err != nil {
  269. log.Println("[HybridBackup] Failed to copy file: ", filepath.Base(filename)+". "+err.Error())
  270. return err
  271. }
  272. return nil
  273. }
  274. func fileHashIdentical(srcFile string, matchingFile string) (bool, error) {
  275. srcHash, err := getFileHash(srcFile)
  276. if err != nil {
  277. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(srcFile), err.Error(), " Skipping.")
  278. return false, nil
  279. }
  280. targetHash, err := getFileHash(matchingFile)
  281. if err != nil {
  282. log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(matchingFile), err.Error(), " Skipping.")
  283. return false, nil
  284. }
  285. if srcHash != targetHash {
  286. return false, nil
  287. } else {
  288. return true, nil
  289. }
  290. }
  291. //List all restorable for version backup
  292. func listVersionRestorables(task *BackupTask) ([]*RestorableFile, error) {
  293. //Check if mode is set correctly
  294. restorableFiles := []*RestorableFile{}
  295. if task.Mode != "version" {
  296. return restorableFiles, errors.New("This task mode is not supported by this list function")
  297. }
  298. //List directories of the restorable snapshots
  299. snapshotPath := filepath.ToSlash(filepath.Clean(filepath.Join(task.DiskPath, "/version/")))
  300. filesInSnapshotFolder, err := filepath.Glob(snapshotPath + "/*")
  301. if err != nil {
  302. return restorableFiles, err
  303. }
  304. //Check if the foler is actually a snapshot
  305. avaibleSnapshot := []string{}
  306. for _, fileObject := range filesInSnapshotFolder {
  307. possibleSnapshotDatalinkFile := filepath.Join(fileObject, "snapshot.datalink")
  308. if fileExists(possibleSnapshotDatalinkFile) {
  309. //This is a snapshot
  310. avaibleSnapshot = append(avaibleSnapshot, fileObject)
  311. }
  312. }
  313. //Build restorabe file struct for returning
  314. for _, snapshot := range avaibleSnapshot {
  315. thisFile := RestorableFile{
  316. Filename: filepath.Base(snapshot),
  317. IsHidden: false,
  318. Filesize: 0,
  319. RelpathOnDisk: filepath.ToSlash(snapshot),
  320. RestorePoint: task.ParentUID,
  321. BackupDiskUID: task.DiskUID,
  322. RemainingTime: -1,
  323. DeleteTime: -1,
  324. IsSnapshot: true,
  325. }
  326. restorableFiles = append(restorableFiles, &thisFile)
  327. }
  328. return restorableFiles, nil
  329. }