versionBackup.go 16 KB

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