Parcourir la source

Completed snapshot restore feature

TC pushbot 5 il y a 4 ans
Parent
commit
f1441a508f

+ 28 - 5
backup.go

@@ -56,6 +56,18 @@ func backup_renderSnapshotSummary(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	//Get parent disk hierarcy
+	parentDiskID, err := userinfo.HomeDirectories.HyperBackupManager.GetParentDiskIDByRestoreDiskID(fsh.UUID)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	parentFsh, err := GetFsHandlerByUUID(parentDiskID)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
 	//Get task by the backup disk id
 	task, err := userinfo.HomeDirectories.HyperBackupManager.GetTaskByBackupDiskID(fsh.UUID)
 	if err != nil {
@@ -65,10 +77,21 @@ func backup_renderSnapshotSummary(w http.ResponseWriter, r *http.Request) {
 
 	if task.Mode == "version" {
 		//Generate snapshot summary
-		summary, err := task.GenerateSnapshotSummary(snapshot)
-		if err != nil {
-			sendErrorResponse(w, err.Error())
-			return
+		var summary *hybridBackup.SnapshotSummary
+		if parentFsh.Hierarchy == "user" {
+			s, err := task.GenerateSnapshotSummary(snapshot, &userinfo.Username)
+			if err != nil {
+				sendErrorResponse(w, err.Error())
+				return
+			}
+			summary = s
+		} else {
+			s, err := task.GenerateSnapshotSummary(snapshot, nil)
+			if err != nil {
+				sendErrorResponse(w, err.Error())
+				return
+			}
+			summary = s
 		}
 
 		js, _ := json.Marshal(summary)
@@ -111,7 +134,7 @@ func backup_restoreSelected(w http.ResponseWriter, r *http.Request) {
 	}
 
 	//Handle restore of the file
-	err = userinfo.HomeDirectories.HyperBackupManager.HandleRestore(fsh.UUID, relpath)
+	err = userinfo.HomeDirectories.HyperBackupManager.HandleRestore(fsh.UUID, relpath, &userinfo.Username)
 	if err != nil {
 		sendErrorResponse(w, err.Error())
 		return

+ 32 - 23
mod/disk/hybridBackup/hybridBackup.go

@@ -167,8 +167,6 @@ func (m *Manager) Close() error {
 
 //Main handler function for hybrid backup
 func (backupConfig *BackupTask) HandleBackupProcess() (string, error) {
-	log.Println(">>>>>> [Debug] Running backup process: ", backupConfig)
-
 	//Check if the target disk is writable and mounted
 	if fileExists(filepath.Join(backupConfig.ParentPath, "aofs.db")) && fileExists(filepath.Join(backupConfig.ParentPath, "aofs.db.lock")) {
 		//This parent filesystem is mounted
@@ -194,12 +192,14 @@ func (backupConfig *BackupTask) HandleBackupProcess() (string, error) {
 			deepBackup = false
 			backupConfig.LastCycleTime = time.Now().Unix()
 		}
+		log.Println("[HybridBackup] Basic backup executed: " + backupConfig.ParentUID + ":/ -> " + backupConfig.DiskUID + ":/")
 		return executeBackup(backupConfig, deepBackup)
 	} else if backupConfig.Mode == "nightly" {
 		if time.Now().Unix()-backupConfig.LastCycleTime >= 86400 {
 			//24 hours from last backup. Execute deep backup now
 			executeBackup(backupConfig, true)
 			backupConfig.LastCycleTime = time.Now().Unix()
+			log.Println("[HybridBackup] Executing nightly backup: " + backupConfig.ParentUID + ":/ -> " + backupConfig.DiskUID + ":/")
 		}
 
 	} else if backupConfig.Mode == "version" {
@@ -208,6 +208,7 @@ func (backupConfig *BackupTask) HandleBackupProcess() (string, error) {
 			//Scheduled backup or initial backup
 			executeVersionBackup(backupConfig)
 			backupConfig.LastCycleTime = time.Now().Unix()
+			log.Println("[HybridBackup] Executing backup schedule: " + backupConfig.ParentUID + ":/ -> " + backupConfig.DiskUID + ":/")
 		}
 	}
 
@@ -229,7 +230,7 @@ func (m *Manager) GetParentDiskIDByRestoreDiskID(restoreDiskID string) (string,
 }
 
 //Restore accidentailly removed file from backup
-func (m *Manager) HandleRestore(restoreDiskID string, targetFileRelpath string) error {
+func (m *Manager) HandleRestore(restoreDiskID string, targetFileRelpath string, username *string) error {
 	//Get the backup task from backup disk id
 	backupTask := m.getTaskByBackupDiskID(restoreDiskID)
 	if backupTask == nil {
@@ -242,31 +243,39 @@ func (m *Manager) HandleRestore(restoreDiskID string, targetFileRelpath string)
 	restoreSource := filepath.Join(backupTask.DiskPath, targetFileRelpath)
 	if backupTask.Mode == "basic" || backupTask.Mode == "nightly" {
 		restoreSource = filepath.Join(backupTask.DiskPath, "/backup/", targetFileRelpath)
-	} else if backupTask.Mode == "version" {
-		restoreSource = filepath.Join(backupTask.DiskPath, "/versions/", targetFileRelpath)
-	}
+		restoreTarget := filepath.Join(backupTask.ParentPath, targetFileRelpath)
 
-	restoreTarget := filepath.Join(backupTask.ParentPath, targetFileRelpath)
+		if !fileExists(restoreSource) {
+			//Restore source not exists
+			return errors.New("Restore source file not exists")
+		}
 
-	if !fileExists(restoreSource) {
-		//Restore source not exists
-		return errors.New("Restore source file not exists")
-	}
+		if fileExists(restoreTarget) {
+			//Restore target already exists.
+			return errors.New("Restore target already exists. Cannot overwrite.")
+		}
 
-	if fileExists(restoreTarget) {
-		//Restore target already exists.
-		return errors.New("Restore target already exists. Cannot overwrite.")
-	}
+		//Check if the restore target parent folder exists. If not, create it
+		if !fileExists(filepath.Dir(restoreTarget)) {
+			os.MkdirAll(filepath.Dir(restoreTarget), 0755)
+		}
 
-	//Check if the restore target parent folder exists. If not, create it
-	if !fileExists(filepath.Dir(restoreTarget)) {
-		os.MkdirAll(filepath.Dir(restoreTarget), 0755)
-	}
+		//Ready to move it back
+		err := BufferedLargeFileCopy(restoreSource, restoreTarget, 4086)
+		if err != nil {
+			return errors.New("Restore failed: " + err.Error())
+		}
+	} else if backupTask.Mode == "version" {
+		//Check if username is set
+		if username == nil {
+			return errors.New("Snapshot mode backup require username to restore")
+		}
 
-	//Ready to move it back
-	err := BufferedLargeFileCopy(restoreSource, restoreTarget, 4086)
-	if err != nil {
-		return errors.New("Restore failed: " + err.Error())
+		//Restore the snapshot
+		err := restoreSnapshotByName(backupTask, targetFileRelpath, username)
+		if err != nil {
+			return errors.New("Restore failed: " + err.Error())
+		}
 	}
 
 	//Restore completed

+ 103 - 0
mod/disk/hybridBackup/restoreSnapshot.go

@@ -0,0 +1,103 @@
+package hybridBackup
+
+import (
+	"errors"
+	"log"
+	"os"
+	"path/filepath"
+)
+
+/*
+	restoreSnapshot.go
+
+	Restore snapshot for a certain user in the snapshot
+	The steps basically as follows.
+
+	1. Check and validate the snapshot
+	2. Iterate and restore all files contained in that snapshot to source drive if it is owned by the user
+	3. Get the snapshot link file. Restore all files with pointer still exists and owned by the user
+
+*/
+
+//Restore a snapshot by task and name
+func restoreSnapshotByName(backupTask *BackupTask, snapshotName string, username *string) error {
+	//Step 1: Check and validate snapshot
+	snapshotBaseFolder := filepath.Join(backupTask.DiskPath, "/version/", snapshotName)
+	snapshotRestoreDirectory := filepath.ToSlash(filepath.Clean(backupTask.ParentPath))
+	if !fileExists(snapshotBaseFolder) {
+		return errors.New("Given snapshot ID not found")
+	}
+
+	if !fileExists(filepath.Join(snapshotBaseFolder, "snapshot.datalink")) {
+		return errors.New("Snapshot corrupted. snapshot.datalink pointer file not found.")
+	}
+
+	log.Println("[HybridBackup] Restoring from snapshot ID: ", filepath.Base(snapshotBaseFolder))
+
+	//Step 2: Restore all the files changed during that snapshot period
+	fastWalk(snapshotBaseFolder, func(filename string) error {
+		//Skip the datalink file
+		if filepath.Base(filename) == "snapshot.datalink" {
+			return nil
+		}
+		//Calculate the relative path of this file
+		relPath, err := filepath.Rel(snapshotBaseFolder, filepath.ToSlash(filename))
+		if err != nil {
+			//Just skip this cycle
+			return nil
+		}
+
+		assumedRestoreLocation := filepath.ToSlash(filepath.Join(snapshotRestoreDirectory, relPath))
+		allowRestore := false
+		if username == nil {
+			//Restore all files
+			allowRestore = true
+		} else {
+			//Restore only files owned by this user
+
+			isOwnedByThisUser := snapshotFileBelongsToUser("/"+filepath.ToSlash(relPath), *username)
+			if isOwnedByThisUser {
+				allowRestore = true
+			}
+
+		}
+
+		if allowRestore {
+			//Check if the restore file parent folder exists.
+			if !fileExists(filepath.Dir(assumedRestoreLocation)) {
+				os.MkdirAll(filepath.Dir(assumedRestoreLocation), 0775)
+			}
+			//Copy this file from backup to source, overwriting source if exists
+			err := BufferedLargeFileCopy(filepath.ToSlash(filename), filepath.ToSlash(assumedRestoreLocation), 0775)
+			if err != nil {
+				log.Println("[HybridBackup] Restore failed: " + err.Error())
+			}
+		}
+
+		return nil
+	})
+
+	//Step 3: Restore files from datalinking file
+	linkMap, err := readLinkFile(snapshotBaseFolder)
+	if err != nil {
+		return err
+	}
+
+	for relPath, restorePointer := range linkMap.UnchangedFile {
+		//Get the assume restore position and source location
+		sourceFileLocation := filepath.ToSlash(filepath.Join(backupTask.DiskPath, "/version/", "/"+restorePointer+"/", relPath))
+		assumedRestoreLocation := filepath.ToSlash(filepath.Join(snapshotRestoreDirectory, relPath))
+
+		//Check if the restore file parent folder exists.
+		if snapshotFileBelongsToUser(filepath.ToSlash(relPath), *username) {
+			if !fileExists(filepath.Dir(assumedRestoreLocation)) {
+				os.MkdirAll(filepath.Dir(assumedRestoreLocation), 0775)
+			}
+			//Copy this file from backup to source, overwriting source if exists
+			BufferedLargeFileCopy(filepath.ToSlash(sourceFileLocation), filepath.ToSlash(assumedRestoreLocation), 0775)
+			log.Println("[HybridBackup] Restored " + assumedRestoreLocation + " for user " + *username)
+		}
+	}
+
+	return nil
+}

+ 49 - 6
mod/disk/hybridBackup/versionBackup.go

@@ -366,7 +366,7 @@ func listVersionRestorables(task *BackupTask) ([]*RestorableFile, error) {
 			Filename:      filepath.Base(snapshot),
 			IsHidden:      false,
 			Filesize:      0,
-			RelpathOnDisk: filepath.ToSlash(snapshot),
+			RelpathOnDisk: filepath.Base(snapshot),
 			RestorePoint:  task.ParentUID,
 			BackupDiskUID: task.DiskUID,
 			RemainingTime: -1,
@@ -381,13 +381,31 @@ func listVersionRestorables(task *BackupTask) ([]*RestorableFile, error) {
 
 }
 
-//This function generate and return a snapshot summary
-func (task *BackupTask) GenerateSnapshotSummary(snapshotName string) (*SnapshotSummary, error) {
+//Check if a file in snapshot relPath (start with /) belongs to a user
+func snapshotFileBelongsToUser(relPath string, username string) bool {
+	relPath = filepath.ToSlash(filepath.Clean(relPath))
+	userPath := "/users/" + username + "/"
+	if len(relPath) > len(userPath) && relPath[:len(userPath)] == userPath {
+		return true
+	} else {
+		return false
+	}
+}
+
+//This function generate and return a snapshot summary. For public drive, leave username as nil
+func (task *BackupTask) GenerateSnapshotSummary(snapshotName string, username *string) (*SnapshotSummary, error) {
 	//Check if the task is version
 	if task.Mode != "version" {
 		return nil, errors.New("Invalid backup mode. This function only support snapshot mode backup task.")
 	}
 
+	userSumamryMode := false
+	targetUserName := ""
+	if username != nil {
+		targetUserName = *username
+		userSumamryMode = true
+	}
+
 	//Check if the snapshot folder exists
 	targetSnapshotFolder := filepath.Join(task.DiskPath, "/version/", snapshotName)
 	if !fileExists(targetSnapshotFolder) {
@@ -414,7 +432,15 @@ func (task *BackupTask) GenerateSnapshotSummary(snapshotName string) (*SnapshotS
 			return err
 		}
 
-		summary.ChangedFiles["/"+filepath.ToSlash(relPath)] = snapshotName
+		//Check if user mode, check if folder owned by them
+		if userSumamryMode == true {
+			if snapshotFileBelongsToUser("/"+filepath.ToSlash(relPath), targetUserName) {
+				summary.ChangedFiles["/"+filepath.ToSlash(relPath)] = snapshotName
+			}
+		} else {
+			summary.ChangedFiles["/"+filepath.ToSlash(relPath)] = snapshotName
+		}
+
 		return nil
 	})
 
@@ -425,8 +451,25 @@ func (task *BackupTask) GenerateSnapshotSummary(snapshotName string) (*SnapshotS
 	}
 
 	//Move the file map into result
-	summary.UnchangedFiles = linkFileMap.UnchangedFile
-	summary.DeletedFiles = linkFileMap.DeletedFiles
+	if userSumamryMode {
+		//Only show the files that belongs to this user
+		for relPath, linkTarget := range linkFileMap.UnchangedFile {
+			if snapshotFileBelongsToUser(filepath.ToSlash(relPath), targetUserName) {
+				summary.UnchangedFiles[filepath.ToSlash(relPath)] = linkTarget
+			}
+		}
+
+		for relPath, linkTarget := range linkFileMap.DeletedFiles {
+			if snapshotFileBelongsToUser(filepath.ToSlash(relPath), targetUserName) {
+				summary.DeletedFiles[filepath.ToSlash(relPath)] = linkTarget
+			}
+		}
+
+	} else {
+		//Show all files (public mode)
+		summary.UnchangedFiles = linkFileMap.UnchangedFile
+		summary.DeletedFiles = linkFileMap.DeletedFiles
+	}
 
 	return &summary, nil
 }

BIN
web/SystemAO/desktop/img/webscript.png


BIN
web/SystemAO/desktop/img/webscript.psd


+ 31 - 6
web/SystemAO/disk/disk_restore.html

@@ -100,6 +100,15 @@
                 </div>
             </h4>
        </div>
+       <div id="restoreCancel" class="ui grey inverted segment hidden">
+            <h4 class="ui header">
+                <i class="remove icon"></i>
+                <div class="content">
+                    Snapshot Restore Cancelled
+                    <div class="sub reason header" style="color: white;">Operation cancelled by user</div>
+                </div>
+            </h4>
+        </div>  
        <div id="succ" class="ui green segment" style="display:none;">
         <h4 class="ui header">
             <i class="green checkmark icon"></i>
@@ -270,6 +279,11 @@
             filedata = JSON.parse(decodeURIComponent(filedata));
             console.log(filedata);
 
+            if (filedata.IsSnapshot == true && !confirm("Confirm snapshot restore? All newly created files will be kept.")){
+                $("#restoreCancel").stop().finish().slideDown('fast').delay(3000).slideUp('fast');
+                return;
+            }
+
             $.ajax({
                 url: "../../system/backup/restoreFile",
                 method: "POST",
@@ -279,7 +293,12 @@
                         alert("Restore failed: " + data.error);
                     }else{
                         console.log(data);
-                        restoreFileInfo = data;
+                        if (filedata.IsSnapshot){
+                            restoreFileInfo = filedata;
+                        }else{
+                            restoreFileInfo = data;
+                        }
+                       
 
                         if (restoreFileInfo.RestoredVirtualPath == ""){
                             $("#openfolder").hide();
@@ -296,7 +315,11 @@
         }
 
         function handleOpenRestoreLocation(){
-            if (restoreFileInfo.RestoredVirtualPath != undefined && restoreFileInfo.RestoredVirtualPath != ""){
+            if (restoreFileInfo.RestorePoint !== undefined){
+                //Snapshot
+                ao_module_openPath(restoreFileInfo.RestorePoint + ":/");
+                return
+            }else if (restoreFileInfo.RestoredVirtualPath != undefined && restoreFileInfo.RestoredVirtualPath != ""){
                 var filepath = restoreFileInfo.RestoredVirtualPath;
                 var tmp = filepath.split("/");
                 var filename = tmp.pop();
@@ -339,7 +362,7 @@
                         $("#snapshotList").html("");
                         restorableSnapshots.forEach(snapshot => {
                             let snapshotData = encodeURIComponent(JSON.stringify(snapshot));
-                            $("#snapshotList").append(`<div class="item restorableFile" filedata="${snapshotData}">
+                            $("#snapshotList").append(`<div class="item restorableFile snapshot" 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>
@@ -386,7 +409,7 @@
                                 isHidden = "hiddenfile";
                             }
 
-                            $("#restorableList").append(`<div class="item restorableFile ${isHidden}" filedata="${thisFileData}">
+                            $("#restorableList").append(`<div class="item restorableFile file ${isHidden}" filedata="${thisFileData}">
                                 <div class="right floated content">
                                     <div class="timeleft" title="Will be deleted in ${deleteTimeLeftHumanReadable}">
                                         <img class="ui fluid image" src="../../img/icons/backup/${timerIcon}"/>
@@ -424,8 +447,10 @@
         function restoreAll(){
             if (confirm("Confirm restore all files from backup?")){
                 $(".restorableFile").each(function(){
-                    //Restore this file
-                    restoreThisFile($(this));
+                    //Restore this file if it is not a snapshot
+                    if ($(this).hasClass("snapshot") == false){
+                        restoreThisFile($(this));
+                    }
                 });
             }
         }

+ 14 - 2
web/SystemAO/disk/disk_snapshot.html

@@ -31,7 +31,7 @@
                     <input type="checkbox" onchange="toggleUnchageFileVisibility(this.checked);">
                     <label>Show Unchanged Files</label>
                 </div>
-                <table class="ui striped celled table">
+                <table class="ui striped celled green table">
                     <thead>
                         <tr>
                         <th>#</th>
@@ -40,7 +40,7 @@
                         </tr>
                     </thead>
                     <tbody id="filelist">
-
+                        
                     </tbody>
                 </table>
             </div>
@@ -76,12 +76,15 @@
                         $("#summary").show();
                         $("#content").hide();
                         $("#filelist").html("");
+
+                        var totalChangeCount = 0;
                         for (let relpath in data.ChangedFiles){
                             $("#filelist").append(`<tr class="positive changed">
                                 <td><i class="add icon"></i></td>
                                 <td>${relpath}</td>
                                 <td>${data.ChangedFiles[relpath]}</td>
                             </tr>`);
+                            totalChangeCount++;
                         }
 
                         for (let relpath in data.DeletedFiles){
@@ -90,6 +93,15 @@
                                 <td>${relpath}</td>
                                 <td>${data.DeletedFiles[relpath]}</td>
                             </tr>`);
+                            totalChangeCount++;
+                        }
+
+                        if (totalChangeCount == 0){
+                            $("#filelist").append(`<tr class="">
+                                <td><i class="clock outline icon"></i></td>
+                                <td>No Changed / Deleted File in this snapshot</td>
+                                <td></td>
+                            </tr>`);
                         }
 
                         for (let relpath in data.UnchangedFiles){