Browse Source

Merge commit '2ba30fcc229a527cb271743d6cbb3a82a04466d5' into OAuth

AY 4 years ago
parent
commit
21b44a737d

+ 13 - 6
backup.go

@@ -143,14 +143,21 @@ func backup_listRestorable(w http.ResponseWriter, r *http.Request) {
 	if paretnfsh.Hierarchy == "user" {
 	if paretnfsh.Hierarchy == "user" {
 		//The file system is user based. Filter out those file that is not belong to this user
 		//The file system is user based. Filter out those file that is not belong to this user
 		for _, restorableFile := range restorableReport.RestorableFiles {
 		for _, restorableFile := range restorableReport.RestorableFiles {
-			fileAbsPath := filepath.Join(fsh.Path, restorableFile.RelpathOnDisk)
-			_, err := userinfo.RealPathToVirtualPath(fileAbsPath)
-			if err != nil {
-				//Cannot translate this file. That means the file is not owned by this user
-			} else {
-				//Can translate the path.
+			if restorableFile.IsSnapshot {
+				//Is snapshot. Always allow access
 				result.RestorableFiles = append(result.RestorableFiles, restorableFile)
 				result.RestorableFiles = append(result.RestorableFiles, restorableFile)
+			} else {
+				//Is file
+				fileAbsPath := filepath.Join(fsh.Path, restorableFile.RelpathOnDisk)
+				_, err := userinfo.RealPathToVirtualPath(fileAbsPath)
+				if err != nil {
+					//Cannot translate this file. That means the file is not owned by this user
+				} else {
+					//Can translate the path.
+					result.RestorableFiles = append(result.RestorableFiles, restorableFile)
+				}
 			}
 			}
+
 		}
 		}
 	} else {
 	} else {
 		result = restorableReport
 		result = restorableReport

+ 1 - 0
documents/aroz version backup.drawio

@@ -0,0 +1 @@
+<mxfile host="Electron" modified="2021-08-04T15:38:00.121Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.3 Chrome/85.0.4183.98 Electron/10.1.2 Safari/537.36" etag="iStb3NhjgmRJ35-A-Wke" version="13.7.3" type="device"><diagram id="VnsJyidMvFq4PCg9yRLU" name="第1頁">3Zpdb+I4FIZ/DZdFsR2H5HIonRltNbvVMtJ09s4kDvFOiJFjCsyvX5s4H8b0gxUlBYSEfWKfxM955fgcMUC3i80XQZbZN57QfAC9ZDNAkwGEwIdwoL9esq0sOBhVhrlgiRnUGqbsNzVGz1hXLKGlNVBynku2tI0xLwoaS8tGhOBre1jKc/uuSzKnjmEak9y1/mCJzCprCEet/Stl86y+Mwii6sqC1IPNSsqMJHzdMaG7AboVnMuqtdjc0lzDq7lU8z4/c7V5MEEL+ZYJhCwW3+bf7+4fPk/K9d0//47vVzfGyxPJV2bB5mHltiZAi+STBql6BS+UcZzJRa56QDWr0TRxILZPBZq1KpFQvqBSbNWQdUsTG0JZB2RtEzQnkj3Z7okJ6rxx19zhgTN1Y+gZATZ+jPz8yLNdlHwlYmpmdem94gjsO5JEzKl0HKlGZ9mtaRecIwIFrzxQCHlDr/OBpwnby25BGA2j7uesIUVXHlIfoPcI6ctu+w0pdkJaitiJquCrIqHaj6fCuM6YpNMlifXVtXqL2lFOWZ7f8pyL3VyUEBqmsbKXUvBftHMliEM6SxtdPFEh6eZYZdQTAnu3g57pd5QTHFCO7z0vEov1sWCDKwWL+gY7ulKwzSbWF9jQAasOxyxliuIp6aZpCuODdJNgFuDgNHThR5Nt5NCd0JxKelq2YUwPs52F2Mfeadgi/4Mpt069XjqYHIX1BJBw+MzxuwNpdABSI9TTU3JTp2lBlmXG9VkFOMTU2qWNheRsXqh2rBhQJayxJsRU6vnJXFiwJNHTx4KW7DeZ7Vxp3kt9ftktCI8HeKJ9rSQvq+T5RMSbDLxO3IFLPDxAHL4bcDcFqoG7+rw02o1OX6B9SN/vR9vNTmra6PJpj6IPRtt3aP9NS8mFeqF5HZUHueY8E6o1lw2LC44ExvjVSAB41lC4OZxDuZOWxzkpSxbbzNXixfax2/mpmQ5x3Z1sDOOqt617GyYfdyM9CEy/mumj0PTbqbrTnflABVMMdJAn7dnk9fqAWtou9X7DkaBKn1879fZWcIjsTdTff/m/tcSA945kCO85eucyAnDT3bNr8EaJEESWCGGAL0KEo15FuFcodnaqN4tw70gARmcWoVsauHYR9qUZld1YJU1gKwhEwyD4fyJSk4cI7QtyCHBbIA3D88rKLYxcZsUJh/bRpfcaKXCLIldCtu+KSL2FX0cxbx9v78W8+sXSwbvbftT+Db2EpqzQnL2ZXoObA8lMJ0l/TP/6U/3krPilfhRXtxR4abmR79txOlTzAuBAoNDxgVLd9p8I1Vbf/p8D3f0H</diagram></mxfile>

BIN
documents/aroz version backup.png


+ 1 - 0
mod/disk/hybridBackup/compareRoots.go

@@ -47,6 +47,7 @@ func (t *BackupTask) compareRootPaths() ([]*RestorableFile, error) {
 				BackupDiskUID: t.DiskUID,
 				BackupDiskUID: t.DiskUID,
 				RemainingTime: 86400 - (time.Now().Unix() - value),
 				RemainingTime: 86400 - (time.Now().Unix() - value),
 				DeleteTime:    value,
 				DeleteTime:    value,
+				IsSnapshot:    false,
 			}
 			}
 			results = append(results, &thisFile)
 			results = append(results, &thisFile)
 		}
 		}

+ 9 - 0
mod/disk/hybridBackup/hybridBackup.go

@@ -63,6 +63,7 @@ type RestorableFile struct {
 	BackupDiskUID string //The UID of disk that is hold the backup of this file
 	BackupDiskUID string //The UID of disk that is hold the backup of this file
 	RemainingTime int64  //Remaining time till auto remove
 	RemainingTime int64  //Remaining time till auto remove
 	DeleteTime    int64  //Delete time
 	DeleteTime    int64  //Delete time
+	IsSnapshot    bool   //Define is this restorable file point to a snapshot instead
 }
 }
 
 
 //The restorable report
 //The restorable report
@@ -287,6 +288,14 @@ func (m *Manager) ListRestorable(parentDiskID string) (RestorableReport, error)
 				diffFiles = append(diffFiles, restorable)
 				diffFiles = append(diffFiles, restorable)
 			}
 			}
 		} else if task.Mode == "version" {
 		} else if task.Mode == "version" {
+			restorableFiles, err := listVersionRestorables(task)
+			if err != nil {
+				//Something went wrong. Skip this
+				continue
+			}
+			for _, restorable := range restorableFiles {
+				diffFiles = append(diffFiles, restorable)
+			}
 
 
 		} else {
 		} else {
 			//Unknown mode. Skip it
 			//Unknown mode. Skip it

+ 64 - 0
mod/disk/hybridBackup/linker.go

@@ -0,0 +1,64 @@
+package hybridBackup
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"path/filepath"
+)
+
+/*
+	Linker.go
+
+	This script handle the linking file operations
+
+*/
+
+type LinkFileMap struct {
+	UnchangedFile map[string]string
+	DeletedFiles  map[string]string
+}
+
+//Generate and write link file to disk
+func generateLinkFile(snapshotFolder string, lf LinkFileMap) error {
+	js, err := json.MarshalIndent(lf, "", "\t")
+	if err != nil {
+		return err
+	}
+
+	return ioutil.WriteFile(filepath.Join(snapshotFolder, "snapshot.datalink"), js, 0755)
+}
+
+//Read link file and parse it into link file map
+func readLinkFile(snapshotFolder string) (*LinkFileMap, error) {
+	result := LinkFileMap{
+		UnchangedFile: map[string]string{},
+		DeletedFiles:  map[string]string{},
+	}
+
+	//Check if the link file exists
+	expectedLinkFilePath := filepath.Join(snapshotFolder, "snapshot.datalink")
+	if fileExists(expectedLinkFilePath) {
+		//Read the content of the link file
+		content, err := ioutil.ReadFile(expectedLinkFilePath)
+		if err == nil {
+			//No error. Read and parse the content
+			lfContent := LinkFileMap{}
+			err := json.Unmarshal(content, &lfContent)
+			if err == nil {
+				return &lfContent, nil
+			}
+		}
+	}
+
+	return &result, nil
+}
+
+//Check if a file exists in a linkFileMap. return boolean and its linked to snapshot name
+func (lfm *LinkFileMap) fileExists(fileRelPath string) (bool, string) {
+	val, ok := lfm.UnchangedFile[filepath.ToSlash(fileRelPath)]
+	if !ok {
+		return false, ""
+	} else {
+		return true, val
+	}
+}

+ 164 - 29
mod/disk/hybridBackup/versionBackup.go

@@ -2,7 +2,6 @@ package hybridBackup
 
 
 import (
 import (
 	"errors"
 	"errors"
-	"io/ioutil"
 	"log"
 	"log"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
@@ -38,15 +37,22 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 	}
 	}
 
 
 	todayFolderName := time.Now().Format("2006-01-02")
 	todayFolderName := time.Now().Format("2006-01-02")
-	previousSnapshotName, _ := getPreviousSnapshotName(backupConfig, todayFolderName)
+	previousSnapshotExists := true
+	previousSnapshotName, err := getPreviousSnapshotName(backupConfig, todayFolderName)
+	if err != nil {
+		previousSnapshotExists = false
+	}
 	snapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName)
 	snapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName)
 	previousSnapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", previousSnapshotName)
 	previousSnapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", previousSnapshotName)
 
 
+	//Create today folder if not exist
 	if !fileExists(snapshotLocation) {
 	if !fileExists(snapshotLocation) {
-		//Create today folder if not exist
 		os.MkdirAll(snapshotLocation, 0755)
 		os.MkdirAll(snapshotLocation, 0755)
 	}
 	}
 
 
+	//Read the previous snapshot datalink into a LinkFileMap and use binary search for higher performance
+	previousSnapshotMap, _ := readLinkFile(previousSnapshotLocation)
+
 	/*
 	/*
 		Run a three pass compare logic between
 		Run a three pass compare logic between
 		1. source disk and new backup disk to check any new / modified files (created today)
 		1. source disk and new backup disk to check any new / modified files (created today)
@@ -54,6 +60,8 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 		3. file in today backup disk no longer in the current source disk (created today, deleted today)
 		3. file in today backup disk no longer in the current source disk (created today, deleted today)
 	*/
 	*/
 	copiedFileList := []string{}
 	copiedFileList := []string{}
+	linkedFileList := map[string]string{}
+	deletedFileList := map[string]string{}
 
 
 	//First pass: Check if there are any updated file from source and backup it to backup drive
 	//First pass: Check if there are any updated file from source and backup it to backup drive
 	fastWalk(parentRootAbs, func(filename string) error {
 	fastWalk(parentRootAbs, func(filename string) error {
@@ -74,39 +82,57 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 		yesterdayBackupLocation := filepath.Join(previousSnapshotLocation, relPath)
 		yesterdayBackupLocation := filepath.Join(previousSnapshotLocation, relPath)
 
 
 		//Check if the file exists
 		//Check if the file exists
-		if !fileExists(fileBackupLocation) && !fileExists(yesterdayBackupLocation) {
-			//File not exists in both current source and yesterday one. Copy it to the target location
-			if !isDir(fileBackupLocation) && fileExists(fileBackupLocation+".deleted") {
-				os.Remove(fileBackupLocation + ".deleted")
-			}
+		if !fileExists(yesterdayBackupLocation) {
+			//This file not in last snapshot location.
+			//Check if it is in previous snapshot map
+			fileFoundInSnapshotLinkFile, nameOfSnapshot := previousSnapshotMap.fileExists(relPath)
+			if fileFoundInSnapshotLinkFile {
+				//File found in the snapshot link file. Compare the one in snapshot
+				linkedSnapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", nameOfSnapshot)
+				linkedSnapshotOriginalFile := filepath.Join(linkedSnapshotLocation, relPath)
+				if fileExists(linkedSnapshotOriginalFile) {
+					//Linked file exists. Compare hash
+					fileHashMatch, err := fileHashIdentical(fileAbs, linkedSnapshotOriginalFile)
+					if err != nil {
+						return nil
+					}
 
 
-			if !fileExists(filepath.Dir(fileBackupLocation)) {
-				os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
-			}
+					if fileHashMatch {
+						//append this record to this snapshot linkdata file
+						linkedFileList[relPath] = nameOfSnapshot
+					} else {
+						//File hash mismatch. Do file copy to renew data
+						copyFileToBackupLocation(filename, fileBackupLocation)
+						copiedFileList = append(copiedFileList, fileBackupLocation)
+					}
+				} else {
+					//Invalid snapshot linkage. Assume new and do copy
+					log.Println("[HybridBackup] Link lost. Cloning source file to snapshot.")
+					copyFileToBackupLocation(filename, fileBackupLocation)
+					copiedFileList = append(copiedFileList, fileBackupLocation)
+				}
 
 
-			err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
-			if err != nil {
-				log.Println("[HybridBackup] Failed to copy file: ", filepath.Base(filename)+". "+err.Error())
+			} else {
+				//This file is not in snapshot link file.
+				//This is new file. Copy it to backup
+				copyFileToBackupLocation(filename, fileBackupLocation)
+				copiedFileList = append(copiedFileList, fileBackupLocation)
 			}
 			}
 
 
-			copiedFileList = append(copiedFileList, fileBackupLocation)
-
 		} else if fileExists(yesterdayBackupLocation) {
 		} else if fileExists(yesterdayBackupLocation) {
 			//The file exists in the last snapshot
 			//The file exists in the last snapshot
 			//Check if their hash is the same. If no, update it
 			//Check if their hash is the same. If no, update it
-			srcHash, err := getFileHash(fileAbs)
+			fileHashMatch, err := fileHashIdentical(fileAbs, yesterdayBackupLocation)
 			if err != nil {
 			if err != nil {
-				log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
-				return nil
-			}
-			targetHash, err := getFileHash(yesterdayBackupLocation)
-			if err != nil {
-				log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(fileBackupLocation), err.Error(), " Skipping.")
 				return nil
 				return nil
 			}
 			}
 
 
-			if srcHash != targetHash {
+			if !fileHashMatch {
 				//Hash mismatch. Overwrite the file
 				//Hash mismatch. Overwrite the file
+				if !fileExists(filepath.Dir(fileBackupLocation)) {
+					os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
+				}
+
 				err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
 				err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
 				if err != nil {
 				if err != nil {
 					log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
 					log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
@@ -114,6 +140,9 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 					//No problem. Add this filepath into the list
 					//No problem. Add this filepath into the list
 					copiedFileList = append(copiedFileList, fileBackupLocation)
 					copiedFileList = append(copiedFileList, fileBackupLocation)
 				}
 				}
+			} else {
+				//Create a link file for this relative path
+				linkedFileList[relPath] = previousSnapshotName
 			}
 			}
 		} else {
 		} else {
 			//Default case
 			//Default case
@@ -133,6 +162,10 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 
 
 				if srcHash != targetHash {
 				if srcHash != targetHash {
 					//Hash mismatch. Overwrite the file
 					//Hash mismatch. Overwrite the file
+					if !fileExists(filepath.Dir(fileBackupLocation)) {
+						os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
+					}
+
 					err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
 					err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
 					if err != nil {
 					if err != nil {
 						log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
 						log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
@@ -149,8 +182,12 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 
 
 	//2nd pass: Check if there are anything exists in the previous backup but no longer exists in the source now
 	//2nd pass: Check if there are anything exists in the previous backup but no longer exists in the source now
 	//For case where the file is backed up in previous snapshot but now the file has been removed
 	//For case where the file is backed up in previous snapshot but now the file has been removed
-	if fileExists(previousSnapshotLocation) {
+	if previousSnapshotExists {
 		fastWalk(previousSnapshotLocation, func(filename string) error {
 		fastWalk(previousSnapshotLocation, func(filename string) error {
+			if filepath.Base(filename) == "snapshot.datalink" {
+				//System reserved file. Skip this
+				return nil
+			}
 			//Get the target paste location
 			//Get the target paste location
 			rootAbs, _ := filepath.Abs(previousSnapshotLocation)
 			rootAbs, _ := filepath.Abs(previousSnapshotLocation)
 			fileAbs, _ := filepath.Abs(filename)
 			fileAbs, _ := filepath.Abs(filename)
@@ -160,15 +197,25 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 
 
 			relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
 			relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
 			sourcAssumeLocation := filepath.Join(parentRootAbs, relPath)
 			sourcAssumeLocation := filepath.Join(parentRootAbs, relPath)
-			todaySnapshotLocation := filepath.Join(snapshotLocation, relPath)
+			//todaySnapshotLocation := filepath.Join(snapshotLocation, relPath)
 
 
 			if !fileExists(sourcAssumeLocation) {
 			if !fileExists(sourcAssumeLocation) {
 				//File exists in yesterday snapshot but not in the current source
 				//File exists in yesterday snapshot but not in the current source
 				//Assume it has been deleted, create a dummy indicator file
 				//Assume it has been deleted, create a dummy indicator file
-				ioutil.WriteFile(todaySnapshotLocation+".deleted", []byte(""), 0755)
+				//ioutil.WriteFile(todaySnapshotLocation+".deleted", []byte(""), 0755)
+				deletedFileList[relPath] = todayFolderName
 			}
 			}
 			return nil
 			return nil
 		})
 		})
+
+		//Check for deleting of unchanged file as well
+		for relPath, _ := range previousSnapshotMap.UnchangedFile {
+			sourcAssumeLocation := filepath.Join(parentRootAbs, relPath)
+			if !fileExists(sourcAssumeLocation) {
+				//The source file no longer exists
+				deletedFileList[relPath] = todayFolderName
+			}
+		}
 	}
 	}
 
 
 	//3rd pass: Check if there are anything (except file with .deleted) in today backup drive that didn't exists in the source drive
 	//3rd pass: Check if there are anything (except file with .deleted) in today backup drive that didn't exists in the source drive
@@ -179,7 +226,7 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 			return nil
 			return nil
 		}
 		}
 
 
-		if filepath.Ext(filename) == ".deleted" {
+		if filepath.Ext(filename) == ".datalink" {
 			//Deleted file marker. Skip this
 			//Deleted file marker. Skip this
 			return nil
 			return nil
 		}
 		}
@@ -201,6 +248,16 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 		return nil
 		return nil
 	})
 	})
 
 
+	//Generate linkfile for this snapshot
+	generateLinkFile(snapshotLocation, LinkFileMap{
+		UnchangedFile: linkedFileList,
+		DeletedFiles:  deletedFileList,
+	})
+
+	if err != nil {
+		return "", err
+	}
+
 	return "", nil
 	return "", nil
 }
 }
 
 
@@ -216,7 +273,7 @@ func getPreviousSnapshotName(backupConfig *BackupTask, currentSnapshotName strin
 	existingSnapshots := []string{}
 	existingSnapshots := []string{}
 	files, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(backupRootAbs)) + "/*")
 	files, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(backupRootAbs)) + "/*")
 	for _, file := range files {
 	for _, file := range files {
-		if isDir(file) {
+		if isDir(file) && fileExists(filepath.Join(file, "snapshot.datalink")) {
 			existingSnapshots = append(existingSnapshots, filepath.Base(file))
 			existingSnapshots = append(existingSnapshots, filepath.Base(file))
 		}
 		}
 	}
 	}
@@ -245,3 +302,81 @@ func getPreviousSnapshotName(backupConfig *BackupTask, currentSnapshotName strin
 
 
 	return previousSnapshotName, nil
 	return previousSnapshotName, nil
 }
 }
+
+func copyFileToBackupLocation(filename string, fileBackupLocation string) error {
+	if !fileExists(filepath.Dir(fileBackupLocation)) {
+		os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
+	}
+
+	err := BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
+	if err != nil {
+		log.Println("[HybridBackup] Failed to copy file: ", filepath.Base(filename)+". "+err.Error())
+		return err
+	}
+	return nil
+}
+
+func fileHashIdentical(srcFile string, matchingFile string) (bool, error) {
+	srcHash, err := getFileHash(srcFile)
+	if err != nil {
+		log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(srcFile), err.Error(), " Skipping.")
+		return false, nil
+	}
+	targetHash, err := getFileHash(matchingFile)
+	if err != nil {
+		log.Println("[HybridBackup] Hash calculation failed for file "+filepath.Base(matchingFile), err.Error(), " Skipping.")
+		return false, nil
+	}
+
+	if srcHash != targetHash {
+		return false, nil
+	} else {
+		return true, nil
+	}
+}
+
+//List all restorable for version backup
+func listVersionRestorables(task *BackupTask) ([]*RestorableFile, error) {
+	//Check if mode is set correctly
+	restorableFiles := []*RestorableFile{}
+	if task.Mode != "version" {
+		return restorableFiles, errors.New("This task mode is not supported by this list function")
+	}
+
+	//List directories of the restorable snapshots
+	snapshotPath := filepath.ToSlash(filepath.Clean(filepath.Join(task.DiskPath, "/version/")))
+	filesInSnapshotFolder, err := filepath.Glob(snapshotPath + "/*")
+	if err != nil {
+		return restorableFiles, err
+	}
+
+	//Check if the foler is actually a snapshot
+	avaibleSnapshot := []string{}
+	for _, fileObject := range filesInSnapshotFolder {
+		possibleSnapshotDatalinkFile := filepath.Join(fileObject, "snapshot.datalink")
+		if fileExists(possibleSnapshotDatalinkFile) {
+			//This is a snapshot
+			avaibleSnapshot = append(avaibleSnapshot, fileObject)
+		}
+	}
+
+	//Build restorabe file struct for returning
+	for _, snapshot := range avaibleSnapshot {
+		thisFile := RestorableFile{
+			Filename:      filepath.Base(snapshot),
+			IsHidden:      false,
+			Filesize:      0,
+			RelpathOnDisk: filepath.ToSlash(snapshot),
+			RestorePoint:  task.ParentUID,
+			BackupDiskUID: task.DiskUID,
+			RemainingTime: -1,
+			DeleteTime:    -1,
+			IsSnapshot:    true,
+		}
+
+		restorableFiles = append(restorableFiles, &thisFile)
+	}
+
+	return restorableFiles, nil
+
+}

+ 76 - 13
web/SystemAO/disk/disk_restore.html

@@ -59,6 +59,12 @@
             overflow-y: scroll;
             overflow-y: scroll;
             padding-right: 4px;
             padding-right: 4px;
         }
         }
+
+        #snapshotList{
+            max-height: 300px;
+            overflow-y: scroll;
+            padding-right: 4px;
+        }
         
         
         .timeleft{
         .timeleft{
             width: 30px;
             width: 30px;
@@ -120,14 +126,22 @@
             <i class="icon remove"></i>
             <i class="icon remove"></i>
         </button>
         </button>
        </div>
        </div>
+       <div id="snapshotList" class="ui middle aligned divided list">
+            <div class="item">
+                <img class="ui mini image nointeract" src="../../img/system/drive-backup.svg">
+                <div class="content">
+                No Restorable Snapshot(s) for this virtual disk.
+                </div>
+            </div>
+        </div>
         <div id="restorableList" class="ui middle aligned divided list">
         <div id="restorableList" class="ui middle aligned divided list">
             <div class="item">
             <div class="item">
-              <img class="ui mini image nointeract" src="../../img/system/file-notfound.svg">
-              <div class="content">
+                <img class="ui mini image nointeract" src="../../img/system/file-notfound.svg">
+                <div class="content">
                 No Restorable File(s) for this virtual disk.
                 No Restorable File(s) for this virtual disk.
-              </div>
+                </div>
             </div>
             </div>
-          </div>
+        </div>
         <div class="ui checkbox">
         <div class="ui checkbox">
             <input type="checkbox" name="showhidden" autocomplete="false" onchange="toggleHiddenFiles(this.checked);">
             <input type="checkbox" name="showhidden" autocomplete="false" onchange="toggleHiddenFiles(this.checked);">
             <label>Show hidden / cache files</label>
             <label>Show hidden / cache files</label>
@@ -164,7 +178,12 @@
             filedata = JSON.parse(decodeURIComponent(filedata));
             filedata = JSON.parse(decodeURIComponent(filedata));
             console.log(filedata);
             console.log(filedata);
             //Show information on page
             //Show information on page
-            $("#fileinfo").find(".filename").text(filedata.Filename + ` (${ao_module_utils.formatBytes(filedata.Filesize, 2)})`);
+            if (filedata.IsSnapshot){
+                $("#fileinfo").find(".filename").text("Snapshot " + filedata.Filename);
+            }else{
+                $("#fileinfo").find(".filename").text(filedata.Filename + ` (${ao_module_utils.formatBytes(filedata.Filesize, 2)})`);
+            }
+            
             
             
             //Hide the filename in relpath
             //Hide the filename in relpath
             var shortenRelpath = filedata.RelpathOnDisk.split("/");
             var shortenRelpath = filedata.RelpathOnDisk.split("/");
@@ -176,6 +195,14 @@
             $("#fileinfo").find(".restorePoint").html('<i class="ui refresh icon"></i> Restore to ' + (filedata.RestorePoint));
             $("#fileinfo").find(".restorePoint").html('<i class="ui refresh icon"></i> Restore to ' + (filedata.RestorePoint));
             $("#fileinfo").find(".deleteRemainingTime").html('<i class="ui red remove icon"></i> Auto delete in '+ secondsToHms(filedata.RemainingTime));
             $("#fileinfo").find(".deleteRemainingTime").html('<i class="ui red remove icon"></i> Auto delete in '+ secondsToHms(filedata.RemainingTime));
 
 
+            if (filedata.IsSnapshot){
+                $("#fileinfo").find(".deleteRemainingTime").hide();
+                $("#fileinfo").find(".deleteTime").hide();
+            }else{
+                $("#fileinfo").find(".deleteRemainingTime").show();
+                $("#fileinfo").find(".deleteTime").show();
+            }
+
             $("#fileinfo").slideDown('fast');
             $("#fileinfo").slideDown('fast');
         }
         }
 
 
@@ -265,15 +292,49 @@
                     if (data.error !== undefined){
                     if (data.error !== undefined){
                         $("#error").find(".reason").texxt(data.error);
                         $("#error").find(".reason").texxt(data.error);
                     }else{
                     }else{
-                      
-                        //Sort the result by latest first
-                        data.RestorableFiles.sort(function(a, b) {
+
+                        //Filter out the restorable snapshot and files
+                        let restorableFiles = [];
+                        let restorableSnapshots = [];
+
+                        data.RestorableFiles.forEach(file => {
+                            if (file.IsSnapshot){
+                                restorableSnapshots.push(file);
+                            }else{
+                                restorableFiles.push(file);
+                                
+                            }
+                        });
+
+                        //Sort the result by latest first (file only)
+                        restorableFiles.sort(function(a, b) {
                             return b.DeleteTime - a.DeleteTime;
                             return b.DeleteTime - a.DeleteTime;
                         });
                         });
+                        restorableSnapshots.sort(function(a, b) {
+                            return b.Filename > a.Filename;
+                        });
 
 
-                        //Display the result
+                        //Display the reuslt for restorableSnapshot
+                        $("#snapshotList").html("");
+                        restorableSnapshots.forEach(snapshot => {
+                            let snapshotData = encodeURIComponent(JSON.stringify(snapshot));
+                            $("#snapshotList").append(`<div class="item restorableFile" filedata="${snapshotData}">
+                                <div class="right floated content">
+                                    <div class="ui tiny button" onclick="showFileInfo(this.parentNode.parentNode)">Info</div>
+                                    <div class="ui tiny green button" onclick="restoreThisFile($(this).parent().parent());">Restore</div>
+                                </div>
+                                
+                                <div class="content">
+                                    <p style="word-break: break-all; font-size: 97%"><img class="ui mini spaced image nointeract" src="../../img/system/drive-backup.svg">
+                                        Snapshot ${snapshot.Filename}
+                                    </p>
+                                </div>
+                            </div>`);
+                        })
+
+                        //Display the result for restorableFile
                         $("#restorableList").html("");
                         $("#restorableList").html("");
-                        data.RestorableFiles.forEach(fileObject => {
+                        restorableFiles.forEach(fileObject => {
                             let thisFileData = encodeURIComponent(JSON.stringify(fileObject));
                             let thisFileData = encodeURIComponent(JSON.stringify(fileObject));
 
 
                             var timerIcon = "100.svg";
                             var timerIcon = "100.svg";
@@ -321,7 +382,7 @@
                             </div>`);
                             </div>`);
                         });
                         });
 
 
-                        if (data.RestorableFiles.length == 0){
+                        if (restorableFiles.length == 0){
                             $("#restorableList").html(`<div class="item">
                             $("#restorableList").html(`<div class="item">
                             <img class="ui mini image nointeract" src="../../img/system/file-notfound.svg">
                             <img class="ui mini image nointeract" src="../../img/system/file-notfound.svg">
                             <div class="content">
                             <div class="content">
@@ -330,10 +391,12 @@
                             </div>`);
                             </div>`);
 
 
                         }
                         }
-
-                        $(".hiddenfile").hide();
                     }
                     }
+                    
+
+                    $(".hiddenfile").hide();
                 }
                 }
+                
             })
             })
         }
         }