Explorar o código

Merge branch 'master' into OAuth

AY %!s(int64=4) %!d(string=hai) anos
pai
achega
ba44b9aee5
Modificáronse 100 ficheiros con 3940 adicións e 718 borrados
  1. 6 0
      .gitignore
  2. 42 2
      AGI Documentation.md
  3. 2 1
      auth.go
  4. 204 4
      backup.go
  5. 6 0
      build.sh
  6. 7 1
      desktop.go
  7. BIN=BIN
      documents/icon/rev-2.png
  8. BIN=BIN
      documents/icon/theme.mid
  9. BIN=BIN
      documents/icon/theme.mp3
  10. BIN=BIN
      documents/icon/theme.ogg
  11. 169 15
      file_system.go
  12. 5 7
      go.mod
  13. 19 54
      go.sum
  14. 46 10
      hardware.power.go
  15. 4 2
      main.flags.go
  16. 3 3
      main.go
  17. 8 8
      main.router.go
  18. 57 52
      mod/agi/agi.file.go
  19. 4 4
      mod/agi/agi.go
  20. 1 1
      mod/agi/userFunc.go
  21. 56 28
      mod/apt/apt.go
  22. 22 5
      mod/auth/authlogger/authlogger.go
  23. 14 0
      mod/common/common.go
  24. 97 0
      mod/disk/diskcapacity/dftool/dftool.go
  25. 3 94
      mod/disk/diskcapacity/diskcapacity.go
  26. 58 40
      mod/disk/hybridBackup/basicBackup.go
  27. 1 1
      mod/disk/hybridBackup/compareRoots.go
  28. BIN=BIN
      mod/disk/hybridBackup/doc.txt
  29. 10 0
      mod/disk/hybridBackup/fileUtil.go
  30. 138 51
      mod/disk/hybridBackup/hybridBackup.go
  31. 36 0
      mod/disk/hybridBackup/linker.go
  32. 204 0
      mod/disk/hybridBackup/snaoshotOpr.go
  33. 227 48
      mod/disk/hybridBackup/versionBackup.go
  34. 29 0
      mod/filesystem/config.go
  35. 8 8
      mod/filesystem/fileOpr.go
  36. 1 1
      mod/filesystem/filesystem.go
  37. 13 0
      mod/filesystem/fsextend/fsextend.go
  38. 86 0
      mod/filesystem/fssort/fssort.go
  39. 1 0
      mod/filesystem/metadata/metadata.go
  40. 3 20
      mod/filesystem/metadata/video.go
  41. 56 0
      mod/filesystem/shortcut/shortcut.go
  42. 12 0
      mod/filesystem/static.go
  43. 1 1
      mod/info/hardwareinfo/sysinfo.go
  44. 235 0
      mod/info/hardwareinfo/sysinfo_darwin.go
  45. 63 7
      mod/info/usageinfo/usageinfo.go
  46. 2 0
      mod/modules/module.go
  47. 21 0
      mod/network/dynamicproxy/dpcore/LICENSE
  48. 394 0
      mod/network/dynamicproxy/dpcore/dpcore.go
  49. 195 0
      mod/network/dynamicproxy/dynamicproxy.go
  50. 99 0
      mod/network/dynamicproxy/proxyRequestHandler.go
  51. 44 0
      mod/network/dynamicproxy/subdomain.go
  52. 9 6
      mod/share/share.go
  53. 115 0
      mod/storage/bridge/bridge.go
  54. 44 0
      mod/storage/du/diskusage.go
  55. 17 0
      mod/storage/du/diskusage_test.go
  56. 55 0
      mod/storage/du/diskusage_windows.go
  57. 1 1
      mod/storage/ftp/aofs.go
  58. 7 1
      mod/storage/ftp/drivers.go
  59. 6 8
      mod/storage/static.go
  60. 15 3
      mod/storage/storage.go
  61. 3 1
      mod/storage/webdav/webdav.go
  62. 6 16
      mod/subservice/subservice.go
  63. 1 1
      mod/time/scheduler/scheduler.go
  64. 13 45
      mod/user/directoryHandler.go
  65. 52 0
      mod/user/internal.go
  66. 0 27
      mod/user/user.go
  67. 31 0
      mod/user/useropr.go
  68. 1 1
      oauth.go
  69. 164 0
      reverseproxy.go
  70. 0 2
      start.sh.backup
  71. 2 2
      startup.flags.go
  72. 14 12
      startup.go
  73. 108 0
      storage.bridge.go
  74. 51 1
      storage.go
  75. 151 32
      storage.pool.go
  76. 1 1
      subservice.go
  77. 0 0
      subservice/WsTTY/.disabled
  78. 56 14
      system.info.go
  79. 5 3
      system/auth/register.system
  80. 2 2
      system/reset/resetCodeTemplate.html
  81. 2 2
      system/reset/resetPasswordTemplate.html
  82. 3 0
      system/share/audio.html
  83. 5 1
      system/share/downloadPage.html
  84. 47 7
      system/share/downloadPageFolder.html
  85. 7 1
      system/share/image.html
  86. 6 0
      system/share/video.html
  87. 13 0
      user.go
  88. 2 2
      web/FFmpeg Factory/backend/convert.js
  89. 9 9
      web/FFmpeg Factory/config/i2i.json
  90. 4 4
      web/FFmpeg Factory/config/other.json
  91. 9 9
      web/FFmpeg Factory/config/v2a.json
  92. 14 14
      web/FFmpeg Factory/config/v2v.json
  93. 1 1
      web/MDEditor/mde.html
  94. 1 1
      web/Music/embedded.html
  95. 1 1
      web/Music/functions/getMeta.js
  96. 104 19
      web/Photo/embedded.html
  97. 27 0
      web/Photo/embedded/arrow-left.ai
  98. 8 0
      web/Photo/embedded/arrow-left.svg
  99. 27 0
      web/Photo/embedded/arrow-right.ai
  100. 8 0
      web/Photo/embedded/arrow-right.svg

+ 6 - 0
.gitignore

@@ -25,6 +25,7 @@ system/network/wifi/ap/*
 system/storage.json
 system/dev.uuid
 system/storage.json
+system/bridge.json
 system/storage/*.json
 system/cron.json
 
@@ -54,3 +55,8 @@ arozos.exe
 */arozos.exe
 arozos
 upx.exe
+
+#Script related
+start.sh.backup
+*.backup
+system/bridge.json

+ 42 - 2
AGI Documentation.md

@@ -434,8 +434,8 @@ if (!requirelib("filelib")){
 	filelib.deleteFile("user:/Desktop/test.txt"); 						//Delete a file by given path
 	filelib.readdir("user:/Desktop/"); 									//List all subdirectories within this directory
 	filelib.walk("user:/Desktop/"); 									//Recursive scan dir and return all files and folder in subdirs
-	filelib.glob("user:/Desktop/*.jpg");
-	filelib.aglob("user:/Desktop/*.jpg");
+	filelib.glob("user:/Desktop/*.jpg", "smallToLarge");
+	filelib.aglob("user:/Desktop/*.jpg", "user");
 	filelib.filesize("user:/Desktop/test.jpg");
 	filelib.fileExists("user:/Desktop/test.jpg");
 	filelib.isDir("user:/Desktop/NewFolder/");
@@ -445,8 +445,48 @@ if (!requirelib("filelib")){
 	filelib.rname("user:/Deskop"); 										//Get Rootname, return "User"
 ```
 
+##### Special sorting mode for glob and aglob
+
+For glob and aglob, developer can pass in the following sorting modes (case sensitive)
+
+- default
+- reverse
+- smallToLarge
+- largeToSmall
+- mostRecent
+- leastRecent
+
+```
+//Example for sorting the desktop files to largeToSmall
+filelib.aglob("user:/Desktop/*", "largeToSmall");
+```
+
+To use the user default option which user has set in File Manager WebApp, pass in "user". Default sorting method is "default"
+
+```
+//Example of using user's selected mode
+filelib.aglob("user:/Desktop/*.jpg", "user");
+```
+
+### appdata
+
+An API for access files inside the web folder. This API only provide read only functions. Include the appdata lib as follows.
+
+```
+requirelib("appdata");
+```
+
+#### appdata functions
+
+```
+appdata.readFile("UnitTest/appdata.txt"); //Return false (boolean) if read failed
+appdata.listDir("UnitTest/backend/"); //Return a list of files in JSON string
+```
+
+
 
 ### imagelib
+
 A basic image handling library to process images. Allowing basic image resize,
 get image dimension and others (to be expanded)
 

+ 2 - 1
auth.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 
 	auth "imuslab.com/arozos/mod/auth"
+	"imuslab.com/arozos/mod/common"
 	prout "imuslab.com/arozos/mod/prouter"
 )
 
@@ -33,7 +34,7 @@ func AuthInit() {
 	authAgent = auth.NewAuthenticationAgent("ao_auth", []byte(*session_key), sysdb, *allow_public_registry, func(w http.ResponseWriter, r *http.Request) {
 		//Login Redirection Handler, redirect it login.system
 		w.Header().Set("Cache-Control", "no-cache, no-store, no-transform, must-revalidate, private, max-age=0")
-		http.Redirect(w, r, "/login.system?redirect="+r.URL.Path, 307)
+		http.Redirect(w, r, common.ConstructRelativePathFromRequestURL(r.RequestURI, "login.system")+"?redirect="+r.URL.Path, 307)
 	})
 
 	if *allow_autologin == true {

+ 204 - 4
backup.go

@@ -2,10 +2,13 @@ package main
 
 import (
 	"encoding/json"
+	"errors"
 	"net/http"
 	"path/filepath"
+	"strings"
 
 	"imuslab.com/arozos/mod/disk/hybridBackup"
+	user "imuslab.com/arozos/mod/user"
 
 	prout "imuslab.com/arozos/mod/prouter"
 )
@@ -23,6 +26,166 @@ func backup_init() {
 	//Register API endpoints
 	router.HandleFunc("/system/backup/listRestorable", backup_listRestorable)
 	router.HandleFunc("/system/backup/restoreFile", backup_restoreSelected)
+	router.HandleFunc("/system/backup/snapshotSummary", backup_renderSnapshotSummary)
+	router.HandleFunc("/system/backup/listAll", backup_listAllBackupDisk)
+
+	//Register settings
+	registerSetting(settingModule{
+		Name:         "Backup Disks",
+		Desc:         "All backup disk in the system",
+		IconPath:     "img/system/backup.svg",
+		Group:        "Disk",
+		StartDir:     "SystemAO/disk/backup/backups.html",
+		RequireAdmin: true,
+	})
+}
+
+//List all backup disk info
+func backup_listAllBackupDisk(w http.ResponseWriter, r *http.Request) {
+	//Get all fsh from the system
+	runningBackupTasks := []*hybridBackup.BackupTask{}
+
+	//Render base storage pool
+	for _, fsh := range baseStoragePool.Storages {
+		if fsh.Hierarchy == "backup" {
+			task, err := baseStoragePool.HyperBackupManager.GetTaskByBackupDiskID(fsh.UUID)
+			if err != nil {
+				continue
+			}
+
+			runningBackupTasks = append(runningBackupTasks, task)
+		}
+	}
+
+	//Render group storage pool
+	for _, pg := range permissionHandler.PermissionGroups {
+		for _, fsh := range pg.StoragePool.Storages {
+			task, err := pg.StoragePool.HyperBackupManager.GetTaskByBackupDiskID(fsh.UUID)
+			if err != nil {
+				continue
+			}
+
+			runningBackupTasks = append(runningBackupTasks, task)
+		}
+	}
+
+	type backupDrive struct {
+		DiskUID             string //The backup disk UUID
+		DiskName            string // The Backup disk name
+		ParentUID           string //Parent disk UID
+		ParentName          string //Parent disk name
+		BackupMode          string //The backup mode of the drive
+		LastBackupCycleTime int64  //Last backup timestamp
+		BackupCycleCount    int64  //How many backup cycle has proceeded since the system startup
+		Error               bool   //If there are error occured in the last cycle
+		ErrorMessage        string //If there are any error msg
+	}
+
+	backupDrives := []*backupDrive{}
+	for _, task := range runningBackupTasks {
+		diskFsh, diskErr := GetFsHandlerByUUID(task.DiskUID)
+		parentFsh, parentErr := GetFsHandlerByUUID(task.ParentUID)
+
+		//Check for error in getting FS Handler
+		if diskErr != nil || parentErr != nil {
+			sendErrorResponse(w, "Unable to get backup task info from backup disk: "+task.DiskUID)
+			return
+		}
+
+		thisBackupDrive := backupDrive{
+			DiskUID:             diskFsh.UUID,
+			DiskName:            diskFsh.Name,
+			ParentUID:           parentFsh.UUID,
+			ParentName:          parentFsh.Name,
+			BackupMode:          task.Mode,
+			LastBackupCycleTime: task.LastCycleTime,
+			BackupCycleCount:    task.CycleCounter,
+			Error:               task.PanicStopped,
+			ErrorMessage:        task.ErrorMessage,
+		}
+
+		backupDrives = append(backupDrives, &thisBackupDrive)
+	}
+
+	js, _ := json.Marshal(backupDrives)
+	sendJSONResponse(w, string(js))
+}
+
+//Generate a snapshot summary for vroot
+func backup_renderSnapshotSummary(w http.ResponseWriter, r *http.Request) {
+	//Get user accessiable storage pools
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	//Get Backup disk ID from request
+	bdid, err := mv(r, "bdid", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid backup disk ID given")
+		return
+	}
+
+	//Get target snapshot name from request
+	snapshot, err := mv(r, "snapshot", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid snapshot name given")
+		return
+	}
+
+	//Get fsh from the id
+	fsh, err := GetFsHandlerByUUID(bdid)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		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 {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	if task.Mode == "version" {
+		//Generate snapshot summary
+		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)
+		sendJSONResponse(w, string(js))
+	} else {
+		sendErrorResponse(w, "Unable to genreate snapshot summary: Backup mode is not snapshot")
+		return
+	}
+
 }
 
 //Restore a given file
@@ -55,8 +218,15 @@ func backup_restoreSelected(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	//Pick the correct HybridBackup Manager
+	targetHybridBackupManager, err := backup_pickHybridBackupManager(userinfo, fsh.UUID)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
 	//Handle restore of the file
-	err = userinfo.HomeDirectories.HyperBackupManager.HandleRestore(fsh.UUID, relpath)
+	err = targetHybridBackupManager.HandleRestore(fsh.UUID, relpath, &userinfo.Username)
 	if err != nil {
 		sendErrorResponse(w, err.Error())
 		return
@@ -73,7 +243,7 @@ func backup_restoreSelected(w http.ResponseWriter, r *http.Request) {
 	}
 
 	//Get access path for this file
-	parentDiskId, err := userinfo.HomeDirectories.HyperBackupManager.GetParentDiskIDByRestoreDiskID(fsh.UUID)
+	parentDiskId, err := targetHybridBackupManager.GetParentDiskIDByRestoreDiskID(fsh.UUID)
 	if err != nil {
 		//Unable to get parent disk ID???
 
@@ -96,6 +266,31 @@ func backup_restoreSelected(w http.ResponseWriter, r *http.Request) {
 	sendJSONResponse(w, string(js))
 }
 
+//As one user might be belongs to multiple groups, check which storage pool is this disk ID owned by and return its corect backup maanger
+func backup_pickHybridBackupManager(userinfo *user.User, diskID string) (*hybridBackup.Manager, error) {
+	//Filter out the :/ if it exists in the disk ID
+	if strings.Contains(diskID, ":") {
+		diskID = strings.Split(diskID, ":")[0]
+	}
+
+	//Get all backup managers that this user ac can access
+	userpg := userinfo.GetUserPermissionGroup()
+
+	if userinfo.HomeDirectories.ContainDiskID(diskID) {
+		return userinfo.HomeDirectories.HyperBackupManager, nil
+	}
+
+	//Extract the backup Managers
+	for _, pg := range userpg {
+		if pg.StoragePool.ContainDiskID(diskID) {
+			return pg.StoragePool.HyperBackupManager, nil
+		}
+
+	}
+
+	return nil, errors.New("Disk ID not found in any storage pool this user can access")
+}
+
 //Generate and return a restorable report
 func backup_listRestorable(w http.ResponseWriter, r *http.Request) {
 	//Get user accessiable storage pools
@@ -119,10 +314,15 @@ func backup_listRestorable(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	userBackupManager := userinfo.HomeDirectories.HyperBackupManager
+	//Get all backup managers that this user ac can access
+	targetBackupManager, err := backup_pickHybridBackupManager(userinfo, vroot)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
 
 	//Get the user's storage pool and list restorable by the user's storage pool access
-	restorableReport, err := userBackupManager.ListRestorable(fsh.UUID)
+	restorableReport, err := targetBackupManager.ListRestorable(fsh.UUID)
 	if err != nil {
 		sendErrorResponse(w, err.Error())
 		return

+ 6 - 0
build.sh

@@ -43,8 +43,14 @@ rm ../aroz_online_autorelease/system/ao.db
 mv ../aroz_online_autorelease/system/storage.json ../aroz_online_autorelease/system/storage.json.example
 rm -rf ../aroz_online_autorelease/system/aecron/
 rm ../aroz_online_autorelease/system/cron.json
+rm ../aroz_online_autorelease/system/bridge.json
 rm ../aroz_online_autorelease/system/auth/authlog.db
 
+#Remove modules that should not go into the build folder
+rm -rf "../aroz_online_autorelease/web/Cyinput"
+rm -rf "../aroz_online_autorelease/system/Label Maker"
+
+
 echo "Creating tarball for all required files"
 cd ../aroz_online_autorelease/
 rm web.tar.gz

+ 7 - 1
desktop.go

@@ -556,6 +556,7 @@ func desktop_theme_handler(w http.ResponseWriter, r *http.Request) {
 func desktop_preference_handler(w http.ResponseWriter, r *http.Request) {
 	preferenceType, _ := mv(r, "preference", false)
 	value, _ := mv(r, "value", false)
+	remove, _ := mv(r, "remove", false)
 	username, err := authAgent.GetUserName(w, r)
 	if err != nil {
 		//user not logged in. Redirect to login page.
@@ -566,13 +567,18 @@ func desktop_preference_handler(w http.ResponseWriter, r *http.Request) {
 		//Invalid options. Return error reply.
 		sendTextResponse(w, "Error. Undefined paramter.")
 		return
-	} else if preferenceType != "" && value == "" {
+	} else if preferenceType != "" && value == "" && remove == "" {
 		//Getting config from the key.
 		result := ""
 		sysdb.Read("desktop", username+"/preference/"+preferenceType, &result)
 		jsonString, _ := json.Marshal(result)
 		sendJSONResponse(w, string(jsonString))
 		return
+	} else if preferenceType != "" && value == "" && remove == "true" {
+		//Remove mode
+		sysdb.Delete("desktop", username+"/preference/"+preferenceType)
+		sendOK(w)
+		return
 	} else if preferenceType != "" && value != "" {
 		//Setting config from the key
 		sysdb.Write("desktop", username+"/preference/"+preferenceType, value)

BIN=BIN
documents/icon/rev-2.png


BIN=BIN
documents/icon/theme.mid


BIN=BIN
documents/icon/theme.mp3


BIN=BIN
documents/icon/theme.ogg


+ 169 - 15
file_system.go

@@ -29,6 +29,7 @@ import (
 	fsp "imuslab.com/arozos/mod/filesystem/fspermission"
 	hidden "imuslab.com/arozos/mod/filesystem/hidden"
 	metadata "imuslab.com/arozos/mod/filesystem/metadata"
+	"imuslab.com/arozos/mod/filesystem/shortcut"
 	module "imuslab.com/arozos/mod/modules"
 	prout "imuslab.com/arozos/mod/prouter"
 	"imuslab.com/arozos/mod/share"
@@ -91,6 +92,10 @@ func FileSystemInit() {
 	//Thumbnail caching functions
 	router.HandleFunc("/system/file_system/handleFolderCache", system_fs_handleFolderCache)
 	router.HandleFunc("/system/file_system/handleCacheRender", system_fs_handleCacheRender)
+	router.HandleFunc("/system/file_system/loadThumbnail", system_fs_handleThumbnailLoad)
+
+	//Directory specific config
+	router.HandleFunc("/system/file_system/sortMode", system_fs_handleFolderSortModePreference)
 
 	//Register the module
 	moduleHandler.RegisterModule(module.ModuleInfo{
@@ -146,6 +151,13 @@ func FileSystemInit() {
 		panic(err)
 	}
 
+	//Create new table for sort preference
+	err = sysdb.NewTable("fs-sortpref")
+	if err != nil {
+		log.Println("Failed to create table for file system")
+		panic(err)
+	}
+
 	//Create a RenderHandler for caching thumbnails
 	thumbRenderHandler = metadata.NewRenderHandler()
 
@@ -370,6 +382,12 @@ func system_fs_handleLowMemoryUpload(w http.ResponseWriter, r *http.Request) {
 	//Start websocket connection
 	var upgrader = websocket.Upgrader{}
 	c, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		log.Println("Failed to upgrade websocket connection: ", err.Error())
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 WebSocket upgrade failed"))
+		return
+	}
 	defer c.Close()
 
 	//Handle WebSocket upload
@@ -464,7 +482,6 @@ func system_fs_handleLowMemoryUpload(w http.ResponseWriter, r *http.Request) {
 		c.Close()
 		return
 	}
-	defer out.Close()
 
 	for _, filesrc := range chunkName {
 		srcChunkReader, err := os.Open(filesrc)
@@ -477,6 +494,22 @@ func system_fs_handleLowMemoryUpload(w http.ResponseWriter, r *http.Request) {
 		srcChunkReader.Close()
 	}
 
+	out.Close()
+
+	//Check if the size fit in user quota
+	fi, err := os.Stat(targetUploadLocation)
+	if err != nil {
+		// Could not obtain stat, handle error
+		log.Println("Failed to validate uploaded file: ", targetUploadLocation, ". Error Message: ", err.Error())
+		c.WriteMessage(1, []byte(`{\"error\":\"Failed to validate uploaded file\"}`))
+		return
+	}
+
+	if !userinfo.StorageQuota.HaveSpace(fi.Size()) {
+		c.WriteMessage(1, []byte(`{\"error\":\"User Storage Quota Exceeded\"}`))
+		return
+	}
+
 	//Set owner of the new uploaded file
 	userinfo.SetOwnerOfFile(targetUploadLocation)
 
@@ -487,7 +520,7 @@ func system_fs_handleLowMemoryUpload(w http.ResponseWriter, r *http.Request) {
 	done <- true
 
 	//Clear the tmp folder
-	time.Sleep(1 * time.Second)
+	time.Sleep(300 * time.Millisecond)
 	err = os.RemoveAll(uploadFolder)
 	if err != nil {
 		log.Println(err)
@@ -495,7 +528,7 @@ func system_fs_handleLowMemoryUpload(w http.ResponseWriter, r *http.Request) {
 
 	//Close WebSocket connection after finished
 	c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
-	time.Sleep(1 * time.Second)
+	time.Sleep(300 * time.Second)
 	c.Close()
 
 }
@@ -573,7 +606,7 @@ func system_fs_handleUpload(w http.ResponseWriter, r *http.Request) {
 	//Check for storage quota
 	uploadFileSize := handler.Size
 	if !userinfo.StorageQuota.HaveSpace(uploadFileSize) {
-		sendErrorResponse(w, "Storage Quota Full")
+		sendErrorResponse(w, "User Storage Quota Exceeded")
 		return
 	}
 
@@ -675,7 +708,7 @@ func system_fs_validateFileOpr(w http.ResponseWriter, r *http.Request) {
 	}
 	vsrcFiles, _ := mv(r, "src", true)
 	vdestFile, _ := mv(r, "dest", true)
-	var duplicateFiles []string
+	var duplicateFiles []string = []string{}
 
 	//Loop through all files are see if there are duplication during copy and paste
 	sourceFiles := []string{}
@@ -689,7 +722,7 @@ func system_fs_validateFileOpr(w http.ResponseWriter, r *http.Request) {
 	rdestFile, _ := userinfo.VirtualPathToRealPath(vdestFile)
 	for _, file := range sourceFiles {
 		rsrcFile, _ := userinfo.VirtualPathToRealPath(string(file))
-		if fileExists(rdestFile + filepath.Base(rsrcFile)) {
+		if fileExists(filepath.Join(rdestFile, filepath.Base(rsrcFile))) {
 			//File exists already.
 			vpath, _ := userinfo.RealPathToVirtualPath(rsrcFile)
 			duplicateFiles = append(duplicateFiles, vpath)
@@ -725,8 +758,19 @@ func system_fs_WebSocketScanTrashBin(w http.ResponseWriter, r *http.Request) {
 	scanningRoots := []string{}
 	//Get all roots to scan
 	for _, storage := range userinfo.GetAllFileSystemHandler() {
-		storageRoot := storage.Path
-		scanningRoots = append(scanningRoots, storageRoot)
+		if storage.Hierarchy == "backup" {
+			//Skip this fsh
+			continue
+		}
+
+		if storage.Hierarchy == "user" {
+			storageRoot := filepath.ToSlash(filepath.Join(storage.Path, "users", userinfo.Username))
+			scanningRoots = append(scanningRoots, storageRoot)
+		} else {
+			storageRoot := storage.Path
+			scanningRoots = append(scanningRoots, storageRoot)
+		}
+
 	}
 
 	for _, rootPath := range scanningRoots {
@@ -917,8 +961,19 @@ func system_fs_listTrash(username string) ([]string, error) {
 	scanningRoots := []string{}
 	//Get all roots to scan
 	for _, storage := range userinfo.GetAllFileSystemHandler() {
-		storageRoot := storage.Path
-		scanningRoots = append(scanningRoots, storageRoot)
+		if storage.Hierarchy == "backup" {
+			//Skip this fsh
+			continue
+		}
+
+		if storage.Hierarchy == "user" {
+			storageRoot := filepath.ToSlash(filepath.Join(storage.Path, "users", userinfo.Username))
+			scanningRoots = append(scanningRoots, storageRoot)
+		} else {
+			storageRoot := storage.Path
+			scanningRoots = append(scanningRoots, storageRoot)
+		}
+
 	}
 
 	files := []string{}
@@ -955,6 +1010,13 @@ func system_fs_handleNewObjects(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	//Validate the token
+	tokenValid := CSRFTokenManager.HandleTokenValidation(w, r)
+	if !tokenValid {
+		http.Error(w, "Invalid CSRF token", 401)
+		return
+	}
+
 	fileType, _ := mv(r, "type", true)     //File creation type, {file, folder}
 	vsrc, _ := mv(r, "src", true)          //Virtual file source folder, do not include filename
 	filename, _ := mv(r, "filename", true) //Filename for the new file
@@ -1121,7 +1183,7 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 
 	//Permission checking
 	if !userinfo.CanWrite(vdestFile) {
-		log.Println(vdestFile)
+		log.Println("Access denied for " + userinfo.Username + " try to access " + vdestFile)
 		w.WriteHeader(http.StatusForbidden)
 		w.Write([]byte("403 - Access Denied"))
 		return
@@ -2085,9 +2147,13 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 		if filepath.Clean(realpath) == filepath.Clean(userRoot) {
 			//Initiate user folder (Initiaed in user object)
 			userinfo.GetHomeDirectory()
+		} else if !strings.Contains(filepath.ToSlash(filepath.Clean(currentDir)), "/") {
+			//User root not created. Create the root folder
+			os.MkdirAll(filepath.Clean(realpath), 0775)
 		} else {
 			//Folder not exists
-			sendJSONResponse(w, "{\"error\":\"Folder not exists\"}")
+			log.Println("[File Explorer] Requested path: ", realpath, " does not exists!")
+			sendErrorResponse(w, "Folder not exists")
 			return
 		}
 
@@ -2100,12 +2166,30 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 	files, _ := system_fs_specialGlob(filepath.Clean(realpath) + "/*")
 
 	var parsedFilelist []fs.FileData
-
+	var shortCutInfo *shortcut.ShortcutData = nil
 	for _, v := range files {
-		if showHidden != "true" && filepath.Base(v)[:1] == "." {
+		//Check if it is hidden file
+		isHidden, _ := hidden.IsHidden(v, false)
+		if showHidden != "true" && isHidden {
 			//Skipping hidden files
 			continue
 		}
+
+		//Check if this is an aodb file
+		if filepath.Base(v) == "aofs.db" || filepath.Base(v) == "aofs.db.lock" {
+			//Database file (reserved)
+			continue
+		}
+
+		//Check if it is shortcut file. If yes, render a shortcut data struct
+		if filepath.Ext(v) == ".shortcut" {
+			//This is a shortcut file
+			shorcutData, err := shortcut.ReadShortcut(v)
+			if err == nil {
+				shortCutInfo = shorcutData
+			}
+		}
+
 		rawsize := fs.GetFileSize(v)
 		modtime, _ := fs.GetModTime(v)
 		thisFile := fs.FileData{
@@ -2117,6 +2201,7 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 			Displaysize: fs.GetFileDisplaySize(rawsize, 2),
 			ModTime:     modtime,
 			IsShared:    shareManager.FileIsShared(v),
+			Shortcut:    shortCutInfo,
 		}
 
 		parsedFilelist = append(parsedFilelist, thisFile)
@@ -2336,6 +2421,31 @@ func system_fs_handleCacheRender(w http.ResponseWriter, r *http.Request) {
 
 	//Perform cache rendering
 	thumbRenderHandler.HandleLoadCache(w, r, rpath)
+}
+
+//Handle loading of one thumbnail
+func system_fs_handleThumbnailLoad(w http.ResponseWriter, r *http.Request) {
+	userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
+	vpath, err := mv(r, "vpath", false)
+	if err != nil {
+		sendErrorResponse(w, "vpath not defined")
+		return
+	}
+
+	rpath, err := userinfo.VirtualPathToRealPath(vpath)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	thumbnailPath, err := thumbRenderHandler.LoadCache(rpath, false)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	js, _ := json.Marshal(thumbnailPath)
+	sendJSONResponse(w, string(js))
 
 }
 
@@ -2355,10 +2465,54 @@ func system_fs_handleFolderCache(w http.ResponseWriter, r *http.Request) {
 	}
 
 	thumbRenderHandler.BuildCacheForFolder(rpath)
-
 	sendOK(w)
 }
 
+//Handle the get and set of sort mode of a particular folder
+func system_fs_handleFolderSortModePreference(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+	folder, err := mv(r, "folder", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid folder given")
+		return
+	}
+
+	opr, _ := mv(r, "opr", true)
+
+	folder = filepath.ToSlash(filepath.Clean(folder))
+
+	if opr == "" || opr == "get" {
+		sortMode := "default"
+		if sysdb.KeyExists("fs-sortpref", userinfo.Username+"/"+folder) {
+			sysdb.Read("fs-sortpref", userinfo.Username+"/"+folder, &sortMode)
+		}
+
+		js, _ := json.Marshal(sortMode)
+		sendJSONResponse(w, string(js))
+	} else if opr == "set" {
+		sortMode, err := mv(r, "mode", true)
+		if err != nil {
+			sendErrorResponse(w, "Invalid sort mode given")
+			return
+		}
+
+		if !stringInSlice(sortMode, []string{"default", "reverse", "smallToLarge", "largeToSmall", "mostRecent", "leastRecent"}) {
+			sendErrorResponse(w, "Not supported sort mode: "+sortMode)
+			return
+		}
+
+		sysdb.Write("fs-sortpref", userinfo.Username+"/"+folder, sortMode)
+		sendOK(w)
+	} else {
+		sendErrorResponse(w, "Invalid opr mode")
+		return
+	}
+}
+
 //Handle setting and loading of file permission on Linux
 func system_fs_handleFilePermission(w http.ResponseWriter, r *http.Request) {
 	file, err := mv(r, "file", true)

+ 5 - 7
go.mod

@@ -5,7 +5,6 @@ go 1.13
 require (
 	github.com/andybalholm/brotli v1.0.0 // indirect
 	github.com/boltdb/bolt v1.3.1
-	github.com/brutella/hc v1.2.4
 	github.com/dhowden/tag v0.0.0-20200828214007-46e57f75dbfc
 	github.com/disintegration/imaging v1.6.2
 	github.com/fclairamb/ftpserverlib v0.8.0
@@ -14,11 +13,9 @@ require (
 	github.com/frankban/quicktest v1.10.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.1.0
 	github.com/go-git/go-git/v5 v5.2.0
-	github.com/gorilla/schema v1.2.0
 	github.com/gorilla/sessions v1.2.0
 	github.com/gorilla/websocket v1.4.2
 	github.com/grandcat/zeroconf v1.0.0
-	github.com/jung-kurt/gofpdf v1.16.2
 	github.com/klauspost/compress v1.10.6 // indirect
 	github.com/klauspost/pgzip v1.2.4 // indirect
 	github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d
@@ -28,20 +25,21 @@ require (
 	github.com/nwaples/rardecode v1.1.0 // indirect
 	github.com/oliamb/cutter v0.2.2
 	github.com/pierrec/lz4 v2.5.2+incompatible // indirect
-	github.com/ricochet2200/go-disk-usage v0.0.0-20150921141558-f0d1b743428f
 	github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff
 	github.com/satori/go.uuid v1.2.0
 	github.com/smartystreets/cproxy v1.0.2
 	github.com/smartystreets/logging v1.1.1 // indirect
 	github.com/spf13/afero v1.3.1
+	github.com/stretchr/testify v1.7.0 // indirect
 	github.com/tidwall/pretty v1.0.2
-	github.com/ulikunitz/xz v0.5.7 // indirect
+	github.com/ulikunitz/xz v0.5.10 // indirect
 	github.com/valyala/fasttemplate v1.1.0
 	gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
 	gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3
-	golang.org/x/net v0.0.0-20210119194325-5f4716e94777
+	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
+	golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
 	golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1
 	golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
-	golang.org/x/text v0.3.3 // indirect
+	golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 // indirect
 	gopkg.in/sourcemap.v1 v1.0.5 // indirect
 )

+ 19 - 54
go.sum

@@ -45,7 +45,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
 github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
 github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
 github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
@@ -68,11 +67,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
-github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
-github.com/brutella/dnssd v1.2.0 h1:bgrSycmZ2+u4BoJxRf1BzSlnViSAfeXWVdujqjLA004=
-github.com/brutella/dnssd v1.2.0/go.mod h1:FpJqlQ8+XU6w1vbnG1zJiQPTRE5fvQIRdrcBojMVuuQ=
-github.com/brutella/hc v1.2.4 h1:dQjLi4bjUbKG4436N7WXH6W7iHQgfnCceE9DxyOuSnA=
-github.com/brutella/hc v1.2.4/go.mod h1:TPPdombm3gA/2fsSON6ct2km7z7Vi8lQNqE+fzuDHQM=
 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@@ -160,7 +154,6 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
 github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -196,10 +189,10 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -219,8 +212,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
-github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
 github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
@@ -272,20 +263,15 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
-github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
-github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
 github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
 github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCWIWY=
 github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.10.6 h1:SP6zavvTG3YjOosWePXFDlExpKIWMTO4SE/Y8MZB2vI=
 github.com/klauspost/compress v1.10.6/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
 github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
 github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -297,9 +283,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
@@ -314,9 +298,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
 github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=
 github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
 github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
 github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
 github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@@ -345,7 +326,6 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
 github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
 github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
@@ -370,9 +350,7 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
-github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
-github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
 github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -402,14 +380,11 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/ricochet2200/go-disk-usage v0.0.0-20150921141558-f0d1b743428f h1:w4VLAgWDnrcBDFSi8Ppn/MrB/Z1A570+MV90CvMtVVA=
-github.com/ricochet2200/go-disk-usage v0.0.0-20150921141558-f0d1b743428f/go.mod h1:yhevTRDiduxPJHQDCtlqUn53ojFPkRh/mKhMUzQUCpc=
 github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff h1:+6NUiITWwE5q1KO6SAfUX918c+Tab0+tGAM/mtdlUyA=
 github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@@ -430,7 +405,6 @@ github.com/smartystreets/cproxy v1.0.2/go.mod h1:qDRe8RO2GQUwJ3rt8wukdFrb5hcaI0Y
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/smartystreets/gunit v1.1.3 h1:32x+htJCu3aMswhPw3teoJ+PnWPONqdNgaGs6Qt8ZaU=
 github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
-github.com/smartystreets/logging v1.0.2 h1:ScLtqaKBnjIyTZw218wCALtb4RMMISWsk9xz1QP7EjM=
 github.com/smartystreets/logging v1.0.2/go.mod h1:66odR4LeIkc7lNzxhS+ktkvoSUhsyTM4e0NSXI9Bmac=
 github.com/smartystreets/logging v1.1.1 h1:4UlnyYWB7LDd216NTuP3zTVvMQZREtPrDnJbsz0zftI=
 github.com/smartystreets/logging v1.1.1/go.mod h1:NwFCEPbtiTIug+UCtTbDVjgcJNfandE3brDx0x7QEY8=
@@ -447,17 +421,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1 h1:ms/IQpkxq+t7hWpgKqCE5KjAUQWC24mqBrnL566SWgE=
-github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
 github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
 github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
-github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
-github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
+github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -468,8 +440,6 @@ github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70
 github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
-github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed h1:Gjnw8buhv4V8qXaHtAWPnKXNpCNx62heQpjO8lOY0/M=
-github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -502,13 +472,11 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -521,7 +489,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -564,12 +531,10 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -579,8 +544,9 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -592,9 +558,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -621,7 +585,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -637,21 +600,21 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo=
+golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -705,8 +668,8 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -814,6 +777,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 46 - 10
hardware.power.go

@@ -1,34 +1,44 @@
 package main
 
 import (
-	"net/http"
 	"log"
+	"net/http"
 
 	"os/exec"
 	"runtime"
 )
 
-func HardwarePowerInit(){
-	if (*allow_hardware_management){
+func HardwarePowerInit() {
+	if *allow_hardware_management {
 		//Only register these paths when hardware management is enabled
 		http.HandleFunc("/system/power/shutdown", hardware_power_poweroff)
 		http.HandleFunc("/system/power/restart", hardware_power_restart)
+
+		//Register a power handler in system setting menu
+		registerSetting(settingModule{
+			Name:         "Power",
+			Desc:         "Set the power state of the host device",
+			IconPath:     "SystemAO/boot/img/boot.png",
+			Group:        "Info",
+			StartDir:     "SystemAO/boot/poweroff.html",
+			RequireAdmin: true,
+		})
 	}
 
 	http.HandleFunc("/system/power/accessCheck", hardware_power_checkIfHardware)
 }
 
-func hardware_power_checkIfHardware(w http.ResponseWriter, r *http.Request){
-	if (*allow_hardware_management){
+func hardware_power_checkIfHardware(w http.ResponseWriter, r *http.Request) {
+	if *allow_hardware_management {
 		sendJSONResponse(w, "true")
-	}else{
+	} else {
 		sendJSONResponse(w, "false")
 	}
 }
 
 func hardware_power_poweroff(w http.ResponseWriter, r *http.Request) {
-	userinfo, err := userHandler.GetUserInfoFromRequest(w,r)
-	if err != nil{
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
 		w.WriteHeader(http.StatusUnauthorized)
 		w.Write([]byte("401 Unauthorized"))
 		return
@@ -44,6 +54,19 @@ func hardware_power_poweroff(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	//Double check password for this user
+	password, err := mv(r, "password", true)
+	if err != nil {
+		sendErrorResponse(w, "Password Incorrect")
+		return
+	}
+
+	passwordCorrect := authAgent.ValidateUsernameAndPassword(userinfo.Username, password)
+	if !passwordCorrect {
+		sendErrorResponse(w, "Password Incorrect")
+		return
+	}
+
 	if runtime.GOOS == "windows" {
 		//Only allow Linux to do power operation
 		cmd := exec.Command("shutdown", "-s", "-t", "20")
@@ -81,8 +104,8 @@ func hardware_power_poweroff(w http.ResponseWriter, r *http.Request) {
 }
 
 func hardware_power_restart(w http.ResponseWriter, r *http.Request) {
-	userinfo, err := userHandler.GetUserInfoFromRequest(w,r)
-	if err != nil{
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
 		w.WriteHeader(http.StatusUnauthorized)
 		w.Write([]byte("401 Unauthorized"))
 		return
@@ -98,6 +121,19 @@ func hardware_power_restart(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	//Double check password for this user
+	password, err := mv(r, "password", true)
+	if err != nil {
+		sendErrorResponse(w, "Password Incorrect")
+		return
+	}
+
+	passwordCorrect := authAgent.ValidateUsernameAndPassword(userinfo.Username, password)
+	if !passwordCorrect {
+		sendErrorResponse(w, "Password Incorrect")
+		return
+	}
+
 	if runtime.GOOS == "windows" {
 		//Only allow Linux to do power operation
 		cmd := exec.Command("shutdown", "-r", "-t", "20")

+ 4 - 2
main.flags.go

@@ -3,6 +3,7 @@ package main
 import (
 	"flag"
 	"os"
+	"time"
 
 	apt "imuslab.com/arozos/mod/apt"
 	auth "imuslab.com/arozos/mod/auth"
@@ -27,7 +28,7 @@ var subserviceBasePort = 12810            //Next subservice port
 
 // =========== SYSTEM BUILD INFORMATION ==============
 var build_version = "development"                      //System build flag, this can be either {development / production / stable}
-var internal_version = "0.1.114"                       //Internal build version, please follow git commit counter for setting this value. max value \[0-9].[0-9][0-9].[0-9][0-9][0-9]\
+var internal_version = "0.1.116"                       //Internal build version, please follow git commit counter for setting this value. max value \[0-9].[0-9][0-9].[0-9][0-9][0-9]\
 var deviceUUID string                                  //The device uuid of this host
 var deviceVendor = "IMUSLAB.INC"                       //Vendor of the system
 var deviceVendorURL = "http://imuslab.com"             //Vendor contact information
@@ -39,6 +40,7 @@ var iconSystem = "img/vendor/system_icon.png"          //System icon location
 // =========== RUNTTIME RELATED ================S
 var max_upload_size int64 = 8192 << 20                         //Maxmium upload size, default 8GB
 var sudo_mode bool = (os.Geteuid() == 0 || os.Geteuid() == -1) //Check if the program is launched as sudo mode or -1 on windows
+var startupTime int64 = time.Now().Unix()                      //The startup time of the ArozOS Core
 
 // =========== SYSTEM FLAGS ==============
 //Flags related to System startup
@@ -60,8 +62,8 @@ var enable_gzip = flag.Bool("gzip", true, "Enable gzip compress on file server")
 var use_tls = flag.Bool("tls", false, "Enable TLS on HTTP serving (HTTPS Mode)")
 var disable_http = flag.Bool("disable_http", false, "Disable HTTP server, require tls=true")
 var tls_cert = flag.String("cert", "localhost.crt", "TLS certificate file (.crt)")
-var session_key = flag.String("session_key", "", "Session key, must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256). Leave empty for auto generated.")
 var tls_key = flag.String("key", "localhost.key", "TLS key file (.key)")
+var session_key = flag.String("session_key", "", "Session key, must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256). Leave empty for auto generated.")
 
 //Flags related to hardware or interfaces
 var allow_hardware_management = flag.Bool("enable_hwman", true, "Enable hardware management functions in system")

+ 3 - 3
main.go

@@ -41,9 +41,9 @@ func executeShutdownSequence() {
 	log.Println("\r- Shutting down auth gateway")
 	authAgent.Close()
 
-	//Shutdown file system handler db
-	log.Println("\r- Shutting down fsdb")
-	CloseAllStorages()
+	//Shutdown all storage pools
+	log.Println("\r- Shutting down storage pools")
+	closeAllStoragePools()
 
 	//Shutdown Subservices
 	log.Println("\r- Shutting down background subservices")

+ 8 - 8
main.router.go

@@ -13,6 +13,7 @@ import (
 	"strconv"
 	"strings"
 
+	"imuslab.com/arozos/mod/common"
 	fs "imuslab.com/arozos/mod/filesystem"
 )
 
@@ -22,8 +23,8 @@ func mrouter(h http.Handler) http.Handler {
 			You can also check the path for url using r.URL.Path
 		*/
 
-		if r.URL.Path == "/favicon.ico" || r.URL.Path == "/manifest.webmanifest" {
-			//Serving favicon or manifest. Allow no auth access.
+		if r.URL.Path == "/favicon.ico" || r.URL.Path == "/manifest.webmanifest" || r.URL.Path == "/robots.txt" || r.URL.Path == "/humans.txt" {
+			//Serving web specification files. Allow no auth access.
 			h.ServeHTTP(w, r)
 		} else if r.URL.Path == "/login.system" {
 			//Login page. Require special treatment for template.
@@ -72,20 +73,20 @@ func mrouter(h http.Handler) http.Handler {
 			} else {
 				interfaceModule := userinfo.GetInterfaceModules()
 				if len(interfaceModule) == 1 && interfaceModule[0] == "Desktop" {
-					http.Redirect(w, r, "desktop.system", 307)
+					http.Redirect(w, r, "./desktop.system", 307)
 				} else if len(interfaceModule) == 1 {
 					//User with default interface module not desktop
 					modileInfo := moduleHandler.GetModuleInfoByID(interfaceModule[0])
 					http.Redirect(w, r, modileInfo.StartDir, 307)
 				} else if len(interfaceModule) > 1 {
 					//Redirect to module selector
-					http.Redirect(w, r, "SystemAO/boot/interface_selector.html", 307)
+					http.Redirect(w, r, "./SystemAO/boot/interface_selector.html", 307)
 				} else if len(interfaceModule) == 0 {
 					//Redirect to error page
-					http.Redirect(w, r, "SystemAO/boot/no_interfaceing.html", 307)
+					http.Redirect(w, r, "./SystemAO/boot/no_interfaceing.html", 307)
 				} else {
 					//For unknown operations, send it to desktop
-					http.Redirect(w, r, "desktop.system", 307)
+					http.Redirect(w, r, "./desktop.system", 307)
 				}
 			}
 		} else if ((len(r.URL.Path) >= 5 && r.URL.Path[:5] == "/www/") || r.URL.Path == "/www") && *allow_homepage == true {
@@ -143,10 +144,9 @@ func mrouter(h http.Handler) http.Handler {
 				h.ServeHTTP(w, r)
 			} else {
 				//Other paths
-
 				//Rediect to login page
 				w.Header().Set("Cache-Control", "no-cache, no-store, no-transform, must-revalidate, private, max-age=0")
-				http.Redirect(w, r, "/login.system?redirect="+r.URL.Path, 307)
+				http.Redirect(w, r, common.ConstructRelativePathFromRequestURL(r.RequestURI, "login.system")+"?redirect="+r.URL.Path, 307)
 			}
 
 		}

+ 57 - 52
mod/agi/agi.file.go

@@ -6,11 +6,10 @@ import (
 	"log"
 	"os"
 	"path/filepath"
-	"sort"
-	"strings"
 
 	"github.com/robertkrimen/otto"
 	fs "imuslab.com/arozos/mod/filesystem"
+	"imuslab.com/arozos/mod/filesystem/fssort"
 	user "imuslab.com/arozos/mod/user"
 )
 
@@ -255,6 +254,8 @@ func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
 	//Glob
 	//glob("user:/Desktop/*.mp3") => return fileList in array
 	//glob("/") => return a list of root directories
+	//glob("user:/Desktop/*", "mostRecent") => return fileList in mostRecent sorting mode
+	//glob("user:/Desktop/*", "user") => return fileList in array in user prefered sorting method
 	vm.Set("_filelib_glob", func(call otto.FunctionCall) otto.Value {
 		regex, err := call.Argument(0).ToString()
 		if err != nil {
@@ -263,13 +264,20 @@ func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
 			return reply
 		}
 
+		userSortMode, err := call.Argument(1).ToString()
+		if err != nil || userSortMode == "" || userSortMode == "undefined" {
+			userSortMode = "default"
+		}
+
 		//Handle when regex = "." or "./" (listroot)
 		if filepath.ToSlash(filepath.Clean(regex)) == "/" || filepath.Clean(regex) == "." {
 			//List Root
 			rootDirs := []string{}
 			fileHandlers := u.GetAllFileSystemHandler()
 			for _, fsh := range fileHandlers {
-				rootDirs = append(rootDirs, fsh.UUID+":/")
+				if fsh.Hierarchy != "backup" {
+					rootDirs = append(rootDirs, fsh.UUID+":/")
+				}
 			}
 
 			reply, _ := vm.ToValue(rootDirs)
@@ -282,6 +290,22 @@ func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
 			//This function can only handle wildcard in filename but not in dir name
 			vrootPath := filepath.Dir(regex)
 			regexFilename := filepath.Base(regex)
+
+			//Rewrite and validate the sort mode
+			if userSortMode == "user" {
+				//Use user sorting mode.
+				if g.Option.UserHandler.GetDatabase().KeyExists("fs-sortpref", u.Username+"/"+filepath.ToSlash(vrootPath)) {
+					g.Option.UserHandler.GetDatabase().Read("fs-sortpref", u.Username+"/"+filepath.ToSlash(vrootPath), &userSortMode)
+				} else {
+					userSortMode = "default"
+				}
+			}
+
+			if !fssort.SortModeIsSupported(userSortMode) {
+				log.Println("[AGI] Sort mode: " + userSortMode + " not supported. Using default")
+				userSortMode = "default"
+			}
+
 			//Translate the virtual path to realpath
 			rrootPath, err := virtualPathToRealPath(vrootPath, u)
 			if err != nil {
@@ -297,8 +321,12 @@ func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
 				return reply
 			}
 
+			//Sort the files
+			newFilelist := fssort.SortFileList(suitableFiles, userSortMode)
+
+			//Return the results in virtual paths
 			results := []string{}
-			for _, file := range suitableFiles {
+			for _, file := range newFilelist {
 				thisRpath, _ := realpathToVirtualpath(filepath.ToSlash(file), u)
 				results = append(results, thisRpath)
 			}
@@ -316,7 +344,10 @@ func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
 			return reply
 		}
 
-		sortMode, _ := call.Argument(1).ToString()
+		userSortMode, err := call.Argument(1).ToString()
+		if err != nil || userSortMode == "" || userSortMode == "undefined" {
+			userSortMode = "default"
+		}
 
 		if regex != "/" && !u.CanRead(regex) {
 			panic(vm.MakeCustomError("PermissionDenied", "Path access denied"))
@@ -325,8 +356,23 @@ func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
 		//This function can only handle wildcard in filename but not in dir name
 		vrootPath := filepath.Dir(regex)
 		regexFilename := filepath.Base(regex)
-		//Translate the virtual path to realpath
 
+		//Rewrite and validate the sort mode
+		if userSortMode == "user" {
+			//Use user sorting mode.
+			if g.Option.UserHandler.GetDatabase().KeyExists("fs-sortpref", u.Username+"/"+filepath.ToSlash(vrootPath)) {
+				g.Option.UserHandler.GetDatabase().Read("fs-sortpref", u.Username+"/"+filepath.ToSlash(vrootPath), &userSortMode)
+			} else {
+				userSortMode = "default"
+			}
+		}
+
+		if !fssort.SortModeIsSupported(userSortMode) {
+			log.Println("[AGI] Sort mode: " + userSortMode + " not supported. Using default")
+			userSortMode = "default"
+		}
+
+		//Translate the virtual path to realpath
 		rrootPath, err := virtualPathToRealPath(vrootPath, u)
 		if err != nil {
 			g.raiseError(err)
@@ -341,55 +387,14 @@ func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
 			return reply
 		}
 
-		type SortingFileData struct {
-			Filename string
-			Filepath string
-			Filesize int64
-			ModTime  int64
-		}
-
-		parsedFilelist := []fs.FileData{}
-		for _, file := range suitableFiles {
-			vpath, err := realpathToVirtualpath(filepath.ToSlash(file), u)
-			if err != nil {
-				g.raiseError(err)
-				reply, _ := vm.ToValue(false)
-				return reply
-			}
-			modtime, _ := fs.GetModTime(file)
-			parsedFilelist = append(parsedFilelist, fs.FileData{
-				Filename: filepath.Base(file),
-				Filepath: vpath,
-				Filesize: fs.GetFileSize(file),
-				ModTime:  modtime,
-			})
-		}
-
-		if sortMode != "" {
-			if sortMode == "reverse" || sortMode == "descending" {
-				//Sort by reverse name
-				sort.Slice(parsedFilelist, func(i, j int) bool {
-					return strings.ToLower(parsedFilelist[i].Filename) > strings.ToLower(parsedFilelist[j].Filename)
-				})
-			} else if sortMode == "smallToLarge" {
-				sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].Filesize < parsedFilelist[j].Filesize })
-			} else if sortMode == "largeToSmall" {
-				sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].Filesize > parsedFilelist[j].Filesize })
-			} else if sortMode == "mostRecent" {
-				sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].ModTime > parsedFilelist[j].ModTime })
-			} else if sortMode == "leastRecent" {
-				sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].ModTime < parsedFilelist[j].ModTime })
-			} else {
-				sort.Slice(parsedFilelist, func(i, j int) bool {
-					return strings.ToLower(parsedFilelist[i].Filename) < strings.ToLower(parsedFilelist[j].Filename)
-				})
-			}
-		}
+		//Sort the files
+		newFilelist := fssort.SortFileList(suitableFiles, userSortMode)
 
 		//Parse the results (Only extract the filepath)
 		results := []string{}
-		for _, fileData := range parsedFilelist {
-			results = append(results, fileData.Filepath)
+		for _, filename := range newFilelist {
+			thisVpath, _ := u.RealPathToVirtualPath(filename)
+			results = append(results, thisVpath)
 		}
 
 		reply, _ := vm.ToValue(results)

+ 4 - 4
mod/agi/agi.go

@@ -74,7 +74,7 @@ func NewGateway(option AgiSysInfo) (*Gateway, error) {
 	for _, script := range startupScripts {
 		scriptContentByte, _ := ioutil.ReadFile(script)
 		scriptContent := string(scriptContentByte)
-		log.Println("Gatewat script loaded (" + script + ")")
+		log.Println("[AGI] Gatewat script loaded (" + script + ")")
 		//Create a new vm for this request
 		vm := otto.New()
 
@@ -83,7 +83,7 @@ func NewGateway(option AgiSysInfo) (*Gateway, error) {
 
 		_, err := vm.Run(scriptContent)
 		if err != nil {
-			log.Println("AJI Load Failed: " + script + ". Skipping.")
+			log.Println("[AGI] Load Failed: " + script + ". Skipping.")
 			log.Println(err)
 			continue
 		}
@@ -108,7 +108,7 @@ func (g *Gateway) RunScript(script string) error {
 
 	_, err := vm.Run(script)
 	if err != nil {
-		log.Println("Script Execution Failed: ", err.Error())
+		log.Println("[AGI] Script Execution Failed: ", err.Error())
 		return err
 	}
 
@@ -127,7 +127,7 @@ func (g *Gateway) RegisterLib(libname string, entryPoint AgiLibIntergface) error
 }
 
 func (g *Gateway) raiseError(err error) {
-	log.Println("*AGI Engine* [Runtime Error] " + err.Error())
+	log.Println("[AGI] Runtime Error " + err.Error())
 
 	//To be implemented
 }

+ 1 - 1
mod/agi/userFunc.go

@@ -28,7 +28,7 @@ func (g *Gateway) injectUserFunctions(vm *otto.Otto, scriptFile string, scriptSc
 	vm.Set("USERICON", u.GetUserIcon())
 	vm.Set("USERQUOTA_TOTAL", u.StorageQuota.TotalStorageQuota)
 	vm.Set("USERQUOTA_USED", u.StorageQuota.UsedStorageQuota)
-	vm.Set("USER_VROOTS", u.GetAllFileSystemHandler())
+	vm.Set("USER_VROOTS", u.GetAllAccessibleFileSystemHandler())
 	vm.Set("USER_MODULES", u.GetUserAccessibleModules())
 
 	//File system and path related

+ 56 - 28
mod/apt/apt.go

@@ -33,38 +33,17 @@ func (a *AptPackageManager) InstallIfNotExists(pkgname string, mustComply bool)
 	pkgname = strings.ReplaceAll(pkgname, "&", "")
 	pkgname = strings.ReplaceAll(pkgname, "|", "")
 
-	if runtime.GOOS == "windows" {
-		//Check if the command already exists in windows path paramters.
-		cmd := exec.Command("where", pkgname, "2>", "nul")
-		_, err := cmd.CombinedOutput()
-		if err != nil {
-			return errors.New("Package " + pkgname + " not found in Windows %PATH%.")
-		}
-		return nil
-	} else if runtime.GOOS == "darwin" {
-		//Mac OS. Check if package exists
-		cmd := exec.Command("whereis", pkgname)
-		out, err := cmd.CombinedOutput()
-		if err != nil {
-			return errors.New("Package " + pkgname + " not found in MacOS ENV variable.")
-		}
-
-		if strings.TrimSpace(string(out)) == "" {
-			//Package not exists
-			return errors.New("Package " + pkgname + " not installed on this Mac")
-		}
-		return nil
-	}
-
 	if a.AllowAutoInstall == false {
 		return errors.New("Package auto install is disabled")
 	}
 
-	cmd := exec.Command("which", pkgname)
-	out, _ := cmd.CombinedOutput()
+	installed, err := PackageExists(pkgname)
+	if err != nil {
+		log.Println(err.Error())
+	}
 
 	//log.Println(packageInfo)
-	if len(string(out)) > 1 {
+	if installed {
 		return nil
 	} else {
 		//Package not installed. Install if now if running in sudo mode
@@ -76,11 +55,10 @@ func (a *AptPackageManager) InstallIfNotExists(pkgname string, mustComply bool)
 		if err != nil {
 			if mustComply {
 				//Panic and terminate server process
-				log.Println("Installation failed on package: "+pkgname, string(out))
+				log.Println("Installation failed on package: " + pkgname)
 				os.Exit(0)
 			} else {
 				log.Println("Installation failed on package: " + pkgname)
-				log.Println(string(out))
 			}
 			return err
 		}
@@ -90,6 +68,56 @@ func (a *AptPackageManager) InstallIfNotExists(pkgname string, mustComply bool)
 	return nil
 }
 
+func PackageExists(pkgname string) (bool, error) {
+	if runtime.GOOS == "windows" {
+		//Check if the command already exists in windows path paramters.
+		cmd := exec.Command("where", pkgname, "2>", "nul")
+		_, err := cmd.CombinedOutput()
+		if err != nil {
+			return false, errors.New("Package " + pkgname + " not found in Windows %PATH%.")
+		}
+		return true, nil
+	} else if runtime.GOOS == "darwin" {
+		//Mac OS. Check if package exists
+		cmd := exec.Command("whereis", pkgname)
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			return false, errors.New("Package " + pkgname + " not found in MacOS ENV variable.")
+		}
+
+		if strings.TrimSpace(string(out)) == "" {
+			//Package not found by whereis. Try brew
+			cmd := exec.Command("bash", "-c", "brew list | grep "+pkgname)
+			out, err = cmd.CombinedOutput()
+			if err != nil {
+				return false, errors.New("Package " + pkgname + " not found in MacOS ENV variable.")
+			}
+
+			if strings.TrimSpace(string(out)) != "" {
+				//Exists!
+				return true, nil
+			} else {
+				return false, errors.New("Package " + pkgname + " not installed on this Mac")
+			}
+
+		}
+	} else if runtime.GOOS == "linux" {
+		cmd := exec.Command("which", pkgname)
+		out, _ := cmd.CombinedOutput()
+
+		if len(string(out)) > 1 {
+			return true, nil
+		} else {
+			return false, errors.New("Package " + pkgname + " not installed on this Linux Host")
+		}
+
+	} else {
+		return false, errors.New("Unsupported Platform")
+	}
+
+	return false, errors.New("Unknown error occured when checking package installed")
+}
+
 func HandlePackageListRequest(w http.ResponseWriter, r *http.Request) {
 	if runtime.GOOS == "windows" {
 		w.Header().Set("Content-Type", "application/json")

+ 22 - 5
mod/auth/authlogger/authlogger.go

@@ -30,6 +30,7 @@ type LoginRecord struct {
 	TargetUsername string
 	LoginSucceed   bool
 	IpAddr         string
+	AuthType       string
 	Port           int
 }
 
@@ -48,7 +49,21 @@ func NewLogger() (*Logger, error) {
 func (l *Logger) LogAuth(r *http.Request, loginStatus bool) error {
 	username, _ := mv(r, "username", true)
 	timestamp := time.Now().Unix()
+	//handling the reverse proxy remote IP issue
+	remoteIP := r.Header.Get("X-FORWARDED-FOR")
+	if remoteIP != "" {
+		//grab the last known remote IP from header
+		remoteIPs := strings.Split(remoteIP, ", ")
+		remoteIP = remoteIPs[len(remoteIPs)-1]
+	} else {
+		//if there is no X-FORWARDED-FOR, use default remote IP
+		remoteIP = r.RemoteAddr
+	}
+	return l.LogAuthByRequestInfo(username, remoteIP, timestamp, loginStatus, "web")
+}
 
+//Log the current authentication to record by custom filled information. Use LogAuth if your module is authenticating via web interface
+func (l *Logger) LogAuthByRequestInfo(username string, remoteAddr string, timestamp int64, loginSucceed bool, authType string) error {
 	//Get the current month as the table name, create table if not exists
 	current := time.Now().UTC()
 	tableName := current.Format("Jan-2006")
@@ -60,16 +75,16 @@ func (l *Logger) LogAuth(r *http.Request, loginStatus bool) error {
 
 	//Split the remote address into ipaddr and port
 	remoteAddrInfo := []string{"unknown", "N/A"}
-	if strings.Contains(r.RemoteAddr, ":") {
+	if strings.Contains(remoteAddr, ":") {
 		//For general IPv4  address
-		remoteAddrInfo = strings.Split(r.RemoteAddr, ":")
+		remoteAddrInfo = strings.Split(remoteAddr, ":")
 	}
 
 	//Check for IPV6
-	if strings.Contains(r.RemoteAddr, "[") && strings.Contains(r.RemoteAddr, "]") {
+	if strings.Contains(remoteAddr, "[") && strings.Contains(remoteAddr, "]") {
 		//This is an IPV6 address. Rewrite the split
 		//IPv6 should have the format of something like this [::1]:80
-		ipv6info := strings.Split(r.RemoteAddr, ":")
+		ipv6info := strings.Split(remoteAddr, ":")
 		port := ipv6info[len(ipv6info)-1:]
 		ipAddr := ipv6info[:len(ipv6info)-1]
 		remoteAddrInfo = []string{strings.Join(ipAddr, ":"), strings.Join(port, ":")}
@@ -85,8 +100,9 @@ func (l *Logger) LogAuth(r *http.Request, loginStatus bool) error {
 	thisRecord := LoginRecord{
 		Timestamp:      timestamp,
 		TargetUsername: username,
-		LoginSucceed:   loginStatus,
+		LoginSucceed:   loginSucceed,
 		IpAddr:         remoteAddrInfo[0],
+		AuthType:       authType,
 		Port:           port,
 	}
 
@@ -100,6 +116,7 @@ func (l *Logger) LogAuth(r *http.Request, loginStatus bool) error {
 	}
 
 	return nil
+
 }
 
 //Close the database when system shutdown

+ 14 - 0
mod/common/common.go

@@ -8,6 +8,7 @@ import (
 	"log"
 	"net/http"
 	"os"
+	"strings"
 	"time"
 )
 
@@ -117,3 +118,16 @@ func LoadImageAsBase64(filepath string) (string, error) {
 	encoded := base64.StdEncoding.EncodeToString(content)
 	return string(encoded), nil
 }
+
+//Use for redirections
+func ConstructRelativePathFromRequestURL(requestURI string, redirectionLocation string) string {
+	if strings.Count(requestURI, "/") == 1 {
+		//Already root level
+		return redirectionLocation
+	}
+	for i := 0; i < strings.Count(requestURI, "/")-1; i++ {
+		redirectionLocation = "../" + redirectionLocation
+	}
+
+	return redirectionLocation
+}

+ 97 - 0
mod/disk/diskcapacity/dftool/dftool.go

@@ -0,0 +1,97 @@
+package dftool
+
+import (
+	"errors"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strconv"
+	"strings"
+
+	"imuslab.com/arozos/mod/disk/diskspace"
+)
+
+type Capacity struct {
+	PhysicalDevice    string //The ID of the physical device, like C:/ or /dev/sda1
+	MountingHierarchy string //The Mounting Hierarchy of the vroot
+	Used              int64  //Used capacity in bytes
+	Avilable          int64  //Avilable capacity in bytes
+	Total             int64  //Total capacity in bytes
+}
+
+func GetCapacityInfoFromPath(realpath string) (*Capacity, error) {
+	rpathAbs, err := filepath.Abs(realpath)
+	if err != nil {
+		return nil, err
+	}
+
+	if runtime.GOOS == "windows" {
+		//Windows
+		//Extract disk ID from path
+		rpathAbs = filepath.ToSlash(filepath.Clean(rpathAbs))
+		diskRoot := strings.Split(rpathAbs, "/")[0]
+
+		//Match the disk space info generated from diskspace
+		logicDiskInfo := diskspace.GetAllLogicDiskInfo()
+		for _, ldi := range logicDiskInfo {
+			if strings.TrimSpace(ldi.Device) == strings.TrimSpace(diskRoot) {
+				//Matching device ID
+				return &Capacity{
+					PhysicalDevice: ldi.Device,
+					Used:           ldi.Used,
+					Avilable:       ldi.Available,
+					Total:          ldi.Volume,
+				}, nil
+			}
+		}
+
+	} else {
+		//Assume Linux or Mac
+		//Use command: df -P {abs_path}
+		cmd := exec.Command("df", "-P", rpathAbs)
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			return nil, err
+		}
+
+		//Get the last line of the output
+		diskInfo := strings.TrimSpace(string(out))
+		tmp := strings.Split(diskInfo, "\n")
+		targetDiskInfo := strings.Join(tmp[len(tmp)-1:], " ")
+		for strings.Contains(targetDiskInfo, "  ") {
+			targetDiskInfo = strings.ReplaceAll(targetDiskInfo, "  ", " ")
+		}
+
+		diskInfoSlice := strings.Split(targetDiskInfo, " ")
+
+		if len(diskInfoSlice) < 4 {
+			return nil, errors.New("Malformed output for df -P")
+		}
+
+		//Extract capacity information from df output
+		total, err := strconv.ParseInt(diskInfoSlice[1], 10, 64)
+		if err != nil {
+			return nil, errors.New("Malformed output for df -P")
+		}
+
+		used, err := strconv.ParseInt(diskInfoSlice[2], 10, 64)
+		if err != nil {
+			return nil, errors.New("Malformed output for df -P")
+		}
+
+		availbe, err := strconv.ParseInt(diskInfoSlice[3], 10, 64)
+		if err != nil {
+			return nil, errors.New("Malformed output for df -P")
+		}
+
+		//Return the capacity info struct, capacity is reported in 1024 bytes block
+		return &Capacity{
+			PhysicalDevice: diskInfoSlice[0],
+			Used:           used * 1024,
+			Avilable:       availbe * 1024,
+			Total:          total * 1024,
+		}, nil
+	}
+
+	return nil, errors.New("Unable to resolve matching disk capacity information")
+}

+ 3 - 94
mod/disk/diskcapacity/diskcapacity.go

@@ -2,17 +2,11 @@ package diskcapacity
 
 import (
 	"encoding/json"
-	"errors"
 	"net/http"
 	"path/filepath"
-	"runtime"
-	"strings"
-	"log"
-	"strconv"
-	"os/exec"
 
 	"imuslab.com/arozos/mod/common"
-	"imuslab.com/arozos/mod/disk/diskspace"
+	"imuslab.com/arozos/mod/disk/diskcapacity/dftool"
 	"imuslab.com/arozos/mod/user"
 )
 
@@ -28,14 +22,6 @@ type Resolver struct {
 	UserHandler *user.UserHandler
 }
 
-type Capacity struct {
-	PhysicalDevice    string //The ID of the physical device, like C:/ or /dev/sda1
-	MountingHierarchy string //The Mounting Hierarchy of the vroot
-	Used              int64  //Used capacity in bytes
-	Avilable          int64  //Avilable capacity in bytes
-	Total             int64  //Total capacity in bytes
-}
-
 //Create a new Capacity Resolver with the given user handler
 func NewCapacityResolver(u *user.UserHandler) *Resolver {
 	return &Resolver{
@@ -78,7 +64,7 @@ func (cr *Resolver) HandleCapacityResolving(w http.ResponseWriter, r *http.Reque
 
 }
 
-func (cr *Resolver) ResolveCapacityInfo(username string, vpath string) (*Capacity, error) {
+func (cr *Resolver) ResolveCapacityInfo(username string, vpath string) (*dftool.Capacity, error) {
 	//Resolve the vpath for this user
 	userinfo, err := cr.UserHandler.GetUserInfoFromUsername(username)
 	if err != nil {
@@ -91,83 +77,6 @@ func (cr *Resolver) ResolveCapacityInfo(username string, vpath string) (*Capacit
 	}
 
 	realpath = filepath.ToSlash(filepath.Clean(realpath))
-	return cr.GetCapacityInfo(realpath)
-}
-
-func (cr *Resolver) GetCapacityInfo(realpath string) (*Capacity, error) {
-	rpathAbs, err := filepath.Abs(realpath)
-	if err != nil {
-		return nil, err
-	}
-
-	if runtime.GOOS == "windows" {
-		//Windows
-		//Extract disk ID from path
-		rpathAbs = filepath.ToSlash(filepath.Clean(rpathAbs))
-		diskRoot := strings.Split(rpathAbs, "/")[0]
-
-		//Match the disk space info generated from diskspace
-		logicDiskInfo := diskspace.GetAllLogicDiskInfo()
-		for _, ldi := range logicDiskInfo {
-			if strings.TrimSpace(ldi.Device) == strings.TrimSpace(diskRoot) {
-				//Matching device ID
-				return &Capacity{
-					PhysicalDevice: ldi.Device,
-					Used:           ldi.Used,
-					Avilable:       ldi.Available,
-					Total:          ldi.Volume,
-				}, nil
-			}
-		}
-
-	} else {
-		//Assume Linux or Mac
-		//Use command: df -P {abs_path}
-		cmd := exec.Command("df", "-P", rpathAbs)
-		log.Println("df", "-P", rpathAbs)
-		out, err := cmd.CombinedOutput()
-		if err != nil {
-			return nil, err
-		}
-
-		//Get the last line of the output
-		diskInfo := strings.TrimSpace(string(out))
-		tmp := strings.Split(diskInfo, "\n")
-		targetDiskInfo := strings.Join(tmp[len(tmp) - 1:], " ");
-		for strings.Contains(targetDiskInfo, "  "){
-			targetDiskInfo = strings.ReplaceAll(targetDiskInfo, "  ", " ")
-		}
-
-		diskInfoSlice := strings.Split(targetDiskInfo, " ")
-
-		if len(diskInfoSlice) < 4{
-			return nil, errors.New("Malformed output for df -P")
-		}
-
-		//Extract capacity information from df output
-		total, err := strconv.ParseInt(diskInfoSlice[1], 10, 64)
-		if err != nil{
-			return nil, errors.New("Malformed output for df -P")
-		}
-
-		used, err := strconv.ParseInt(diskInfoSlice[2], 10, 64)
-		if err != nil{
-			return nil, errors.New("Malformed output for df -P")
-		}
-
-		availbe, err := strconv.ParseInt(diskInfoSlice[3], 10, 64)
-		if err != nil{
-			return nil, errors.New("Malformed output for df -P")
-		}
-
-		//Return the capacity info struct, capacity is reported in 1024 bytes block
-		return &Capacity{
-			PhysicalDevice: diskInfoSlice[0],
-			Used: used * 1024,
-			Avilable: availbe * 1024,
-			Total: total * 1024,
-		}, nil
-	}
 
-	return nil, errors.New("Unable to resolve matching disk capacity information")
+	return dftool.GetCapacityInfoFromPath(realpath)
 }

+ 58 - 40
mod/disk/hybridBackup/basicBackup.go

@@ -15,6 +15,8 @@ import (
 	This script handle basic backup process
 */
 
+var autoDeleteTime int64 = 86400 * 30
+
 func executeBackup(backupConfig *BackupTask, deepBackup bool) (string, error) {
 	copiedFileList := []string{}
 
@@ -23,25 +25,31 @@ func executeBackup(backupConfig *BackupTask, deepBackup bool) (string, error) {
 	//Check if the backup parent root is identical / within backup disk
 	parentRootAbs, err := filepath.Abs(backupConfig.ParentPath)
 	if err != nil {
+		backupConfig.PanicStopped = true
 		return "", errors.New("Unable to resolve parent disk path")
 	}
 
 	backupRootAbs, err := filepath.Abs(filepath.Join(backupConfig.DiskPath, "/backup/"))
 	if err != nil {
+		backupConfig.PanicStopped = true
 		return "", errors.New("Unable to resolve backup disk path")
 	}
 
 	if len(parentRootAbs) >= len(backupRootAbs) {
 		if parentRootAbs[:len(backupRootAbs)] == backupRootAbs {
 			//parent root is within backup root. Raise configuration error
-			log.Println("*HyperBackup* Invalid backup cycle: Parent drive is located inside backup drive")
+			log.Println("[HyperBackup] Invalid backup cycle: Parent drive is located inside backup drive")
+			backupConfig.PanicStopped = true
 			return "", errors.New("Configuration Error. Skipping backup cycle.")
 		}
 	}
 
+	backupConfig.PanicStopped = false
+
 	//Add file cycles
+	//log.Println("[Debug] Cycle 1: Adding files")
 	fastWalk(rootPath, func(filename string) error {
-		if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
+		if filepath.Ext(filename) == ".db" || filepath.Ext(filename) == ".lock" {
 			//Reserved filename, skipping
 			return nil
 		}
@@ -110,7 +118,7 @@ func executeBackup(backupConfig *BackupTask, deepBackup bool) (string, error) {
 					}
 
 					if srcHash != targetHash {
-						log.Println("[Debug] Hash mismatch. Copying ", fileAbs)
+						//log.Println("[Debug] Hash mismatch. Copying ", fileAbs)
 						//This file has been recently changed. Copy it to new location
 						err = BufferedLargeFileCopy(fileAbs, assumedTargetPosition, 1024)
 						if err != nil {
@@ -125,6 +133,7 @@ func executeBackup(backupConfig *BackupTask, deepBackup bool) (string, error) {
 						if ok {
 							//File exists. remove it from delete file amrker
 							delete(backupConfig.DeleteFileMarkers, relPath)
+							backupConfig.Database.Delete("DeleteMarkers", relPath)
 							log.Println("Removing ", relPath, " from delete marker list")
 						}
 					}
@@ -133,47 +142,56 @@ func executeBackup(backupConfig *BackupTask, deepBackup bool) (string, error) {
 			}
 		}
 
-		///Remove file cycle
-		backupDriveRootPath := filepath.ToSlash(filepath.Clean(filepath.Join(backupConfig.DiskPath, "/backup/")))
-		fastWalk(backupConfig.DiskPath, func(filename string) error {
-			if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
-				//Reserved filename, skipping
-				return nil
-			}
-			//Get the target paste location
-			rootAbs, _ := filepath.Abs(backupDriveRootPath)
-			fileAbs, _ := filepath.Abs(filename)
-
-			rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
-			fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
-
-			thisFileRel := filename[len(backupDriveRootPath):]
-			originalFileOnDiskPath := filepath.ToSlash(filepath.Clean(filepath.Join(backupConfig.ParentPath, thisFileRel)))
-
-			//Check if the taget file not exists and this file has been here for more than 24h
-			if !fileExists(originalFileOnDiskPath) {
-				//This file not exists. Check if it is in the delete file marker for more than 24 hours
-				val, ok := backupConfig.DeleteFileMarkers[thisFileRel]
-				if !ok {
-					//This file is newly deleted. Push into the marker map
-					backupConfig.DeleteFileMarkers[thisFileRel] = time.Now().Unix()
-					log.Println("[Debug] Adding " + filename + " to delete marker")
-				} else {
-					//This file has been marked. Check if it is time to delete
-					if time.Now().Unix()-val > 3600*24 {
-						log.Println("[Debug] Deleting " + filename)
+		return nil
+	})
 
-						//Remove the backup file
-						os.RemoveAll(filename)
+	///Remove file cycle
+	//log.Println("[Debug] Cycle 2: Removing files")
+	backupDriveRootPath := filepath.ToSlash(filepath.Clean(filepath.Join(backupConfig.DiskPath, "/backup/")))
+	fastWalk(backupConfig.DiskPath, func(filename string) error {
+		if filepath.Ext(filename) == ".db" || filepath.Ext(filename) == ".lock" {
+			//Reserved filename, skipping
+			return nil
+		}
+		//Get the target paste location
+		rootAbs, _ := filepath.Abs(backupDriveRootPath)
+		fileAbs, _ := filepath.Abs(filename)
 
-						//Remove file from delete file markers
-						delete(backupConfig.DeleteFileMarkers, thisFileRel)
-					}
+		rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
+		fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
+
+		thisFileRel := filename[len(backupDriveRootPath):]
+		originalFileOnDiskPath := filepath.ToSlash(filepath.Clean(filepath.Join(backupConfig.ParentPath, thisFileRel)))
+
+		//Check if the taget file not exists and this file has been here for more than 24h
+		if !fileExists(originalFileOnDiskPath) {
+			//This file not exists. Check if it is in the delete file marker for more than 24 hours
+			val, ok := backupConfig.DeleteFileMarkers[thisFileRel]
+			if !ok {
+				//This file is newly deleted. Push into the marker map
+				deleteTime := time.Now().Unix()
+				backupConfig.DeleteFileMarkers[thisFileRel] = deleteTime
+
+				//Write the delete marker to database
+				backupConfig.Database.Write("DeleteMarkers", thisFileRel, deleteTime)
+
+				log.Println("[HybridBackup] Adding " + filename + " to delete marker")
+			} else {
+				//This file has been marked for 30 days. Check if it is time to delete
+				if time.Now().Unix()-val > autoDeleteTime {
+					//log.Println("[Debug] Deleting " + filename)
+
+					//Remove the backup file
+					os.RemoveAll(filename)
+
+					//Remove file from delete file markers
+					delete(backupConfig.DeleteFileMarkers, thisFileRel)
+
+					//Remove file from database
+					backupConfig.Database.Delete("DeleteMarkers", thisFileRel)
 				}
 			}
-			return nil
-		})
-
+		}
 		return nil
 	})
 

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

@@ -45,7 +45,7 @@ func (t *BackupTask) compareRootPaths() ([]*RestorableFile, error) {
 				RelpathOnDisk: filepath.ToSlash(key),
 				RestorePoint:  filepath.ToSlash(assumedSourcePosition),
 				BackupDiskUID: t.DiskUID,
-				RemainingTime: 86400 - (time.Now().Unix() - value),
+				RemainingTime: autoDeleteTime - (time.Now().Unix() - value),
 				DeleteTime:    value,
 				IsSnapshot:    false,
 			}

BIN=BIN
mod/disk/hybridBackup/doc.txt


+ 10 - 0
mod/disk/hybridBackup/fileUtil.go

@@ -73,3 +73,13 @@ func isDir(filename string) bool {
 
 	return fileInfo.IsDir()
 }
+
+func fileSize(filename string) int64 {
+	fi, err := os.Stat("/path/to/file")
+	if err != nil {
+		return -1
+	}
+	// get the size
+	size := fi.Size()
+	return size
+}

+ 138 - 51
mod/disk/hybridBackup/hybridBackup.go

@@ -3,6 +3,7 @@ package hybridBackup
 import (
 	"crypto/sha256"
 	"encoding/hex"
+	"encoding/json"
 	"errors"
 	"io"
 	"log"
@@ -10,6 +11,8 @@ import (
 	"path/filepath"
 	"strings"
 	"time"
+
+	"imuslab.com/arozos/mod/database"
 )
 
 /*
@@ -41,16 +44,26 @@ type Manager struct {
 }
 
 type BackupTask struct {
-	JobName           string           //The name used by the scheduler for executing this config
-	CycleCounter      int64            //The number of backup executed in the background
-	LastCycleTime     int64            //The execution time of the last cycle
-	Enabled           bool             //Check if the task is enabled. Will not execute if this is set to false
-	DiskUID           string           //The UID of the target fsandlr
-	DiskPath          string           //The mount point for the disk
-	ParentUID         string           //Parent virtal disk UUID
-	ParentPath        string           //Parent disk path
-	DeleteFileMarkers map[string]int64 //Markers for those files delete pending, [file path (relative)] time
-	Mode              string           //Backup mode
+	JobName           string             //The name used by the scheduler for executing this config
+	CycleCounter      int64              //The number of backup executed in the background
+	LastCycleTime     int64              //The execution time of the last cycle
+	Enabled           bool               //Check if the task is enabled. Will not execute if this is set to false
+	DiskUID           string             //The UID of the target fsandlr
+	DiskPath          string             //The mount point for the disk
+	ParentUID         string             //Parent virtal disk UUID
+	ParentPath        string             //Parent disk path
+	DeleteFileMarkers map[string]int64   //Markers for those files delete pending, [file path (relative)] time
+	Database          *database.Database //The database for storing requried data
+	Mode              string             //Backup mode
+	PanicStopped      bool               //If the backup process has been stopped due to panic situationc
+	ErrorMessage      string             //Panic stop message
+}
+
+//A snapshot summary
+type SnapshotSummary struct {
+	ChangedFiles   map[string]string
+	UnchangedFiles map[string]string
+	DeletedFiles   map[string]string
 }
 
 //A file in the backup drive that is restorable
@@ -89,13 +102,18 @@ func NewHyperBackupManager() *Manager {
 
 	///Create task executor
 	go func() {
-		defer log.Println("[HybridBackup] Ticker Stopped")
+		defer log.Println("HybridBackup stopped")
 		for {
 			select {
 			case <-ticker.C:
 				for _, task := range newManager.Tasks {
 					if task.Enabled == true {
-						task.HandleBackupProcess()
+						output, err := task.HandleBackupProcess()
+						if err != nil {
+							task.Enabled = false
+							task.PanicStopped = true
+							task.ErrorMessage = output
+						}
 					}
 				}
 			case <-stopper:
@@ -110,7 +128,7 @@ func NewHyperBackupManager() *Manager {
 
 func (m *Manager) AddTask(newtask *BackupTask) error {
 	//Create a job for this
-	newtask.JobName = "backup-[" + newtask.DiskUID + "]"
+	newtask.JobName = "backup-" + newtask.DiskUID + ""
 
 	//Check if the same job name exists
 	for _, task := range m.Tasks {
@@ -119,13 +137,39 @@ func (m *Manager) AddTask(newtask *BackupTask) error {
 		}
 	}
 
+	//Create / Load a backup database for the task
+	dbPath := filepath.Join(newtask.DiskPath, newtask.JobName+".db")
+	thisdb, err := database.NewDatabase(dbPath, false)
+	if err != nil {
+		log.Println("[HybridBackup] Failed to create database for backup tasks. Running without one.")
+	} else {
+		newtask.Database = thisdb
+		thisdb.NewTable("DeleteMarkers")
+	}
+
+	if newtask.Mode == "basic" || newtask.Mode == "nightly" {
+		//Load the delete marker from the database if exists
+		if thisdb.TableExists("DeleteMarkers") {
+			//Table exists. Read all its content to delete markers
+			entries, _ := thisdb.ListTable("DeleteMarkers")
+			for _, keypairs := range entries {
+				relPath := string(keypairs[0])
+				delTime := int64(0)
+				json.Unmarshal(keypairs[1], &delTime)
+
+				//Add this to delete marker
+				newtask.DeleteFileMarkers[relPath] = delTime
+			}
+		}
+	}
+
 	//Add task to list
 	m.Tasks = append(m.Tasks, newtask)
 
 	//Start the task
 	m.StartTask(newtask.JobName)
 
-	log.Println(">>>> [Debug] New Backup Tasks added: ", newtask.JobName, newtask)
+	//log.Println(">>>> [Debug] New Backup Tasks added: ", newtask.JobName, newtask)
 
 	return nil
 }
@@ -137,8 +181,16 @@ func (m *Manager) StartTask(jobname string) {
 			//Enable to job
 			task.Enabled = true
 
-			//Run it once
-			task.HandleBackupProcess()
+			//Run it once in go routine
+			go func() {
+				output, err := task.HandleBackupProcess()
+				if err != nil {
+					task.Enabled = false
+					task.PanicStopped = true
+					task.ErrorMessage = output
+				}
+			}()
+
 		}
 	}
 }
@@ -154,20 +206,27 @@ func (m *Manager) StopTask(jobname string) {
 
 //Stop all managed handlers
 func (m *Manager) Close() error {
-	m.StopTicker <- true
+	//Stop the schedule
+	if m != nil {
+		m.StopTicker <- true
+
+		//Close all database opened by backup task
+		for _, task := range m.Tasks {
+			task.Database.Close()
+		}
+	}
+
 	return nil
 }
 
 //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
 
 	} else {
-		//File system not mounted even after 3 backup cycle. Terminate backup scheduler
+		//Parent File system not mounted.Terminate backup scheduler
 		log.Println("[HybridBackup] Skipping backup cycle for " + backupConfig.ParentUID + ":/")
 		return "Parent drive (" + backupConfig.ParentUID + ":/) not mounted", nil
 	}
@@ -183,30 +242,42 @@ func (backupConfig *BackupTask) HandleBackupProcess() (string, error) {
 		if backupConfig.CycleCounter%3 == 0 {
 			//Perform deep backup, use walk function
 			deepBackup = true
+			log.Println("[HybridBackup] Basic backup executed: " + backupConfig.ParentUID + ":/ -> " + backupConfig.DiskUID + ":/")
+			backupConfig.LastCycleTime = time.Now().Unix()
 		} else {
 			deepBackup = false
-			backupConfig.LastCycleTime = time.Now().Unix()
 		}
-		return executeBackup(backupConfig, deepBackup)
+
+		//Add one to the cycle counter
+		backupConfig.CycleCounter++
+		_, err := executeBackup(backupConfig, deepBackup)
+		if err != nil {
+			log.Println("[HybridBackup] Backup failed: " + err.Error())
+		}
 	} 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()
+			executeBackup(backupConfig, true)
+			log.Println("[HybridBackup] Executing nightly backup: " + backupConfig.ParentUID + ":/ -> " + backupConfig.DiskUID + ":/")
+
+			//Add one to the cycle counter
+			backupConfig.CycleCounter++
 		}
 
 	} else if backupConfig.Mode == "version" {
-		//Do a versioning backup
-		if time.Now().Unix()-backupConfig.LastCycleTime >= 86400 || backupConfig.CycleCounter == 0 {
+		//Do a versioning backup every 6 hours
+		if time.Now().Unix()-backupConfig.LastCycleTime >= 21600 {
 			//Scheduled backup or initial backup
-			executeVersionBackup(backupConfig)
 			backupConfig.LastCycleTime = time.Now().Unix()
+			executeVersionBackup(backupConfig)
+			log.Println("[HybridBackup] Executing backup schedule: " + backupConfig.ParentUID + ":/ -> " + backupConfig.DiskUID + ":/")
+
+			//Add one to the cycle counter
+			backupConfig.CycleCounter++
 		}
 	}
 
-	//Add one to the cycle counter
-	backupConfig.CycleCounter++
-
 	//Return the log information
 	return "", nil
 }
@@ -222,7 +293,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 {
@@ -230,36 +301,44 @@ func (m *Manager) HandleRestore(restoreDiskID string, targetFileRelpath string)
 	}
 
 	//Check if source exists and target not exists
-	log.Println("[debug]", backupTask)
+	//log.Println("[debug]", backupTask)
 
 	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
@@ -361,3 +440,11 @@ func getFileHash(filename string) (string, error) {
 
 	return hex.EncodeToString(h.Sum(nil)), nil
 }
+
+func (m *Manager) GetTaskByBackupDiskID(backupDiskID string) (*BackupTask, error) {
+	targetTask := m.getTaskByBackupDiskID(backupDiskID)
+	if targetTask == nil {
+		return nil, errors.New("Task not found")
+	}
+	return targetTask, nil
+}

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

@@ -2,8 +2,10 @@ package hybridBackup
 
 import (
 	"encoding/json"
+	"errors"
 	"io/ioutil"
 	"path/filepath"
+	"strings"
 )
 
 /*
@@ -48,11 +50,45 @@ func readLinkFile(snapshotFolder string) (*LinkFileMap, error) {
 				return &lfContent, nil
 			}
 		}
+	} else {
+		return &result, errors.New("Linker file not exists")
 	}
 
 	return &result, nil
 }
 
+//Update the linker by given a snapshot name to a new one
+func updateLinkerPointer(snapshotFolder string, oldSnapshotLink string, newSnapshotLink string) error {
+	oldSnapshotLink = strings.TrimSpace(oldSnapshotLink)
+	newSnapshotLink = strings.TrimSpace(newSnapshotLink)
+
+	//Load the old linker file
+	oldlinkMap, err := readLinkFile(snapshotFolder)
+	if err != nil {
+		return err
+	}
+
+	//Iterate and replace all link that is pointing to the same snapshot
+	newLinkMap := LinkFileMap{
+		UnchangedFile: map[string]string{},
+		DeletedFiles:  map[string]string{},
+	}
+
+	for rel, link := range oldlinkMap.UnchangedFile {
+		if link == oldSnapshotLink {
+			link = newSnapshotLink
+		}
+		newLinkMap.UnchangedFile[rel] = link
+	}
+
+	for rel, ts := range oldlinkMap.DeletedFiles {
+		newLinkMap.DeletedFiles[rel] = ts
+	}
+
+	//Write it back to file
+	return generateLinkFile(snapshotFolder, newLinkMap)
+}
+
 //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)]

+ 204 - 0
mod/disk/hybridBackup/snaoshotOpr.go

@@ -0,0 +1,204 @@
+package hybridBackup
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+/*
+	snapshotOpr.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
+}
+
+/*
+	Merge Snapshot
+
+	This function is used to merge old snapshots if the system is running out of space
+	the two snapshot has to be sequential
+*/
+
+func mergeOldestSnapshots(backupTask *BackupTask) error {
+	//Get all snapshot names from disk path
+	files, err := filepath.Glob(filepath.ToSlash(filepath.Clean(filepath.Join(backupTask.DiskPath, "/version/"))) + "/*")
+	if err != nil {
+		return err
+	}
+
+	snapshots := []string{}
+	for _, file := range files {
+		if isDir(file) && fileExists(filepath.Join(file, "snapshot.datalink")) {
+			//This is a snapshot file
+			snapshots = append(snapshots, file)
+		}
+	}
+
+	if len(snapshots) < 2 {
+		return errors.New("Not enough snapshot to merge")
+	}
+
+	olderSnapshotDir := filepath.ToSlash(snapshots[0])
+	newerSnapshitDir := filepath.ToSlash(snapshots[1])
+
+	//Check if both snapshot exists
+	if !fileExists(olderSnapshotDir) || !fileExists(newerSnapshitDir) {
+		log.Println("[HybridBackup] Snapshot merge failed: Snapshot folder not found")
+		return errors.New("Snapshot folder not found")
+	}
+
+	//Check if link file exists
+	linkFileLocation := filepath.Join(newerSnapshitDir, "snapshot.datalink")
+	if !fileExists(linkFileLocation) {
+		log.Println("[HybridBackup] Snapshot link file not found.")
+		return errors.New("Snapshot link file not found")
+	}
+
+	//Get linker file
+	linkMap, err := readLinkFile(newerSnapshitDir)
+	if err != nil {
+		linkMap = &LinkFileMap{
+			UnchangedFile: map[string]string{},
+			DeletedFiles:  map[string]string{},
+		}
+	}
+
+	log.Println("[HybridBackup] Merging two snapshots in background")
+
+	//All file ready. Merge both snapshots
+	rootAbs, _ := filepath.Abs(olderSnapshotDir)
+	rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
+	fastWalk(olderSnapshotDir, func(filename string) error {
+		fileAbs, _ := filepath.Abs(filename)
+		fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
+
+		relPath := filepath.ToSlash(strings.ReplaceAll(fileAbs, rootAbs, ""))
+		mergeAssumedLocation := filepath.Join(newerSnapshitDir, relPath)
+		if !fileExists(mergeAssumedLocation) {
+			//Check if this is in delete marker. If yes, skip this
+			_, ok := linkMap.DeletedFiles[relPath]
+			if !ok {
+				//This is not in delete map. Move it
+				//This must use rename instead of copy because of lack of space issue
+				if !fileExists(filepath.Dir(mergeAssumedLocation)) {
+					os.MkdirAll(filepath.Dir(mergeAssumedLocation), 0775)
+				}
+				err = os.Rename(filename, mergeAssumedLocation)
+				if err != nil {
+					return err
+				}
+			} else {
+				fmt.Println("Disposing file: ", relPath)
+			}
+		}
+
+		return nil
+	})
+
+	//Rewrite all other datalink file to make olderSnapshot name to new snapshot name
+	oldLink := filepath.Base(olderSnapshotDir)
+	newLink := filepath.Base(newerSnapshitDir)
+	for i := 1; i < len(snapshots); i++ {
+		err = updateLinkerPointer(snapshots[i], oldLink, newLink)
+		if err != nil {
+			log.Println("[HybridBackup] Link file update file: " + filepath.Base(snapshots[i]))
+		}
+		fmt.Println("Updating link file for " + filepath.Base(snapshots[i]))
+	}
+
+	//Remove the old snapshot folder structure
+	err = os.RemoveAll(olderSnapshotDir)
+	return err
+}

+ 227 - 48
mod/disk/hybridBackup/versionBackup.go

@@ -7,6 +7,8 @@ import (
 	"path/filepath"
 	"strings"
 	"time"
+
+	"imuslab.com/arozos/mod/disk/diskcapacity/dftool"
 )
 
 /*
@@ -20,30 +22,43 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 	//Check if the backup parent root is identical / within backup disk
 	parentRootAbs, err := filepath.Abs(backupConfig.ParentPath)
 	if err != nil {
+		backupConfig.PanicStopped = true
 		return "", errors.New("Unable to resolve parent disk path")
 	}
 
 	backupRootAbs, err := filepath.Abs(filepath.Join(backupConfig.DiskPath, "/version/"))
 	if err != nil {
+		backupConfig.PanicStopped = true
 		return "", errors.New("Unable to resolve backup disk path")
 	}
 
 	if len(parentRootAbs) >= len(backupRootAbs) {
 		if parentRootAbs[:len(backupRootAbs)] == backupRootAbs {
 			//parent root is within backup root. Raise configuration error
-			log.Println("*HyperBackup* Invalid backup cycle: Parent drive is located inside backup drive")
+			log.Println("[HyperBackup] Invalid backup cycle: Parent drive is located inside backup drive")
+			backupConfig.PanicStopped = true
 			return "", errors.New("Configuration Error. Skipping backup cycle.")
 		}
 	}
 
+	backupConfig.PanicStopped = false
+
 	todayFolderName := time.Now().Format("2006-01-02")
+	lastSnapshotTime := int64(0)
 	previousSnapshotExists := true
 	previousSnapshotName, err := getPreviousSnapshotName(backupConfig, todayFolderName)
 	if err != nil {
 		previousSnapshotExists = false
 	}
+
 	snapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName)
-	previousSnapshotLocation := filepath.Join(backupConfig.DiskPath, "/version/", previousSnapshotName)
+	previousSnapshotLocation := ""
+	var previousSnapshotMap *LinkFileMap
+	if previousSnapshotExists {
+		previousSnapshotLocation = filepath.Join(backupConfig.DiskPath, "/version/", previousSnapshotName)
+		previousSnapshotMap, _ = readLinkFile(previousSnapshotLocation)
+		lastSnapshotTime = lastModTime(previousSnapshotLocation)
+	}
 
 	//Create today folder if not exist
 	if !fileExists(snapshotLocation) {
@@ -51,7 +66,6 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 	}
 
 	//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
@@ -64,86 +78,115 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 	deletedFileList := map[string]string{}
 
 	//First pass: Check if there are any updated file from source and backup it to backup drive
-	fastWalk(parentRootAbs, func(filename string) error {
-		if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
+	log.Println("[HybridBackup] Snapshot Stage 1 - Started " + backupConfig.JobName)
+	rootAbs, _ := filepath.Abs(backupConfig.ParentPath)
+	rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
+	err = fastWalk(parentRootAbs, func(filename string) error {
+		if filepath.Ext(filename) == ".db" || filepath.Ext(filename) == ".lock" {
+			//Reserved filename, skipping
+			return nil
+		}
+
+		if filepath.Ext(filename) == ".datalink" {
 			//Reserved filename, skipping
 			return nil
 		}
 
 		//Get the target paste location
-		rootAbs, _ := filepath.Abs(backupConfig.ParentPath)
 		fileAbs, _ := filepath.Abs(filename)
-
-		rootAbs = filepath.ToSlash(filepath.Clean(rootAbs))
 		fileAbs = filepath.ToSlash(filepath.Clean(fileAbs))
 
 		relPath := strings.ReplaceAll(fileAbs, rootAbs, "")
 		fileBackupLocation := filepath.Join(backupConfig.DiskPath, "/version/", todayFolderName, relPath)
 		yesterdayBackupLocation := filepath.Join(previousSnapshotLocation, relPath)
 
-		//Check if the file exists
+		//Check if the file exists in previous snapshot folder
 		if !fileExists(yesterdayBackupLocation) {
-			//This file not in last snapshot location.
-			//Check if it is in previous snapshot map
+			//Not exists in snapshot folder. Search the link file
+			//fmt.Println("File not in last snapshot", yesterdayBackupLocation, previousSnapshotLocation, relPath)
+
 			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 fileHashMatch {
-						//append this record to this snapshot linkdata file
-						linkedFileList[relPath] = nameOfSnapshot
+					//Linked file exists. Check for changes
+					if lastModTime(fileAbs) > lastModTime(linkedSnapshotLocation) {
+						//This has changed recently. Match their hash to see if it is identical
+						fileHashMatch, err := fileHashIdentical(fileAbs, linkedSnapshotOriginalFile)
+						if err != nil {
+							return nil
+						}
+
+						if fileHashMatch {
+							//append this record to this snapshot linkdata file
+							linkedFileList[relPath] = nameOfSnapshot
+						} else {
+							//File hash mismatch. Do file copy to renew data
+							err = copyFileToBackupLocation(backupConfig, filename, fileBackupLocation)
+							if err != nil {
+								return err
+							}
+							copiedFileList = append(copiedFileList, fileBackupLocation)
+						}
 					} else {
-						//File hash mismatch. Do file copy to renew data
-						copyFileToBackupLocation(filename, fileBackupLocation)
-						copiedFileList = append(copiedFileList, fileBackupLocation)
+						//It hasn't been changed since last snapshot
+						linkedFileList[relPath] = nameOfSnapshot
 					}
+
 				} else {
 					//Invalid snapshot linkage. Assume new and do copy
 					log.Println("[HybridBackup] Link lost. Cloning source file to snapshot.")
-					copyFileToBackupLocation(filename, fileBackupLocation)
+					err = copyFileToBackupLocation(backupConfig, filename, fileBackupLocation)
+					if err != nil {
+						return err
+					}
 					copiedFileList = append(copiedFileList, fileBackupLocation)
 				}
 
 			} else {
 				//This file is not in snapshot link file.
 				//This is new file. Copy it to backup
-				copyFileToBackupLocation(filename, fileBackupLocation)
+				err = copyFileToBackupLocation(backupConfig, filename, fileBackupLocation)
+				if err != nil {
+					return err
+				}
 				copiedFileList = append(copiedFileList, fileBackupLocation)
 			}
 
 		} else if fileExists(yesterdayBackupLocation) {
 			//The file exists in the last snapshot
-			//Check if their hash is the same. If no, update it
-			fileHashMatch, err := fileHashIdentical(fileAbs, yesterdayBackupLocation)
-			if err != nil {
-				return nil
-			}
-
-			if !fileHashMatch {
-				//Hash mismatch. Overwrite the file
-				if !fileExists(filepath.Dir(fileBackupLocation)) {
-					os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
+			if lastModTime(fileAbs) > lastSnapshotTime {
+				//Check if their hash is the same. If no, update it
+				fileHashMatch, err := fileHashIdentical(fileAbs, yesterdayBackupLocation)
+				if err != nil {
+					return nil
 				}
 
-				err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
-				if err != nil {
-					log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
+				if !fileHashMatch {
+					//Hash mismatch. Overwrite the file
+					if !fileExists(filepath.Dir(fileBackupLocation)) {
+						os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
+					}
+
+					err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
+					if err != nil {
+						log.Println("[HybridBackup] Copy Failed for file "+filepath.Base(fileAbs), err.Error(), " Skipping.")
+					} else {
+						//No problem. Add this filepath into the list
+						copiedFileList = append(copiedFileList, fileBackupLocation)
+					}
 				} else {
-					//No problem. Add this filepath into the list
-					copiedFileList = append(copiedFileList, fileBackupLocation)
+					//Create a link file for this relative path
+					linkedFileList[relPath] = previousSnapshotName
 				}
+
 			} else {
-				//Create a link file for this relative path
+				//Not modified
 				linkedFileList[relPath] = previousSnapshotName
 			}
+
 		} else {
 			//Default case
 			lastModTime := lastModTime(fileAbs)
@@ -180,11 +223,17 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 		return nil
 	})
 
+	if err != nil {
+		//Copy error. Mostly because of disk fulled
+		return err.Error(), err
+	}
+
 	//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
+	log.Println("[HybridBackup] Snapshot Stage 2 - Started " + backupConfig.JobName)
 	if previousSnapshotExists {
 		fastWalk(previousSnapshotLocation, func(filename string) error {
-			if filepath.Base(filename) == "snapshot.datalink" {
+			if filepath.Ext(filename) == ".datalink" {
 				//System reserved file. Skip this
 				return nil
 			}
@@ -202,7 +251,6 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 			if !fileExists(sourcAssumeLocation) {
 				//File exists in yesterday snapshot but not in the current source
 				//Assume it has been deleted, create a dummy indicator file
-				//ioutil.WriteFile(todaySnapshotLocation+".deleted", []byte(""), 0755)
 				deletedFileList[relPath] = todayFolderName
 			}
 			return nil
@@ -218,10 +266,11 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 		}
 	}
 
-	//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 in today backup drive that didn't exists in the source drive
 	//For cases where the backup is applied to overwrite an eariler backup of the same day
+	log.Println("[HybridBackup] Snapshot Stage 3 - Started " + backupConfig.JobName)
 	fastWalk(snapshotLocation, func(filename string) error {
-		if filepath.Base(filename) == "aofs.db" || filepath.Base(filename) == "aofs.db.lock" {
+		if filepath.Ext(filename) == ".db" || filepath.Ext(filename) == ".lock" {
 			//Reserved filename, skipping
 			return nil
 		}
@@ -249,12 +298,14 @@ func executeVersionBackup(backupConfig *BackupTask) (string, error) {
 	})
 
 	//Generate linkfile for this snapshot
+	log.Println("[HybridBackup] Snapshot - Generating Linker File")
 	generateLinkFile(snapshotLocation, LinkFileMap{
 		UnchangedFile: linkedFileList,
 		DeletedFiles:  deletedFileList,
 	})
 
 	if err != nil {
+		log.Println("[HybridBackup] Error! ", err.Error())
 		return "", err
 	}
 
@@ -303,12 +354,47 @@ func getPreviousSnapshotName(backupConfig *BackupTask, currentSnapshotName strin
 	return previousSnapshotName, nil
 }
 
-func copyFileToBackupLocation(filename string, fileBackupLocation string) error {
+func copyFileToBackupLocation(task *BackupTask, filename string, fileBackupLocation string) error {
+	//Make dir the target dir if not exists
 	if !fileExists(filepath.Dir(fileBackupLocation)) {
 		os.MkdirAll(filepath.Dir(fileBackupLocation), 0755)
 	}
 
-	err := BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
+	//Check if the target disk can fit the new file
+	capinfo, err := dftool.GetCapacityInfoFromPath(filepath.Dir(fileBackupLocation))
+	if err == nil {
+		//Capacity info return normally. Estimate if the file will fit
+		srcSize := fileSize(filename)
+		diskSpace := capinfo.Avilable
+		if diskSpace < srcSize {
+			//Merge older snapshots. Maxium merging is 1 week
+			for i := 0; i < 6; i++ {
+				//Merge the oldest snapshot
+				err = mergeOldestSnapshots(task)
+				if err != nil {
+					log.Println("[HybridBackup] " + err.Error())
+					return errors.New("No space left on device")
+				}
+
+				//Check if there are enough space again
+				capinfo, err := dftool.GetCapacityInfoFromPath(filepath.Dir(fileBackupLocation))
+				if err != nil {
+					log.Println("[HybridBackup] " + err.Error())
+					return errors.New("No space left on device")
+				}
+				srcSize = fileSize(filename)
+				diskSpace = capinfo.Avilable
+				if diskSpace > srcSize {
+					//Space enough. Break out
+					break
+				}
+			}
+			log.Println("[HybridBackup] Error: No space left on device! Require ", srcSize, "bytes but only ", diskSpace, " bytes left")
+			return errors.New("No space left on device")
+		}
+	}
+
+	err = BufferedLargeFileCopy(filename, fileBackupLocation, 4096)
 	if err != nil {
 		log.Println("[HybridBackup] Failed to copy file: ", filepath.Base(filename)+". "+err.Error())
 		return err
@@ -366,7 +452,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,
@@ -380,3 +466,96 @@ func listVersionRestorables(task *BackupTask) ([]*RestorableFile, error) {
 	return restorableFiles, nil
 
 }
+
+//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) {
+		return nil, errors.New("Snapshot not exists")
+	}
+
+	if !fileExists(filepath.Join(targetSnapshotFolder, "snapshot.datalink")) {
+		return nil, errors.New("Snapshot datalink file not exists")
+	}
+
+	summary := SnapshotSummary{
+		ChangedFiles:   map[string]string{},
+		UnchangedFiles: map[string]string{},
+		DeletedFiles:   map[string]string{},
+	}
+
+	fastWalk(targetSnapshotFolder, func(filename string) error {
+		if filepath.Base(filename) == "snapshot.datalink" {
+			//Exceptional
+			return nil
+		}
+		relPath, err := filepath.Rel(targetSnapshotFolder, filename)
+		if err != nil {
+			return err
+		}
+
+		//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
+	})
+
+	//Generate the summary
+	linkFileMap, err := readLinkFile(targetSnapshotFolder)
+	if err != nil {
+		return nil, err
+	}
+
+	//Move the file map into result
+	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
+}

+ 29 - 0
mod/filesystem/config.go

@@ -3,6 +3,7 @@ package filesystem
 import (
 	"encoding/json"
 	"errors"
+	"strings"
 )
 
 //FileSystem configuration. Append more lines if required.
@@ -42,25 +43,53 @@ func ValidateOption(options *FileSystemOption) error {
 	if options.Uuid == "" {
 		return errors.New("File System Handler uuid cannot be empty")
 	}
+
+	//Check if uuid is reserved by system
+	if inSlice([]string{"user", "tmp", "share", "network"}, options.Uuid) {
+		return errors.New("This File System Handler UUID is reserved by the system")
+	}
+
 	if !fileExists(options.Path) {
 		return errors.New("Path not exists, given: " + options.Path)
 	}
 
+	//Check if access mode is supported
 	if !inSlice([]string{"readonly", "readwrite"}, options.Access) {
 		return errors.New("Not supported access mode: " + options.Access)
 	}
 
+	//Check if hierarchy is supported
 	if !inSlice([]string{"user", "public", "backup"}, options.Hierarchy) {
 		return errors.New("Not supported hierarchy: " + options.Hierarchy)
 	}
 
+	//Check disk format is supported
 	if !inSlice([]string{"ext4", "ext2", "ext3", "fat", "vfat", "ntfs"}, options.Filesystem) {
 		return errors.New("Not supported file system type: " + options.Filesystem)
 	}
 
+	//Check if mount point exists
 	if options.Mountpt != "" && !fileExists(options.Mountpt) {
 		return errors.New("Mount point not exists: " + options.Mountpt)
 	}
 
+	//This drive is backup drive
+	if options.Hierarchy == "backup" {
+		//Check if parent uid is not empty
+		if strings.TrimSpace(options.Parentuid) == "" {
+			return errors.New("Invalid backup source ID given")
+		}
+
+		//Check if the backup drive source and target are not the same drive
+		if options.Parentuid == options.Uuid {
+			return errors.New("Recursive backup detected. You cannot backup the backup drive itself.")
+		}
+
+		//Check if the backup mode exists
+		if !inSlice([]string{"basic", "nightly", "version"}, options.BackupMode) {
+			return errors.New("Invalid backup mode given")
+		}
+	}
+
 	return nil
 }

+ 8 - 8
mod/filesystem/fileOpr.go

@@ -390,7 +390,7 @@ func FileCopy(src string, dest string, mode string, progressUpdate func(int, str
 	//Check if the copy destination file already have an identical file
 	copiedFilename := filepath.Base(src)
 
-	if fileExists(dest + filepath.Base(src)) {
+	if fileExists(filepath.Join(dest, filepath.Base(src))) {
 		if mode == "" {
 			//Do not specific file exists principle
 			return errors.New("Destination file already exists.")
@@ -401,7 +401,7 @@ func FileCopy(src string, dest string, mode string, progressUpdate func(int, str
 		} else if mode == "overwrite" {
 			//Continue with the following code
 			//Check if the copy and paste dest are identical
-			if src == (dest + filepath.Base(src)) {
+			if filepath.ToSlash(filepath.Clean(src)) == filepath.ToSlash(filepath.Clean(filepath.Join(dest, filepath.Base(src)))) {
 				//Source and target identical. Cannot overwrite.
 				return errors.New("Source and destination paths are identical.")
 
@@ -412,7 +412,7 @@ func FileCopy(src string, dest string, mode string, progressUpdate func(int, str
 			newFilename := strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy" + filepath.Ext(src)
 			//Check if the newFilename already exists. If yes, continue adding suffix
 			duplicateCounter := 0
-			for fileExists(dest + newFilename) {
+			for fileExists(filepath.Join(dest, newFilename)) {
 				duplicateCounter++
 				newFilename = strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy(" + strconv.Itoa(duplicateCounter) + ")" + filepath.Ext(src)
 				if duplicateCounter > 1024 {
@@ -439,7 +439,7 @@ func FileCopy(src string, dest string, mode string, progressUpdate func(int, str
 	if IsDir(src) {
 		//Source file is directory. CopyFolder
 
-		realDest := dest + copiedFilename
+		realDest := filepath.Join(dest, copiedFilename)
 
 		//err := dircpy.Copy(src, realDest)
 
@@ -451,7 +451,7 @@ func FileCopy(src string, dest string, mode string, progressUpdate func(int, str
 
 	} else {
 		//Source is file only. Copy file.
-		realDest := dest + copiedFilename
+		realDest := filepath.Join(dest, copiedFilename)
 		source, err := os.Open(src)
 		if err != nil {
 			return err
@@ -502,7 +502,7 @@ func FileMove(src string, dest string, mode string, fastMove bool, progressUpdat
 	//Check if the target file already exists.
 	movedFilename := filepath.Base(src)
 
-	if fileExists(dest + filepath.Base(src)) {
+	if fileExists(filepath.Join(dest, filepath.Base(src))) {
 		//Handle cases where file already exists
 		if mode == "" {
 			//Do not specific file exists principle
@@ -513,7 +513,7 @@ func FileMove(src string, dest string, mode string, fastMove bool, progressUpdat
 		} else if mode == "overwrite" {
 			//Continue with the following code
 			//Check if the copy and paste dest are identical
-			if src == (dest + filepath.Base(src)) {
+			if filepath.ToSlash(filepath.Clean(src)) == filepath.ToSlash(filepath.Clean(filepath.Join(dest, filepath.Base(src)))) {
 				//Source and target identical. Cannot overwrite.
 				return errors.New("Source and destination paths are identical.")
 			}
@@ -523,7 +523,7 @@ func FileMove(src string, dest string, mode string, fastMove bool, progressUpdat
 			newFilename := strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy" + filepath.Ext(src)
 			//Check if the newFilename already exists. If yes, continue adding suffix
 			duplicateCounter := 0
-			for fileExists(dest + newFilename) {
+			for fileExists(filepath.Join(dest, newFilename)) {
 				duplicateCounter++
 				newFilename = strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)) + " - Copy(" + strconv.Itoa(duplicateCounter) + ")" + filepath.Ext(src)
 				if duplicateCounter > 1024 {

+ 1 - 1
mod/filesystem/filesystem.go

@@ -116,10 +116,10 @@ func NewFileSystemHandler(option FileSystemOption) (*FileSystemHandler, error) {
 				ParentUID:         option.Parentuid,
 				Mode:              option.BackupMode,
 				DeleteFileMarkers: map[string]int64{},
+				PanicStopped:      false,
 			}
 
 		}
-
 		//Create the fsdb for this handler
 		fsdb, err := db.NewDatabase(filepath.ToSlash(filepath.Join(filepath.Clean(option.Path), "aofs.db")), false)
 		if err != nil {

+ 13 - 0
mod/filesystem/fsextend/fsextend.go

@@ -0,0 +1,13 @@
+package fsextend
+
+/*
+	fsextend.go
+
+	This module extend the file system handler function to virtualized / emulated
+	interfaces
+*/
+
+type VirtualizedFileSystemPathResolver interface {
+	VirtualPathToRealPath(string) (string, error)
+	RealPathToVirtualPath(string) (string, error)
+}

+ 86 - 0
mod/filesystem/fssort/fssort.go

@@ -0,0 +1,86 @@
+package fssort
+
+import (
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+)
+
+type sortBufferedStructure struct {
+	Filename string
+	Filepath string
+	Filesize int64
+	ModTime  int64
+}
+
+/*
+	Quick utilties to sort file list according to different modes
+*/
+func SortFileList(filelistRealpath []string, sortMode string) []string {
+	//Build a filelist with information based on the given filelist
+	parsedFilelist := []*sortBufferedStructure{}
+	for _, file := range filelistRealpath {
+		thisFileInfo := sortBufferedStructure{
+			Filename: filepath.Base(file),
+			Filepath: file,
+		}
+
+		//Get Filesize
+		fi, err := os.Stat(file)
+		if err != nil {
+			thisFileInfo.Filesize = 0
+		} else {
+			thisFileInfo.Filesize = fi.Size()
+			thisFileInfo.ModTime = fi.ModTime().Unix()
+		}
+
+		parsedFilelist = append(parsedFilelist, &thisFileInfo)
+
+	}
+
+	//Sort the filelist
+	if sortMode == "default" {
+		//Sort by name, convert filename to window sorting methods
+		sort.Slice(parsedFilelist, func(i, j int) bool {
+			return strings.ToLower(parsedFilelist[i].Filename) < strings.ToLower(parsedFilelist[j].Filename)
+		})
+	} else if sortMode == "reverse" {
+		//Sort by reverse name
+		sort.Slice(parsedFilelist, func(i, j int) bool {
+			return strings.ToLower(parsedFilelist[i].Filename) > strings.ToLower(parsedFilelist[j].Filename)
+		})
+	} else if sortMode == "smallToLarge" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].Filesize < parsedFilelist[j].Filesize })
+	} else if sortMode == "largeToSmall" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].Filesize > parsedFilelist[j].Filesize })
+	} else if sortMode == "mostRecent" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].ModTime > parsedFilelist[j].ModTime })
+	} else if sortMode == "leastRecent" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].ModTime < parsedFilelist[j].ModTime })
+	}
+
+	results := []string{}
+	for _, sortedFile := range parsedFilelist {
+		results = append(results, sortedFile.Filepath)
+	}
+
+	return results
+}
+
+func SortModeIsSupported(sortMode string) bool {
+	if !contains(sortMode, []string{"default", "reverse", "smallToLarge", "largeToSmall", "mostRecent", "leastRecent"}) {
+		return false
+	}
+	return true
+}
+
+func contains(item string, slice []string) bool {
+	set := make(map[string]struct{}, len(slice))
+	for _, s := range slice {
+		set[s] = struct{}{}
+	}
+
+	_, ok := set[item]
+	return ok
+}

+ 1 - 0
mod/filesystem/metadata/metadata.go

@@ -68,6 +68,7 @@ func (rh *RenderHandler) LoadCache(file string, generateOnly bool) (string, erro
 	//Create a cache folder
 	cacheFolder := filepath.ToSlash(filepath.Clean(filepath.Dir(file))) + "/.cache/"
 	os.Mkdir(cacheFolder, 0755)
+
 	hidden.HideFile(cacheFolder)
 
 	//Check if cache already exists. If yes, return the image from the cache folder

+ 3 - 20
mod/filesystem/metadata/video.go

@@ -9,9 +9,9 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"runtime"
 
 	"github.com/oliamb/cutter"
+	"imuslab.com/arozos/mod/apt"
 )
 
 func generateThumbnailForVideo(cacheFolder string, file string, generateOnly bool) (string, error) {
@@ -75,23 +75,6 @@ func generateThumbnailForVideo(cacheFolder string, file string, generateOnly boo
 }
 
 func pkg_exists(pkgname string) bool {
-	if runtime.GOOS == "windows" {
-		//Check if the command already exists in windows path paramters.
-		cmd := exec.Command("where", pkgname, "2>", "nul")
-		_, err := cmd.CombinedOutput()
-		if err != nil {
-			return false
-		}
-		return true
-	} else if runtime.GOOS == "linux" {
-		cmd := exec.Command("which", pkgname)
-		out, _ := cmd.CombinedOutput()
-
-		if len(string(out)) > 1 {
-			return true
-		} else {
-			return false
-		}
-	}
-	return false
+	installed, _ := apt.PackageExists(pkgname)
+	return installed
 }

+ 56 - 0
mod/filesystem/shortcut/shortcut.go

@@ -0,0 +1,56 @@
+package shortcut
+
+import (
+	"errors"
+	"io/ioutil"
+	"strings"
+
+	"imuslab.com/arozos/mod/common"
+)
+
+/*
+	A simple package to better handle shortcuts in ArozOS
+
+	Author: tobychui
+*/
+
+//A shortcut representing struct
+type ShortcutData struct {
+	Type string //The type of shortcut
+	Name string //The name of the shortcut
+	Path string //The path of shortcut
+	Icon string //The icon of shortcut
+}
+
+func ReadShortcut(shortcutFile string) (*ShortcutData, error) {
+	if common.FileExists(shortcutFile) {
+		content, err := ioutil.ReadFile(shortcutFile)
+		if err != nil {
+			return nil, err
+		}
+
+		//Split the content of the shortcut files into lines
+		fileContent := strings.ReplaceAll(strings.TrimSpace(string(content)), "\r\n", "\n")
+		lines := strings.Split(fileContent, "\n")
+
+		if len(lines) < 4 {
+			return nil, errors.New("Corrupted Shortcut File")
+		}
+
+		for i := 0; i < len(lines); i++ {
+			lines[i] = strings.TrimSpace(lines[i])
+		}
+
+		//Render it as shortcut data
+		result := ShortcutData{
+			Type: lines[0],
+			Name: lines[1],
+			Path: lines[2],
+			Icon: lines[3],
+		}
+
+		return &result, nil
+	} else {
+		return nil, errors.New("File not exists.")
+	}
+}

+ 12 - 0
mod/filesystem/static.go

@@ -14,6 +14,7 @@ import (
 	"net/url"
 
 	mimetype "github.com/gabriel-vasile/mimetype"
+	"imuslab.com/arozos/mod/filesystem/shortcut"
 )
 
 //Structure definations
@@ -27,6 +28,7 @@ type FileData struct {
 	Displaysize string
 	ModTime     int64
 	IsShared    bool
+	Shortcut    *shortcut.ShortcutData //This will return nil or undefined if it is not a shortcut file
 }
 
 type TrashedFile struct {
@@ -68,6 +70,15 @@ func GetFileDataFromPath(vpath string, realpath string, sizeRounding int) FileDa
 	fileSize := GetFileSize(realpath)
 	displaySize := GetFileDisplaySize(fileSize, sizeRounding)
 	modtime, _ := GetModTime(realpath)
+
+	var shortcutInfo *shortcut.ShortcutData = nil
+	if filepath.Ext(realpath) == ".shortcut" {
+		scd, err := shortcut.ReadShortcut(realpath)
+		if err == nil {
+			shortcutInfo = scd
+		}
+	}
+
 	return FileData{
 		Filename:    filepath.Base(realpath),
 		Filepath:    vpath,
@@ -77,6 +88,7 @@ func GetFileDataFromPath(vpath string, realpath string, sizeRounding int) FileDa
 		Displaysize: displaySize,
 		ModTime:     modtime,
 		IsShared:    false,
+		Shortcut:    shortcutInfo,
 	}
 
 }

+ 1 - 1
mod/info/hardwareinfo/sysinfo.go

@@ -1,4 +1,4 @@
-// +build linux darwin
+// +build linux
 
 package hardwareinfo
 

+ 235 - 0
mod/info/hardwareinfo/sysinfo_darwin.go

@@ -0,0 +1,235 @@
+// +build darwin
+
+package hardwareinfo
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+	"os/exec"
+	"strconv"
+	"strings"
+)
+
+/*
+	System Info
+	original author: HyperXraft
+	modified by: Alanyeung
+	original date:	2021-02-18
+	modified by: 2021-07-25
+
+	This module get the CPU information on different platform using
+	native terminal commands on FreeBSD platform
+
+	DEFINITIONS
+	===========
+	CPUModel: Refers to the Marketing name of the CPU, e.g. Intel Xeon E7 8890
+	CPUHardware: Refers to the CPUID name, e.g. GenuineIntel-6-3A-9
+	CPUArch: Refers to the ISA of the CPU, e.g. aarch64
+	CPUFreq: Refers to the CPU frequency in terms of gigahertz, e.g. 0.8GHz
+*/
+
+//usbinfo from https://apple.stackexchange.com/questions/170105/list-usb-devices-on-osx-command-line
+const unknown_string = "??? "
+const query_frequency_command = "sysctl machdep.cpu.brand_string | awk '{print $NF}'"
+const query_cpumodel_command = "sysctl machdep.cpu.brand_string | awk '{for(i=1;++i<=NF-3;) printf $i\" \"; print $(NF-2)}'"
+const query_cpuarch_command = "sysctl hw.machine | awk '{print $NF}'"
+const query_cpuhardware_command = "sysctl machdep.cpu.stepping | awk '{print $NF}'"
+const query_netinfo_command = "networksetup -listallhardwareports"
+const query_usbinfo_command = "ioreg -p IOUSB -w0 | sed 's/[^o]*o //; s/@.*$//' | grep -v '^Root.*'"
+const query_memsize_command = "sysctl hw.memsize | awk '{print $NF}'"
+
+// GetCPUFreq() -> String
+// Returns the CPU frequency in the terms of MHz
+func GetCPUFreq() string {
+	shell := exec.Command("bash", "-c", query_frequency_command) // Run command
+	freqByteArr, err := shell.CombinedOutput()                   // Response from cmdline
+	if err != nil {                                              // If done w/ errors then
+		log.Println(err)
+		return unknown_string
+	}
+
+	freqStr := strings.ReplaceAll(string(freqByteArr), "GHz", "")
+	freqStr = strings.ReplaceAll(freqStr, "\n", "")
+	freqStr = strings.ReplaceAll(freqStr, " ", "")
+	freqFloat, _ := strconv.ParseFloat(freqStr, 8)
+	freqFloat = freqFloat * 1000
+	freqStrMHz := strconv.FormatFloat(freqFloat, 'f', -1, 64)
+
+	return freqStrMHz
+}
+
+// GetCPUModel -> String
+// Returns the CPU model name string
+func GetCPUModel() string {
+	shell := exec.Command("bash", "-c", query_cpumodel_command) // Run command
+	modelStr, err := shell.CombinedOutput()                     // Response from cmdline
+	if err != nil {                                             // If done w/ errors then
+		log.Println(err)
+		return unknown_string
+	}
+
+	return string(modelStr)
+}
+
+// GetCPUHardware -> String
+// Returns the CPU ID string
+func GetCPUHardware() string {
+	shell := exec.Command("bash", "-c", query_cpuhardware_command) // Run command
+	hwStr, err := shell.CombinedOutput()                           // Response from cmdline
+	if err != nil {                                                // If done w/ errors then
+		log.Println(err)
+		return unknown_string
+	}
+
+	return string(hwStr)
+}
+
+// GetCPUArch -> String
+// Returns the CPU architecture string
+func GetCPUArch() string {
+	shell := exec.Command("bash", "-c", query_cpuarch_command) // Run command
+	archStr, err := shell.CombinedOutput()                     // Response from cmdline
+	if err != nil {                                            // If done w/ errors then
+		log.Println(err)
+		return unknown_string
+	}
+
+	return string(archStr)
+}
+
+// Inherited code from sysinfo_window.go
+func GetCPUInfo(w http.ResponseWriter, r *http.Request) {
+	CPUInfo := CPUInfo{
+		Freq:        GetCPUFreq(),
+		Hardware:    GetCPUHardware(),
+		Instruction: GetCPUArch(),
+		Model:       GetCPUModel(),
+		Revision:    "unknown",
+	}
+
+	var jsonData []byte
+	jsonData, err := json.Marshal(CPUInfo)
+	if err != nil {
+		log.Println(err)
+	}
+	sendTextResponse(w, string(jsonData))
+}
+
+// Inherited code from sysinfo.go
+func Ifconfig(w http.ResponseWriter, r *http.Request) {
+	cmdin := query_netinfo_command
+	cmd := exec.Command("bash", "-c", cmdin)
+	networkInterfaces, err := cmd.CombinedOutput()
+	if err != nil {
+		networkInterfaces = []byte{}
+	}
+
+	nic := strings.Split(string(networkInterfaces), "\n")
+
+	var arr []string
+	for _, info := range nic {
+		thisInfo := string(info)
+		arr = append(arr, thisInfo)
+	}
+
+	var jsonData []byte
+	jsonData, err = json.Marshal(arr)
+	if err != nil {
+		log.Println(err)
+	}
+	sendTextResponse(w, string(jsonData))
+}
+
+// Inherited code from sysinfo.go
+func GetDriveStat(w http.ResponseWriter, r *http.Request) {
+	//Get drive status using df command
+	cmdin := `df -k | sed -e /Filesystem/d`
+	cmd := exec.Command("bash", "-c", cmdin)
+	dev, err := cmd.CombinedOutput()
+	if err != nil {
+		dev = []byte{}
+	}
+
+	drives := strings.Split(string(dev), "\n")
+
+	if len(drives) == 0 {
+		sendErrorResponse(w, "Invalid disk information")
+		return
+	}
+
+	var arr []LogicalDisk
+	for _, driveInfo := range drives {
+		if driveInfo == "" {
+			continue
+		}
+		for strings.Contains(driveInfo, "  ") {
+			driveInfo = strings.Replace(driveInfo, "  ", " ", -1)
+		}
+		driveInfoChunk := strings.Split(driveInfo, " ")
+		tmp, _ := strconv.Atoi(driveInfoChunk[3])
+		freespaceInByte := int64(tmp)
+
+		LogicalDisk := LogicalDisk{
+			DriveLetter: driveInfoChunk[8],
+			FileSystem:  driveInfoChunk[0],
+			FreeSpace:   strconv.FormatInt(freespaceInByte*1024, 10), //df show disk space in 1KB blocks
+		}
+		arr = append(arr, LogicalDisk)
+	}
+
+	var jsonData []byte
+	jsonData, err = json.Marshal(arr)
+	if err != nil {
+		log.Println(err)
+	}
+	sendTextResponse(w, string(jsonData))
+
+}
+
+// GetUSB(ResponseWriter, HttpRequest) -> nil
+// Takes in http.ResponseWriter w and *http.Request r,
+// Send TextResponse containing USB information extracted from shell in JSON
+func GetUSB(w http.ResponseWriter, r *http.Request) {
+	cmdin := query_usbinfo_command
+	cmd := exec.Command("bash", "-c", cmdin)
+	usbd, err := cmd.CombinedOutput()
+	if err != nil {
+		usbd = []byte{}
+	}
+
+	usbDrives := strings.Split(string(usbd), "\n")
+
+	var arr []string
+	for _, info := range usbDrives {
+		arr = append(arr, info)
+	}
+
+	var jsonData []byte
+	jsonData, err = json.Marshal(arr)
+	if err != nil {
+		log.Println(err)
+	}
+	sendTextResponse(w, string(jsonData))
+}
+
+// GetRamInfo(w ResponseWriter, r *HttpRequest) -> nil
+// Takes in http.ResponseWriter w and *http.Request r,
+// Send TextResponse containing physical memory size
+// extracted from shell in JSON
+func GetRamInfo(w http.ResponseWriter, r *http.Request) {
+	cmd := exec.Command("bash", "-c", query_memsize_command)
+	out, _ := cmd.CombinedOutput()
+
+	strOut := string(out)
+	strOut = strings.ReplaceAll(strOut, "\n", "")
+	ramSize, _ := strconv.ParseInt(strOut, 10, 64)
+	ramSizeInt := ramSize
+
+	var jsonData []byte
+	jsonData, err := json.Marshal(ramSizeInt)
+	if err != nil {
+		log.Println(err)
+	}
+	sendTextResponse(w, string(jsonData))
+}

+ 63 - 7
mod/info/usageinfo/usageinfo.go

@@ -18,7 +18,9 @@ import (
 
 const query_cpuproc_command = "ps -eo pcpu,pid,user,args | sort -k 1 -r | head -10"
 const query_freemem_command = "top -d1 | sed '4q;d' | awk '{print $(NF-1)}'"
+const query_freemem_command_darwin = "ps -A -o %mem | awk '{mem += $1} END {print mem}'"
 const query_phymem_command = "sysctl hw.physmem | awk '{print $NF}'"
+const query_phymem_command_darwin = "sysctl hw.memsize | awk '{print $NF}'"
 
 //Get CPU Usage in percentage
 func GetCPUUsage() float64 {
@@ -35,7 +37,7 @@ func GetCPUUsage() float64 {
 			usage = 0
 		}
 		usage = s
-	} else if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" {
+	} else if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || runtime.GOOS == "darwin" {
 		//Get CPU first 10 processes uses most CPU resources
 		cmd := exec.Command("bash", "-c", query_cpuproc_command)
 		out, err := cmd.CombinedOutput()
@@ -61,19 +63,22 @@ func GetCPUUsage() float64 {
 		queryNCPUCommand := ""
 		if runtime.GOOS == "linux" {
 			queryNCPUCommand = "nproc"
-		} else if runtime.GOOS == "freebsd" {
+		} else if runtime.GOOS == "freebsd" || runtime.GOOS == "darwin" {
 			queryNCPUCommand = "sysctl hw.ncpu | awk '{print $NF}'"
 		}
 
-		// Get CPU core count
-		cmd = exec.Command(queryNCPUCommand)
+		// Get CPU core count (freebsd way)
+		if runtime.GOOS == "freebsd" {
+			cmd = exec.Command(queryNCPUCommand)
+		} else if runtime.GOOS == "darwin" {
+			cmd = exec.Command("bash", "-c", queryNCPUCommand)
+		}
 		out, err = cmd.CombinedOutput()
 		if err != nil {
 			return usageCounter
 		}
-
 		// Divide total CPU usage by processes by total CPU core count
-		coreCount, err := strconv.Atoi(string(out))
+		coreCount, err := strconv.Atoi(strings.TrimSpace(string(out)))
 		if err != nil {
 			coreCount = 1
 		}
@@ -164,6 +169,7 @@ func GetNumericRAMUsage() (int64, int64) {
 
 		// Get usused memory size (free)
 		cmd := exec.Command("bash", "-c", query_freemem_command)
+
 		freeMemByteArr, err := cmd.CombinedOutput()
 		if err != nil {
 			return usedRam, totalRam
@@ -194,7 +200,29 @@ func GetNumericRAMUsage() (int64, int64) {
 
 		totalRam = int64(total)
 		usedRam = int64(used)
+		return usedRam, totalRam
+	} else if runtime.GOOS == "darwin" {
+		cmd := exec.Command("bash", "-c", query_freemem_command_darwin)
+		freeMemStr, err := cmd.CombinedOutput()
+		if err != nil {
+			return usedRam, totalRam
+		}
+		cmd = exec.Command("bash", "-c", query_phymem_command_darwin)
+		phyMemStr, err := cmd.CombinedOutput()
+		if err != nil {
+			return usedRam, totalRam
+		}
 
+		freeMem, err := strconv.ParseFloat(strings.TrimSpace(string(freeMemStr)), 10)
+		if err != nil {
+			return usedRam, totalRam
+		}
+		phyMem, err := strconv.ParseInt(strings.TrimSpace(string(phyMemStr)), 10, 64)
+		if err != nil {
+			return usedRam, totalRam
+		}
+		totalRam = phyMem
+		usedRam = int64(float64(phyMem) - float64(phyMem)*freeMem)
 		return usedRam, totalRam
 	}
 	return -1, -1
@@ -274,7 +302,6 @@ func GetRAMUsage() (string, string, float64) {
 		freeMemStr := string(freeMemByteArr)
 		freeMemStr = strings.ReplaceAll(freeMemStr, "\n", "")
 		freeMemSize, err := strconv.ParseFloat(strings.ReplaceAll(string(freeMemStr), "M", ""), 10)
-
 		// Get phy memory size
 		cmd = exec.Command("bash", "-c", query_phymem_command)
 		phyMemByteArr, err := cmd.CombinedOutput()
@@ -298,6 +325,35 @@ func GetRAMUsage() (string, string, float64) {
 
 		usedPercentage = usedRAMSizeFloat / phyMemSizeFloat * 100
 
+		return usedRam, totalRam, usedPercentage
+	} else if runtime.GOOS == "darwin" {
+		cmd := exec.Command("bash", "-c", query_freemem_command_darwin)
+		freeMemStr, err := cmd.CombinedOutput()
+		if err != nil {
+			return usedRam, totalRam, usedPercentage
+		}
+		cmd = exec.Command("bash", "-c", query_phymem_command_darwin)
+		phyMemStr, err := cmd.CombinedOutput()
+		if err != nil {
+			return usedRam, totalRam, usedPercentage
+		}
+		freeMemSizeFloat, err := strconv.ParseFloat(strings.TrimSpace(string(freeMemStr)), 10)
+		if err != nil {
+			return usedRam, totalRam, usedPercentage
+		}
+		phyMemSizeFloat, err := strconv.ParseFloat(strings.TrimSpace(string(phyMemStr)), 10)
+		if err != nil {
+			return usedRam, totalRam, usedPercentage
+		}
+		phyMemSizeFloat = phyMemSizeFloat / 1048576
+		phyMemSizeFloat = math.Floor(phyMemSizeFloat)
+		totalRam = strconv.FormatFloat(phyMemSizeFloat, 'f', -1, 64) + "MB"
+
+		usedRAMSizeFloat := float64(phyMemSizeFloat) - float64(phyMemSizeFloat)*(1-(freeMemSizeFloat/100))
+		usedRAMSizeFloat = math.Floor(usedRAMSizeFloat)
+		usedRam = strconv.FormatFloat(usedRAMSizeFloat, 'f', -1, 64) + "MB"
+
+		usedPercentage = freeMemSizeFloat
 		return usedRam, totalRam, usedPercentage
 	}
 

+ 2 - 0
mod/modules/module.go

@@ -78,6 +78,8 @@ func (m *ModuleHandler) HandleDefaultLauncher(w http.ResponseWriter, r *http.Req
 	ext, _ := mv(r, "ext", false)
 	moduleName, _ := mv(r, "module", false)
 
+	ext = strings.ToLower(ext)
+
 	//Check if the default folder exists.
 	if opr == "get" {
 		//Get the opener for this file type

+ 21 - 0
mod/network/dynamicproxy/dpcore/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-present tobychui
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 394 - 0
mod/network/dynamicproxy/dpcore/dpcore.go

@@ -0,0 +1,394 @@
+package dpcore
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"net/http"
+	"net/url"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+)
+
+var onExitFlushLoop func()
+
+const (
+	defaultTimeout = time.Minute * 5
+)
+
+// ReverseProxy is an HTTP Handler that takes an incoming request and
+// sends it to another server, proxying the response back to the
+// client, support http, also support https tunnel using http.hijacker
+type ReverseProxy struct {
+	// Set the timeout of the proxy server, default is 5 minutes
+	Timeout time.Duration
+
+	// Director must be a function which modifies
+	// the request into a new request to be sent
+	// using Transport. Its response is then copied
+	// back to the original client unmodified.
+	// Director must not access the provided Request
+	// after returning.
+	Director func(*http.Request)
+
+	// The transport used to perform proxy requests.
+	// default is http.DefaultTransport.
+	Transport http.RoundTripper
+
+	// FlushInterval specifies the flush interval
+	// to flush to the client while copying the
+	// response body. If zero, no periodic flushing is done.
+	FlushInterval time.Duration
+
+	// ErrorLog specifies an optional logger for errors
+	// that occur when attempting to proxy the request.
+	// If nil, logging goes to os.Stderr via the log package's
+	// standard logger.
+	ErrorLog *log.Logger
+
+	// ModifyResponse is an optional function that
+	// modifies the Response from the backend.
+	// If it returns an error, the proxy returns a StatusBadGateway error.
+	ModifyResponse func(*http.Response) error
+
+	//Prepender is an optional prepend text for URL rewrite
+	//
+	Prepender string
+}
+
+type requestCanceler interface {
+	CancelRequest(req *http.Request)
+}
+
+func NewDynamicProxyCore(target *url.URL, prepender string) *ReverseProxy {
+	targetQuery := target.RawQuery
+	director := func(req *http.Request) {
+		req.URL.Scheme = target.Scheme
+		req.URL.Host = target.Host
+		req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
+
+		// If Host is empty, the Request.Write method uses
+		// the value of URL.Host.
+		// force use URL.Host
+		req.Host = req.URL.Host
+		if targetQuery == "" || req.URL.RawQuery == "" {
+			req.URL.RawQuery = targetQuery + req.URL.RawQuery
+		} else {
+			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
+		}
+
+		if _, ok := req.Header["User-Agent"]; !ok {
+			req.Header.Set("User-Agent", "")
+		}
+	}
+
+	return &ReverseProxy{
+		Director:  director,
+		Prepender: prepender,
+	}
+}
+
+func singleJoiningSlash(a, b string) string {
+	aslash := strings.HasSuffix(a, "/")
+	bslash := strings.HasPrefix(b, "/")
+	switch {
+	case aslash && bslash:
+		return a + b[1:]
+	case !aslash && !bslash:
+		return a + "/" + b
+	}
+	return a + b
+}
+
+func copyHeader(dst, src http.Header) {
+	for k, vv := range src {
+		for _, v := range vv {
+			dst.Add(k, v)
+		}
+	}
+}
+
+// Hop-by-hop headers. These are removed when sent to the backend.
+// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
+var hopHeaders = []string{
+	//"Connection",
+	"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
+	"Keep-Alive",
+	"Proxy-Authenticate",
+	"Proxy-Authorization",
+	"Te",      // canonicalized version of "TE"
+	"Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522
+	"Transfer-Encoding",
+	//"Upgrade",
+}
+
+func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
+	if p.FlushInterval != 0 {
+		if wf, ok := dst.(writeFlusher); ok {
+			mlw := &maxLatencyWriter{
+				dst:     wf,
+				latency: p.FlushInterval,
+				done:    make(chan bool),
+			}
+
+			go mlw.flushLoop()
+			defer mlw.stop()
+			dst = mlw
+		}
+	}
+
+	io.Copy(dst, src)
+}
+
+type writeFlusher interface {
+	io.Writer
+	http.Flusher
+}
+
+type maxLatencyWriter struct {
+	dst     writeFlusher
+	latency time.Duration
+	mu      sync.Mutex
+	done    chan bool
+}
+
+func (m *maxLatencyWriter) Write(b []byte) (int, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.dst.Write(b)
+}
+
+func (m *maxLatencyWriter) flushLoop() {
+	t := time.NewTicker(m.latency)
+	defer t.Stop()
+	for {
+		select {
+		case <-m.done:
+			if onExitFlushLoop != nil {
+				onExitFlushLoop()
+			}
+			return
+		case <-t.C:
+			m.mu.Lock()
+			m.dst.Flush()
+			m.mu.Unlock()
+		}
+	}
+}
+
+func (m *maxLatencyWriter) stop() {
+	m.done <- true
+}
+
+func (p *ReverseProxy) logf(format string, args ...interface{}) {
+	if p.ErrorLog != nil {
+		p.ErrorLog.Printf(format, args...)
+	} else {
+		log.Printf(format, args...)
+	}
+}
+
+func removeHeaders(header http.Header) {
+	// Remove hop-by-hop headers listed in the "Connection" header.
+	if c := header.Get("Connection"); c != "" {
+		for _, f := range strings.Split(c, ",") {
+			if f = strings.TrimSpace(f); f != "" {
+				header.Del(f)
+			}
+		}
+	}
+
+	// Remove hop-by-hop headers
+	for _, h := range hopHeaders {
+		if header.Get(h) != "" {
+			header.Del(h)
+		}
+	}
+
+	if header.Get("A-Upgrade") != "" {
+		header.Set("Upgrade", header.Get("A-Upgrade"))
+		header.Del("A-Upgrade")
+	}
+}
+
+func addXForwardedForHeader(req *http.Request) {
+	if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
+		// If we aren't the first proxy retain prior
+		// X-Forwarded-For information as a comma+space
+		// separated list and fold multiple headers into one.
+		if prior, ok := req.Header["X-Forwarded-For"]; ok {
+			clientIP = strings.Join(prior, ", ") + ", " + clientIP
+		}
+		req.Header.Set("X-Forwarded-For", clientIP)
+	}
+}
+
+func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request) error {
+	transport := p.Transport
+	if transport == nil {
+		transport = http.DefaultTransport
+	}
+
+	outreq := new(http.Request)
+	// Shallow copies of maps, like header
+	*outreq = *req
+
+	if cn, ok := rw.(http.CloseNotifier); ok {
+		if requestCanceler, ok := transport.(requestCanceler); ok {
+			// After the Handler has returned, there is no guarantee
+			// that the channel receives a value, so to make sure
+			reqDone := make(chan struct{})
+			defer close(reqDone)
+			clientGone := cn.CloseNotify()
+
+			go func() {
+				select {
+				case <-clientGone:
+					requestCanceler.CancelRequest(outreq)
+				case <-reqDone:
+				}
+			}()
+		}
+	}
+
+	p.Director(outreq)
+	outreq.Close = false
+
+	// We may modify the header (shallow copied above), so we only copy it.
+	outreq.Header = make(http.Header)
+	copyHeader(outreq.Header, req.Header)
+
+	// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
+	removeHeaders(outreq.Header)
+
+	// Add X-Forwarded-For Header.
+	addXForwardedForHeader(outreq)
+
+	res, err := transport.RoundTrip(outreq)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		rw.WriteHeader(http.StatusBadGateway)
+		return err
+	}
+
+	// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
+	removeHeaders(res.Header)
+
+	if p.ModifyResponse != nil {
+		if err := p.ModifyResponse(res); err != nil {
+			p.logf("http: proxy error: %v", err)
+			rw.WriteHeader(http.StatusBadGateway)
+			return err
+		}
+	}
+
+	//Custom header rewriter functions
+	if res.Header.Get("Location") != "" {
+		//Custom redirection fto this rproxy relative path
+		fmt.Println(res.Header.Get("Location"))
+		res.Header.Set("Location", filepath.ToSlash(filepath.Join(p.Prepender, res.Header.Get("Location"))))
+	}
+	// Copy header from response to client.
+	copyHeader(rw.Header(), res.Header)
+
+	// The "Trailer" header isn't included in the Transport's response, Build it up from Trailer.
+	if len(res.Trailer) > 0 {
+		trailerKeys := make([]string, 0, len(res.Trailer))
+		for k := range res.Trailer {
+			trailerKeys = append(trailerKeys, k)
+		}
+		rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
+	}
+
+	rw.WriteHeader(res.StatusCode)
+	if len(res.Trailer) > 0 {
+		// Force chunking if we saw a response trailer.
+		// This prevents net/http from calculating the length for short
+		// bodies and adding a Content-Length.
+		if fl, ok := rw.(http.Flusher); ok {
+			fl.Flush()
+		}
+	}
+
+	p.copyResponse(rw, res.Body)
+	// close now, instead of defer, to populate res.Trailer
+	res.Body.Close()
+	copyHeader(rw.Header(), res.Trailer)
+
+	return nil
+}
+
+func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error {
+	hij, ok := rw.(http.Hijacker)
+	if !ok {
+		p.logf("http server does not support hijacker")
+		return errors.New("http server does not support hijacker")
+	}
+
+	clientConn, _, err := hij.Hijack()
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	proxyConn, err := net.Dial("tcp", req.URL.Host)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	// The returned net.Conn may have read or write deadlines
+	// already set, depending on the configuration of the
+	// Server, to set or clear those deadlines as needed
+	// we set timeout to 5 minutes
+	deadline := time.Now()
+	if p.Timeout == 0 {
+		deadline = deadline.Add(time.Minute * 5)
+	} else {
+		deadline = deadline.Add(p.Timeout)
+	}
+
+	err = clientConn.SetDeadline(deadline)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	err = proxyConn.SetDeadline(deadline)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	_, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	go func() {
+		io.Copy(clientConn, proxyConn)
+		clientConn.Close()
+		proxyConn.Close()
+	}()
+
+	io.Copy(proxyConn, clientConn)
+	proxyConn.Close()
+	clientConn.Close()
+
+	return nil
+}
+
+func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) error {
+	if req.Method == "CONNECT" {
+		err := p.ProxyHTTPS(rw, req)
+		return err
+	} else {
+		err := p.ProxyHTTP(rw, req)
+		return err
+	}
+}

+ 195 - 0
mod/network/dynamicproxy/dynamicproxy.go

@@ -0,0 +1,195 @@
+package dynamicproxy
+
+import (
+	"context"
+	"errors"
+	"log"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"imuslab.com/arozos/mod/network/dynamicproxy/dpcore"
+	"imuslab.com/arozos/mod/network/reverseproxy"
+)
+
+/*
+	Allow users to setup manual proxying for specific path
+
+*/
+type Router struct {
+	ListenPort        int
+	ProxyEndpoints    *sync.Map
+	SubdomainEndpoint *sync.Map
+	Running           bool
+	Root              *ProxyEndpoint
+	mux               http.Handler
+	useTLS            bool
+	server            *http.Server
+}
+
+type RouterOption struct {
+	Port int
+}
+
+type ProxyEndpoint struct {
+	Root       string
+	Domain     string
+	RequireTLS bool
+	Proxy      *dpcore.ReverseProxy `json:"-"`
+}
+
+type SubdomainEndpoint struct {
+	MatchingDomain string
+	Domain         string
+	RequireTLS     bool
+	Proxy          *reverseproxy.ReverseProxy `json:"-"`
+}
+
+type ProxyHandler struct {
+	Parent *Router
+}
+
+func NewDynamicProxy(port int) (*Router, error) {
+	proxyMap := sync.Map{}
+	domainMap := sync.Map{}
+	thisRouter := Router{
+		ListenPort:        port,
+		ProxyEndpoints:    &proxyMap,
+		SubdomainEndpoint: &domainMap,
+		Running:           false,
+		useTLS:            false,
+		server:            nil,
+	}
+
+	thisRouter.mux = &ProxyHandler{
+		Parent: &thisRouter,
+	}
+
+	return &thisRouter, nil
+}
+
+//Start the dynamic routing
+func (router *Router) StartProxyService() error {
+	//Create a new server object
+	if router.server != nil {
+		return errors.New("Reverse proxy server already running")
+	}
+
+	if router.Root == nil {
+		return errors.New("Reverse proxy router root not set")
+	}
+
+	router.server = &http.Server{Addr: ":" + strconv.Itoa(router.ListenPort), Handler: router.mux}
+	router.Running = true
+	go func() {
+		err := router.server.ListenAndServe()
+		log.Println("[DynamicProxy] " + err.Error())
+	}()
+
+	return nil
+}
+
+func (router *Router) StopProxyService() error {
+	if router.server == nil {
+		return errors.New("Reverse proxy server already stopped")
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	err := router.server.Shutdown(ctx)
+	if err != nil {
+		return err
+	}
+
+	//Discard the server object
+	router.server = nil
+	router.Running = false
+	return nil
+}
+
+/*
+	Add an URL into a custom proxy services
+*/
+func (router *Router) AddProxyService(rootname string, domain string, requireTLS bool) error {
+	if domain[len(domain)-1:] == "/" {
+		domain = domain[:len(domain)-1]
+	}
+
+	webProxyEndpoint := domain
+	if requireTLS {
+		webProxyEndpoint = "https://" + webProxyEndpoint
+	} else {
+		webProxyEndpoint = "http://" + webProxyEndpoint
+	}
+	//Create a new proxy agent for this root
+	path, err := url.Parse(webProxyEndpoint)
+	if err != nil {
+		return err
+	}
+
+	proxy := dpcore.NewDynamicProxyCore(path, rootname)
+
+	router.ProxyEndpoints.Store(rootname, &ProxyEndpoint{
+		Root:       rootname,
+		Domain:     domain,
+		RequireTLS: requireTLS,
+		Proxy:      proxy,
+	})
+
+	log.Println("Adding Proxy Rule: ", rootname+" to "+domain)
+	return nil
+}
+
+/*
+	Add an default router for the proxy server
+*/
+func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error {
+	if proxyLocation[len(proxyLocation)-1:] == "/" {
+		proxyLocation = proxyLocation[:len(proxyLocation)-1]
+	}
+
+	webProxyEndpoint := proxyLocation
+	if requireTLS {
+		webProxyEndpoint = "https://" + webProxyEndpoint
+	} else {
+		webProxyEndpoint = "http://" + webProxyEndpoint
+	}
+	//Create a new proxy agent for this root
+	path, err := url.Parse(webProxyEndpoint)
+	if err != nil {
+		return err
+	}
+
+	proxy := dpcore.NewDynamicProxyCore(path, "")
+
+	rootEndpoint := ProxyEndpoint{
+		Root:       "/",
+		Domain:     proxyLocation,
+		RequireTLS: requireTLS,
+		Proxy:      proxy,
+	}
+
+	router.Root = &rootEndpoint
+	return nil
+}
+
+//Do all the main routing in here
+func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if strings.Contains(r.Host, ".") {
+		//This might be a subdomain. See if there are any subdomain proxy router for this
+		sep := h.Parent.getSubdomainProxyEndpointFromHostname(r.Host)
+		if sep != nil {
+			h.subdomainRequest(w, r, sep)
+			return
+		}
+	}
+
+	targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(r.RequestURI)
+	if targetProxyEndpoint != nil {
+		h.proxyRequest(w, r, targetProxyEndpoint)
+	} else {
+		h.proxyRequest(w, r, h.Parent.Root)
+	}
+}

+ 99 - 0
mod/network/dynamicproxy/proxyRequestHandler.go

@@ -0,0 +1,99 @@
+package dynamicproxy
+
+import (
+	"log"
+	"net/http"
+	"net/url"
+
+	"imuslab.com/arozos/mod/network/websocketproxy"
+)
+
+func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *ProxyEndpoint {
+	var targetProxyEndpoint *ProxyEndpoint = nil
+	router.ProxyEndpoints.Range(func(key, value interface{}) bool {
+		rootname := key.(string)
+		if len(requestURI) >= len(rootname) && requestURI[:len(rootname)] == rootname {
+			thisProxyEndpoint := value.(*ProxyEndpoint)
+			targetProxyEndpoint = thisProxyEndpoint
+		}
+		return true
+	})
+
+	return targetProxyEndpoint
+}
+
+func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *SubdomainEndpoint {
+	var targetSubdomainEndpoint *SubdomainEndpoint = nil
+	ep, ok := router.SubdomainEndpoint.Load(hostname)
+	if ok {
+		targetSubdomainEndpoint = ep.(*SubdomainEndpoint)
+	}
+
+	return targetSubdomainEndpoint
+}
+
+func (router *Router) rewriteURL(rooturl string, requestURL string) string {
+	if len(requestURL) > len(rooturl) {
+		return requestURL[len(rooturl):]
+	}
+	return ""
+}
+
+func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *SubdomainEndpoint) {
+	r.Header.Set("X-Forwarded-Host", r.Host)
+	requestURL := r.URL.String()
+	if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
+		//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
+		r.Header.Set("A-Upgrade", "websocket")
+		wsRedirectionEndpoint := target.Domain
+		if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
+			//Append / to the end of the redirection endpoint if not exists
+			wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
+		}
+		if len(requestURL) > 0 && requestURL[:1] == "/" {
+			//Remove starting / from request URL if exists
+			requestURL = requestURL[1:]
+		}
+		u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL)
+		if target.RequireTLS {
+			u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
+		}
+		wspHandler := websocketproxy.NewProxy(u)
+		wspHandler.ServeHTTP(w, r)
+		return
+	}
+
+	r.Host = r.URL.Host
+	err := target.Proxy.ServeHTTP(w, r)
+	if err != nil {
+		log.Println(err.Error())
+	}
+
+}
+
+func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
+	rewriteURL := h.Parent.rewriteURL(target.Root, r.RequestURI)
+	r.URL, _ = url.Parse(rewriteURL)
+	r.Header.Set("X-Forwarded-Host", r.Host)
+	if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
+		//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
+		r.Header.Set("A-Upgrade", "websocket")
+		wsRedirectionEndpoint := target.Domain
+		if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
+			wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
+		}
+		u, _ := url.Parse("ws://" + wsRedirectionEndpoint + r.URL.String())
+		if target.RequireTLS {
+			u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
+		}
+		wspHandler := websocketproxy.NewProxy(u)
+		wspHandler.ServeHTTP(w, r)
+		return
+	}
+
+	r.Host = r.URL.Host
+	err := target.Proxy.ServeHTTP(w, r)
+	if err != nil {
+		log.Println(err.Error())
+	}
+}

+ 44 - 0
mod/network/dynamicproxy/subdomain.go

@@ -0,0 +1,44 @@
+package dynamicproxy
+
+import (
+	"log"
+	"net/url"
+
+	"imuslab.com/arozos/mod/network/reverseproxy"
+)
+
+/*
+	Add an URL intoa custom subdomain service
+
+*/
+
+func (router *Router) AddSubdomainRoutingService(hostnameWithSubdomain string, domain string, requireTLS bool) error {
+	if domain[len(domain)-1:] == "/" {
+		domain = domain[:len(domain)-1]
+	}
+
+	webProxyEndpoint := domain
+	if requireTLS {
+		webProxyEndpoint = "https://" + webProxyEndpoint
+	} else {
+		webProxyEndpoint = "http://" + webProxyEndpoint
+	}
+
+	//Create a new proxy agent for this root
+	path, err := url.Parse(webProxyEndpoint)
+	if err != nil {
+		return err
+	}
+
+	proxy := reverseproxy.NewReverseProxy(path)
+
+	router.SubdomainEndpoint.Store(hostnameWithSubdomain, &SubdomainEndpoint{
+		MatchingDomain: hostnameWithSubdomain,
+		Domain:         domain,
+		RequireTLS:     requireTLS,
+		Proxy:          proxy,
+	})
+
+	log.Println("Adding Subdomain Rule: ", hostnameWithSubdomain+" to "+domain)
+	return nil
+}

+ 9 - 6
mod/share/share.go

@@ -25,6 +25,7 @@ import (
 
 	uuid "github.com/satori/go.uuid"
 	"imuslab.com/arozos/mod/auth"
+	"imuslab.com/arozos/mod/common"
 	"imuslab.com/arozos/mod/database"
 	filesystem "imuslab.com/arozos/mod/filesystem"
 	"imuslab.com/arozos/mod/user"
@@ -121,7 +122,7 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 					w.WriteHeader(http.StatusUnauthorized)
 					w.Write([]byte("401 - Unauthorized"))
 				} else {
-					http.Redirect(w, r, "/login.system?redirect=/share?id="+id, 307)
+					http.Redirect(w, r, common.ConstructRelativePathFromRequestURL(r.RequestURI, "login.system")+"?redirect=/share?id="+id, 307)
 				}
 				return
 			} else {
@@ -134,7 +135,7 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 					w.WriteHeader(http.StatusUnauthorized)
 					w.Write([]byte("401 - Unauthorized"))
 				} else {
-					http.Redirect(w, r, "/login.system?redirect=/share?id="+id, 307)
+					http.Redirect(w, r, common.ConstructRelativePathFromRequestURL(r.RequestURI, "login.system")+"?redirect=/share?id="+id, 307)
 				}
 				return
 			}
@@ -174,7 +175,7 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 					w.WriteHeader(http.StatusUnauthorized)
 					w.Write([]byte("401 - Unauthorized"))
 				} else {
-					http.Redirect(w, r, "/login.system?redirect=/share?id="+id, 307)
+					http.Redirect(w, r, common.ConstructRelativePathFromRequestURL(r.RequestURI, "login.system")+"?redirect=/share?id="+id, 307)
 				}
 				return
 			}
@@ -200,7 +201,7 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 					w.WriteHeader(http.StatusUnauthorized)
 					w.Write([]byte("401 - Unauthorized"))
 				} else {
-					http.Redirect(w, r, "/login.system?redirect=/share?id="+id, 307)
+					http.Redirect(w, r, common.ConstructRelativePathFromRequestURL(r.RequestURI, "login.system")+"?redirect=/share?id="+id, 307)
 				}
 				return
 			}
@@ -370,12 +371,14 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 
 			}
 		} else {
-			if directDownload == true {
+			if directDownload {
 				//Serve the file directly
 				w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+strings.ReplaceAll(url.QueryEscape(filepath.Base(shareOption.FileRealPath)), "+", "%20"))
 				w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
 				http.ServeFile(w, r, shareOption.FileRealPath)
-			} else if directServe == true {
+			} else if directServe {
+				w.Header().Set("Access-Control-Allow-Origin", "*")
+				w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
 				w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
 				http.ServeFile(w, r, shareOption.FileRealPath)
 			} else {

+ 115 - 0
mod/storage/bridge/bridge.go

@@ -0,0 +1,115 @@
+package bridge
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"os"
+)
+
+/*
+	Bridge.go
+
+	This module handle File System Handler bridging cross different storage pool
+	Tricky to use, use with your own risk and make sure admin permission is
+	nessary for all request to this module.
+*/
+
+type Record struct {
+	Filename string
+}
+
+type BridgeConfig struct {
+	FSHUUID string
+	SPOwner string
+}
+
+func NewBridgeRecord(filename string) *Record {
+	return &Record{
+		Filename: filename,
+	}
+}
+
+//Read bridge record
+func (r *Record) ReadConfig() ([]*BridgeConfig, error) {
+	result := []*BridgeConfig{}
+
+	if _, err := os.Stat(r.Filename); os.IsNotExist(err) {
+		//File not exists. Create it
+		js, _ := json.Marshal([]*BridgeConfig{})
+		ioutil.WriteFile(r.Filename, js, 0775)
+	}
+
+	content, err := ioutil.ReadFile(r.Filename)
+	if err != nil {
+		return result, err
+	}
+
+	err = json.Unmarshal(content, &result)
+	if err != nil {
+		return result, err
+	}
+	return result, nil
+}
+
+//Append a new config into the Bridge Record
+func (r *Record) AppendToConfig(config *BridgeConfig) error {
+	currentConfigs, err := r.ReadConfig()
+	if err != nil {
+		return err
+	}
+
+	//Check if this config already exists
+	for _, previousConfig := range currentConfigs {
+		if previousConfig.FSHUUID == config.FSHUUID && previousConfig.SPOwner == config.SPOwner {
+			//Already exists
+			return errors.New("Idential config already registered")
+		}
+	}
+
+	currentConfigs = append(currentConfigs, config)
+
+	err = r.WriteConfig(currentConfigs)
+	return err
+}
+
+//Remove a given config from file
+func (r *Record) RemoveFromConfig(FSHUUID string, groupOwner string) error {
+	currentConfigs, err := r.ReadConfig()
+	if err != nil {
+		return err
+	}
+
+	newConfigs := []*BridgeConfig{}
+	for _, config := range currentConfigs {
+		if !(config.SPOwner == groupOwner && config.FSHUUID == FSHUUID) {
+			newConfigs = append(newConfigs, config)
+		}
+	}
+
+	err = r.WriteConfig(newConfigs)
+	return err
+
+}
+
+//Check if the given UUID in this pool is a bridge object
+func (r *Record) IsBridgedFSH(FSHUUID string, groupOwner string) (bool, error) {
+	currentConfigs, err := r.ReadConfig()
+	if err != nil {
+		return false, err
+	}
+
+	for _, config := range currentConfigs {
+		if config.SPOwner == groupOwner && config.FSHUUID == FSHUUID {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+//Write FSHConfig to disk
+func (r *Record) WriteConfig(config []*BridgeConfig) error {
+	js, _ := json.MarshalIndent(config, "", " ")
+	err := ioutil.WriteFile(r.Filename, js, 0775)
+	return err
+}

+ 44 - 0
mod/storage/du/diskusage.go

@@ -0,0 +1,44 @@
+// +build !windows
+
+package du
+
+import "syscall"
+
+// DiskUsage contains usage data and provides user-friendly access methods
+type DiskUsage struct {
+	stat *syscall.Statfs_t
+}
+
+// NewDiskUsages returns an object holding the disk usage of volumePath
+// or nil in case of error (invalid path, etc)
+func NewDiskUsage(volumePath string) *DiskUsage {
+
+	var stat syscall.Statfs_t
+	syscall.Statfs(volumePath, &stat)
+	return &DiskUsage{&stat}
+}
+
+// Free returns total free bytes on file system
+func (du *DiskUsage) Free() uint64 {
+	return du.stat.Bfree * uint64(du.stat.Bsize)
+}
+
+// Available return total available bytes on file system to an unprivileged user
+func (du *DiskUsage) Available() uint64 {
+	return uint64(du.stat.Bavail) * uint64(du.stat.Bsize)
+}
+
+// Size returns total size of the file system
+func (du *DiskUsage) Size() uint64 {
+	return uint64(du.stat.Blocks) * uint64(du.stat.Bsize)
+}
+
+// Used returns total bytes used in file system
+func (du *DiskUsage) Used() uint64 {
+	return du.Size() - du.Free()
+}
+
+// Usage returns percentage of use on the file system
+func (du *DiskUsage) Usage() float32 {
+	return float32(du.Used()) / float32(du.Size())
+}

+ 17 - 0
mod/storage/du/diskusage_test.go

@@ -0,0 +1,17 @@
+package du
+
+import (
+	"fmt"
+	"testing"
+)
+
+var KB = uint64(1024)
+
+func TestNewDiskUsage(t *testing.T) {
+	usage := NewDiskUsage(".")
+	fmt.Println("Free:", usage.Free()/(KB*KB))
+	fmt.Println("Available:", usage.Available()/(KB*KB))
+	fmt.Println("Size:", usage.Size()/(KB*KB))
+	fmt.Println("Used:", usage.Used()/(KB*KB))
+	fmt.Println("Usage:", usage.Usage()*100, "%")
+}

+ 55 - 0
mod/storage/du/diskusage_windows.go

@@ -0,0 +1,55 @@
+package du
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+type DiskUsage struct {
+	freeBytes  int64
+	totalBytes int64
+	availBytes int64
+}
+
+// NewDiskUsages returns an object holding the disk usage of volumePath
+// or nil in case of error (invalid path, etc)
+func NewDiskUsage(volumePath string) *DiskUsage {
+
+	h := syscall.MustLoadDLL("kernel32.dll")
+	c := h.MustFindProc("GetDiskFreeSpaceExW")
+
+	du := &DiskUsage{}
+
+	c.Call(
+		uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(volumePath))),
+		uintptr(unsafe.Pointer(&du.freeBytes)),
+		uintptr(unsafe.Pointer(&du.totalBytes)),
+		uintptr(unsafe.Pointer(&du.availBytes)))
+
+	return du
+}
+
+// Free returns total free bytes on file system
+func (du *DiskUsage) Free() uint64 {
+	return uint64(du.freeBytes)
+}
+
+// Available returns total available bytes on file system to an unprivileged user
+func (du *DiskUsage) Available() uint64 {
+	return uint64(du.availBytes)
+}
+
+// Size returns total size of the file system
+func (du *DiskUsage) Size() uint64 {
+	return uint64(du.totalBytes)
+}
+
+// Used returns total bytes used in file system
+func (du *DiskUsage) Used() uint64 {
+	return du.Size() - du.Free()
+}
+
+// Usage returns percentage of use on the file system
+func (du *DiskUsage) Usage() float32 {
+	return float32(du.Used()) / float32(du.Size())
+}

+ 1 - 1
mod/storage/ftp/aofs.go

@@ -225,7 +225,7 @@ func (a aofs) pathRewrite(path string) (string, *fs.FileSystemHandler, error) {
 		fsHandlers := a.userinfo.GetAllFileSystemHandler()
 		for _, fsh := range fsHandlers {
 			//Create a folder representation for this virtual directory
-			if fsh.UUID != "tmp" {
+			if !(fsh.UUID == "tmp" || fsh.Hierarchy == "backup") {
 				os.Mkdir(a.tmpFolder+fsh.UUID, 0755)
 			}
 

+ 7 - 1
mod/storage/ftp/drivers.go

@@ -5,6 +5,7 @@ import (
 	"errors"
 	"log"
 	"os"
+	"time"
 
 	ftp "github.com/fclairamb/ftpserverlib"
 )
@@ -60,6 +61,8 @@ func (m mainDriver) AuthUser(cc ftp.ClientContext, user string, pass string) (ft
 		accessOK := userinfo.UserIsInOneOfTheGroupOf(allowedPgs)
 
 		if !accessOK {
+			//log the signin request
+			m.userHandler.GetAuthAgent().Logger.LogAuthByRequestInfo(user, cc.RemoteAddr().String(), time.Now().Unix(), false, "ftp")
 			//Disconnect this user as he is not in the group that is allowed to access ftp
 			log.Println(userinfo.Username + " tries to access FTP endpoint with invalid permission settings.")
 			return nil, errors.New("User " + userinfo.Username + " has no permission to access FTP endpoint")
@@ -71,13 +74,16 @@ func (m mainDriver) AuthUser(cc ftp.ClientContext, user string, pass string) (ft
 
 		//Record username into connected user list
 		m.connectedUserList.Store(cc.ID(), userinfo.Username)
-
+		//log the signin request
+		m.userHandler.GetAuthAgent().Logger.LogAuthByRequestInfo(userinfo.Username, cc.RemoteAddr().String(), time.Now().Unix(), true, "ftp")
 		//Return the aofs object
 		return aofs{
 			userinfo:  userinfo,
 			tmpFolder: tmpFolder,
 		}, nil
 	} else {
+		//log the signin request
+		m.userHandler.GetAuthAgent().Logger.LogAuthByRequestInfo(user, cc.RemoteAddr().String(), time.Now().Unix(), false, "ftp")
 		return nil, errors.New("Invalid username or password")
 	}
 }

+ 6 - 8
mod/storage/static.go

@@ -1,18 +1,16 @@
 package storage
 
+import du "imuslab.com/arozos/mod/storage/du"
 
-import (
-	du "github.com/ricochet2200/go-disk-usage/du"
-)
 /*
 	File for putting return structs
 
 */
 
-func GetDriveCapacity(drive string) (uint64, uint64, uint64){
+func GetDriveCapacity(drive string) (uint64, uint64, uint64) {
 	usage := du.NewDiskUsage(drive)
-	free := usage.Free();
-	total := usage.Size();
-	avi := usage.Available();
+	free := usage.Free()
+	total := usage.Size()
+	avi := usage.Available()
 	return free, total, avi
-}
+}

+ 15 - 3
mod/storage/storage.go

@@ -63,7 +63,7 @@ func NewStoragePool(fsHandlers []*fs.FileSystemHandler, owner string) (*StorageP
 			if parentExists {
 				backupManager.AddTask(&backupConfig)
 			} else {
-				log.Println("*ERROR* Backup disk " + backupConfig.DiskUID + ":/ source disk not found: " + backupConfig.ParentUID + ":/ not exists!")
+				log.Println("*ERROR* Backup disk " + backupConfig.DiskUID + ":/ source disk not found: " + backupConfig.ParentUID + ":/ not exists or it is from other storage pool!")
 			}
 
 		}
@@ -77,6 +77,17 @@ func NewStoragePool(fsHandlers []*fs.FileSystemHandler, owner string) (*StorageP
 	}, nil
 }
 
+//Check if this storage pool contain this particular disk ID
+func (s *StoragePool) ContainDiskID(diskID string) bool {
+	for _, fsh := range s.Storages {
+		if fsh.UUID == diskID {
+			return true
+		}
+	}
+
+	return false
+}
+
 //Use to compare two StoragePool permissions leve
 func (s *StoragePool) HasHigherOrEqualPermissionThan(a *StoragePool) bool {
 	if s.OtherPermission == "readonly" && a.OtherPermission == "readwrite" {
@@ -89,13 +100,14 @@ func (s *StoragePool) HasHigherOrEqualPermissionThan(a *StoragePool) bool {
 
 //Close all fsHandler under this storage pool
 func (s *StoragePool) Close() {
+	//Close the running backup tasks
+	s.HyperBackupManager.Close()
+
 	//For each storage pool, close it
 	for _, fsh := range s.Storages {
 		fsh.Close()
 	}
 
-	//Close the running backup tasks
-	s.HyperBackupManager.Close()
 }
 
 //Helper function

+ 3 - 1
mod/storage/webdav/webdav.go

@@ -17,6 +17,7 @@ import (
 	"sort"
 	"strings"
 	"sync"
+	"time"
 
 	"imuslab.com/arozos/mod/network/webdav"
 	"imuslab.com/arozos/mod/user"
@@ -230,7 +231,8 @@ func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) {
 	authAgent := s.userHandler.GetAuthAgent()
 	passwordValid := authAgent.ValidateUsernameAndPassword(username, password)
 	if !passwordValid {
-		log.Println("Someone try to log into " + username + " WebDAV endpoint with incorrect password")
+		authAgent.Logger.LogAuthByRequestInfo(username, r.RemoteAddr, time.Now().Unix(), false, "webdav")
+		log.Println("Someone from " + r.RemoteAddr + " try to log into " + username + " WebDAV endpoint with incorrect password")
 		http.Error(w, "Invalid username or password", http.StatusUnauthorized)
 		return
 	}

+ 6 - 16
mod/subservice/subservice.go

@@ -91,21 +91,6 @@ func (sr *SubServiceRouter) Launch(servicePath string, startupMode bool) error {
 		binaryExecPath = binaryExecPath + "_" + runtime.GOOS + "_" + runtime.GOARCH
 	}
 
-	/*if runtime.GOOS == "linux" {
-		if runtime.GOARCH == "arm" {
-			binaryExecPath = binaryExecPath + "_linux_arm"
-		} else if runtime.GOARCH == "arm64" {
-			binaryExecPath = binaryExecPath + "_linux_arm64"
-		} else if runtime.GOARCH == "386" {
-			binaryExecPath = binaryExecPath + "_linux_386"
-		} else if runtime.GOARCH == "amd64" {
-			binaryExecPath = binaryExecPath + "_linux_amd64"
-		}
-	} else if runtime.GOOS == "darwin" {
-
-	}
-	*/
-
 	if runtime.GOOS == "windows" && !fileExists(servicePath+"/"+binaryExecPath) {
 		if startupMode {
 			log.Println("Failed to load subservice: "+serviceRoot, " File not exists "+servicePath+"/"+binaryExecPath+". Skipping this service")
@@ -261,7 +246,12 @@ func (sr *SubServiceRouter) Launch(servicePath string, startupMode bool) error {
 			absolutePath, _ = filepath.Abs(initPath)
 		}
 
-		cmd := exec.Command(absolutePath, "-port", ":"+intToString(thisServicePort), "-rpt", "http://localhost:"+intToString(sr.listenPort)+"/api/ajgi/interface")
+		servicePort := ":" + intToString(thisServicePort)
+		if fileExists(filepath.Join(servicePath, "/.intport")) {
+			servicePort = intToString(thisServicePort)
+		}
+
+		cmd := exec.Command(absolutePath, "-port", servicePort, "-rpt", "http://localhost:"+intToString(sr.listenPort)+"/api/ajgi/interface")
 		cmd.Stdout = os.Stdout
 		cmd.Stderr = os.Stderr
 		cmd.Dir = filepath.ToSlash(servicePath + "/")

+ 1 - 1
mod/time/scheduler/scheduler.go

@@ -84,7 +84,7 @@ func NewScheduler(userHandler *user.UserHandler, gateway *agi.Gateway, cronfile
 		}
 		stopChannel := thisScheduler.createTicker(1 * time.Minute)
 		thisScheduler.ticker = stopChannel
-		log.Println("Emulated Crontab Started - Scheduling Tasks")
+		log.Println("ArozOS System Scheduler Started")
 	}()
 
 	//Return the crontask

+ 13 - 45
mod/user/directoryHandler.go

@@ -4,7 +4,6 @@ import (
 	"errors"
 	"os"
 	"path/filepath"
-	"strings"
 
 	fs "imuslab.com/arozos/mod/filesystem"
 )
@@ -23,6 +22,19 @@ func (u *User) GetHomeDirectory() (string, error) {
 	return "", errors.New("User root not found. Is this a permission group instead of a real user?")
 }
 
+//Get all user Acessible file system handlers (ignore special fsh like backups)
+func (u *User) GetAllAccessibleFileSystemHandler() []*fs.FileSystemHandler {
+	results := []*fs.FileSystemHandler{}
+	fshs := u.GetAllFileSystemHandler()
+	for _, fsh := range fshs {
+		if fsh.Hierarchy != "backup" {
+			results = append(results, fsh)
+		}
+	}
+
+	return results
+}
+
 func (u *User) GetAllFileSystemHandler() []*fs.FileSystemHandler {
 	results := []*fs.FileSystemHandler{}
 	uuids := []string{}
@@ -214,47 +226,3 @@ func (u *User) GetFileSystemHandlerFromRealPath(rpath string) (*fs.FileSystemHan
 
 	return u.GetFileSystemHandlerFromVirtualPath(vpath)
 }
-
-/*
-
-	PRIVATE FUNCTIONS HANDLERS
-
-*/
-//Get a fs handler from a virtual path, quick function for getIDFromHandler + GetHandlerFromID
-func getHandlerFromVirtualPath(storages []*fs.FileSystemHandler, vpath string) (*fs.FileSystemHandler, error) {
-	vid, _, err := getIDFromVirtualPath(vpath)
-	if err != nil {
-		return &fs.FileSystemHandler{}, err
-	}
-
-	return getHandlerFromID(storages, vid)
-}
-
-//Get a fs handler from the given virtial device id
-func getHandlerFromID(storages []*fs.FileSystemHandler, vid string) (*fs.FileSystemHandler, error) {
-	for _, storage := range storages {
-		if storage.UUID == vid {
-			//This storage is the one we are looking at
-			return storage, nil
-		}
-	}
-
-	return &fs.FileSystemHandler{}, errors.New("Handler Not Found")
-}
-
-//Get the ID part of a virtual path, return ID, subpath and error
-func getIDFromVirtualPath(vpath string) (string, string, error) {
-	if strings.Contains(vpath, ":") == false {
-		return "", "", errors.New("Path missing Virtual Device ID. Given: " + vpath)
-	}
-
-	//Clean up the virutal path
-	vpath = filepath.ToSlash(filepath.Clean(vpath))
-
-	tmp := strings.Split(vpath, ":")
-	vdID := tmp[0]
-	pathSlice := tmp[1:]
-	path := strings.Join(pathSlice, ":")
-
-	return vdID, path, nil
-}

+ 52 - 0
mod/user/internal.go

@@ -0,0 +1,52 @@
+package user
+
+import (
+	"errors"
+	"path/filepath"
+	"strings"
+
+	fs "imuslab.com/arozos/mod/filesystem"
+)
+
+/*
+	Private functions
+*/
+
+//Get a fs handler from a virtual path, quick function for getIDFromHandler + GetHandlerFromID
+func getHandlerFromVirtualPath(storages []*fs.FileSystemHandler, vpath string) (*fs.FileSystemHandler, error) {
+	vid, _, err := getIDFromVirtualPath(vpath)
+	if err != nil {
+		return &fs.FileSystemHandler{}, err
+	}
+
+	return getHandlerFromID(storages, vid)
+}
+
+//Get a fs handler from the given virtial device id
+func getHandlerFromID(storages []*fs.FileSystemHandler, vid string) (*fs.FileSystemHandler, error) {
+	for _, storage := range storages {
+		if storage.UUID == vid {
+			//This storage is the one we are looking at
+			return storage, nil
+		}
+	}
+
+	return &fs.FileSystemHandler{}, errors.New("Handler Not Found")
+}
+
+//Get the ID part of a virtual path, return ID, subpath and error
+func getIDFromVirtualPath(vpath string) (string, string, error) {
+	if strings.Contains(vpath, ":") == false {
+		return "", "", errors.New("Path missing Virtual Device ID. Given: " + vpath)
+	}
+
+	//Clean up the virutal path
+	vpath = filepath.ToSlash(filepath.Clean(vpath))
+
+	tmp := strings.Split(vpath, ":")
+	vdID := tmp[0]
+	pathSlice := tmp[1:]
+	path := strings.Join(pathSlice, ":")
+
+	return vdID, path, nil
+}

+ 0 - 27
mod/user/user.go

@@ -68,10 +68,6 @@ func (u *UserHandler) UpdateStoragePool(newpool *storage.StoragePool) {
 	u.basePool = newpool
 }
 
-func (u *User) Parent() *UserHandler {
-	return u.parent
-}
-
 //Get User object from username
 func (u *UserHandler) GetUserInfoFromUsername(username string) (*User, error) {
 	//Check if user exists
@@ -156,26 +152,3 @@ func (u *UserHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Requ
 	}
 	return userObject, nil
 }
-
-//Remove the current user
-func (u *User) RemoveUser() {
-	//Remove the user storage quota settings
-	log.Println("Removing User Quota: ", u.Username)
-	u.StorageQuota.RemoveUserQuota()
-
-	//Remove the user authentication register
-	u.parent.authAgent.UnregisterUser(u.Username)
-}
-
-//Get the current user icon
-func (u *User) GetUserIcon() string {
-	var userIconpath []byte
-	u.parent.database.Read("auth", "profilepic/"+u.Username, &userIconpath)
-	return string(userIconpath)
-}
-
-//Set the current user icon
-func (u *User) SetUserIcon(base64data string) {
-	u.parent.database.Write("auth", "profilepic/"+u.Username, []byte(base64data))
-	return
-}

+ 31 - 0
mod/user/useropr.go

@@ -0,0 +1,31 @@
+package user
+
+import "log"
+
+//Get the user's handler
+func (u *User) Parent() *UserHandler {
+	return u.parent
+}
+
+//Remove the current user
+func (u *User) RemoveUser() {
+	//Remove the user storage quota settings
+	log.Println("Removing User Quota: ", u.Username)
+	u.StorageQuota.RemoveUserQuota()
+
+	//Remove the user authentication register
+	u.parent.authAgent.UnregisterUser(u.Username)
+}
+
+//Get the current user icon
+func (u *User) GetUserIcon() string {
+	var userIconpath []byte
+	u.parent.database.Read("auth", "profilepic/"+u.Username, &userIconpath)
+	return string(userIconpath)
+}
+
+//Set the current user icon
+func (u *User) SetUserIcon(base64data string) {
+	u.parent.database.Write("auth", "profilepic/"+u.Username, []byte(base64data))
+	return
+}

+ 1 - 1
oauth.go

@@ -29,7 +29,7 @@ func OAuthInit() {
 		Name:         "OAuth",
 		Desc:         "Allows external account access to system",
 		IconPath:     "SystemAO/advance/img/small_icon.png",
-		Group:        "Advance",
+		Group:        "Security",
 		StartDir:     "SystemAO/advance/oauth.html",
 		RequireAdmin: true,
 	})

+ 164 - 0
reverseproxy.go

@@ -0,0 +1,164 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+
+	module "imuslab.com/arozos/mod/modules"
+	"imuslab.com/arozos/mod/network/dynamicproxy"
+	prout "imuslab.com/arozos/mod/prouter"
+)
+
+var (
+	dynamicProxyRouter *dynamicproxy.Router
+)
+
+//Add user customizable reverse proxy
+func ReverseProxtInit() {
+
+	dprouter, err := dynamicproxy.NewDynamicProxy(80)
+	if err != nil {
+		log.Println(err.Error())
+		return
+	}
+
+	dynamicProxyRouter = dprouter
+
+	//Register the module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:        "Reverse Proxy",
+		Desc:        "Setup reverse proxy to other nearby services",
+		Group:       "System Settings",
+		IconPath:    "SystemAO/reverse_proxy/img/small_icon.png",
+		Version:     "1.0",
+		StartDir:    "SystemAO/reverse_proxy/index.html",
+		SupportFW:   true,
+		InitFWSize:  []int{1080, 580},
+		LaunchFWDir: "SystemAO/reverse_proxy/index.html",
+		SupportEmb:  false,
+	})
+
+	//Register HybridBackup storage restore endpoints
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "Reverse Proxy",
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	router.HandleFunc("/system/proxy/enable", ReverseProxyHandleOnOff)
+	router.HandleFunc("/system/proxy/add", ReverseProxyHandleAddEndpoint)
+	router.HandleFunc("/system/proxy/status", ReverseProxyStatus)
+	router.HandleFunc("/system/proxy/list", ReverseProxyList)
+
+	/*
+		dynamicProxyRouter.SetRootProxy("192.168.0.107:8080", false)
+		dynamicProxyRouter.AddSubdomainRoutingService("loopback.localhost", "localhost:8080", false)
+		dynamicProxyRouter.StartProxyService()
+		go func() {
+			time.Sleep(10 * time.Second)
+			dynamicProxyRouter.StopProxyService()
+			fmt.Println("Proxy stopped")
+		}()
+		log.Println("Dynamic Proxy service started")
+	*/
+}
+
+func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
+	enable, _ := mv(r, "enable", true) //Support root, vdir and subd
+	if enable == "true" {
+		err := dynamicProxyRouter.StartProxyService()
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+	} else {
+		err := dynamicProxyRouter.StopProxyService()
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+	}
+
+	sendOK(w)
+}
+
+func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
+	eptype, err := mv(r, "type", true) //Support root, vdir and subd
+	if err != nil {
+		sendErrorResponse(w, "type not defined")
+		return
+	}
+
+	endpoint, err := mv(r, "ep", true)
+	if err != nil {
+		sendErrorResponse(w, "endpoint not defined")
+		return
+	}
+
+	tls, _ := mv(r, "tls", true)
+	if tls == "" {
+		tls = "false"
+	}
+
+	useTLS := (tls == "true")
+
+	if eptype == "vdir" {
+		vdir, err := mv(r, "vdir", true)
+		if err != nil {
+			sendErrorResponse(w, "vdir not defined")
+			return
+		}
+		dynamicProxyRouter.AddProxyService(vdir, endpoint, useTLS)
+
+	} else if eptype == "subd" {
+		subdomain, err := mv(r, "subdomain", true)
+		if err != nil {
+			sendErrorResponse(w, "subdomain not defined")
+			return
+		}
+		dynamicProxyRouter.AddSubdomainRoutingService(subdomain, endpoint, useTLS)
+	} else if eptype == "root" {
+		dynamicProxyRouter.SetRootProxy(endpoint, useTLS)
+	}
+
+	sendOK(w)
+
+}
+
+func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
+	js, _ := json.Marshal(dynamicProxyRouter)
+	sendJSONResponse(w, string(js))
+}
+
+func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
+	eptype, err := mv(r, "type", true) //Support root, vdir and subd
+	if err != nil {
+		sendErrorResponse(w, "type not defined")
+		return
+	}
+
+	if eptype == "vdir" {
+		results := []*dynamicproxy.ProxyEndpoint{}
+		dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
+			results = append(results, value.(*dynamicproxy.ProxyEndpoint))
+			return true
+		})
+
+		js, _ := json.Marshal(results)
+		sendJSONResponse(w, string(js))
+	} else if eptype == "subd" {
+		results := []*dynamicproxy.SubdomainEndpoint{}
+		dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool {
+			results = append(results, value.(*dynamicproxy.SubdomainEndpoint))
+			return true
+		})
+		js, _ := json.Marshal(results)
+		sendJSONResponse(w, string(js))
+	} else {
+		sendErrorResponse(w, "Invalid type given")
+	}
+}

+ 0 - 2
start.sh.backup

@@ -1,2 +0,0 @@
-#!/bin/bash
-sudo ./arozos -hostname "ixtw" -allow_upnp=true -port 8082 -tls=true -tls_port 8443  -max_upload_size 25000

+ 2 - 2
startup.flags.go

@@ -20,8 +20,8 @@ func StartupFlagsInit() {
 	//Create a admin permission router for handling requests
 	//Register a boot flag modifier
 	registerSetting(settingModule{
-		Name:         "Startup",
-		Desc:         "Platform Startup Flags",
+		Name:         "Runtime",
+		Desc:         "Change startup paramter in runtime",
 		IconPath:     "SystemAO/info/img/small_icon.png",
 		Group:        "Info",
 		StartDir:     "SystemAO/boot/bootflags.html",

+ 14 - 12
startup.go

@@ -39,26 +39,28 @@ func RunStartup() {
 	StorageInit() //See storage.go
 
 	//5. Startup user and permission sytem
-	UserSystemInit()       //See user.go
-	permissionInit()       //Register permission interface after user
-	RegisterSystemInit()   //See register.go
-	OAuthInit()            //Oauth system init
-	GroupStoragePoolInit() //Register permission groups's storage pool, require permissionInit()
+	UserSystemInit()        //See user.go
+	permissionInit()        //Register permission interface after user
+	RegisterSystemInit()    //See register.go
+	OAuthInit()             //Oauth system init
+	GroupStoragePoolInit()  //Register permission groups's storage pool, require permissionInit()
+	BridgeStoragePoolInit() //Register the bridged storage pool based on mounted storage pools
 
 	//6. Start Modules and Package Manager
 	ModuleServiceInit() //Module Handler
 	PackagManagerInit() //Start APT service agent
 
 	//7. Kickstart the File System and Desktop
-	SchedulerInit()     //Start System Scheudler
-	FileSystemInit()    //Start FileSystem
-	DesktopInit()       //Start Desktop
-	HardwarePowerInit() //Start host power manager
+	SchedulerInit()  //Start System Scheudler
+	FileSystemInit() //Start FileSystem
+	DesktopInit()    //Start Desktop
+
 	//StorageDaemonInit() //Start File System handler daemon (for backup and other sync process)
 
 	//8 Start AGI and Subservice modules (Must start after module)
-	AGIInit()        //ArOZ Javascript Gateway Interface, must start after fs
-	SubserviceInit() //Subservice Handler
+	AGIInit()          //ArOZ Javascript Gateway Interface, must start after fs
+	SubserviceInit()   //Subservice Handler
+	ReverseProxtInit() //Start Dynamic Reverse Proxy
 
 	//9. Initiate System Settings Handlers
 	SystemSettingInit()       //Start System Setting Core
@@ -70,6 +72,7 @@ func RunStartup() {
 	AuthSettingsInit()        //Authentication Settings Handler, must be start after user Handler
 	AdvanceSettingInit()      //System Advance Settings
 	StartupFlagsInit()        //System BootFlag settibg
+	HardwarePowerInit()       //Start host power manager
 	RegisterStorageSettings() //Storage Settings
 
 	//10. Startup network services and schedule services
@@ -96,5 +99,4 @@ func RunStartup() {
 
 	//Finally
 	moduleHandler.ModuleSortList() //Sort the system module list
-
 }

+ 108 - 0
storage.bridge.go

@@ -0,0 +1,108 @@
+package main
+
+import (
+	"errors"
+	"log"
+
+	fs "imuslab.com/arozos/mod/filesystem"
+	storage "imuslab.com/arozos/mod/storage"
+)
+
+/*
+	Storage functions related to bridged FSH
+*/
+
+//Initiate bridged storage pool configs
+func BridgeStoragePoolInit() {
+	bridgeRecords, err := bridgeManager.ReadConfig()
+	if err != nil {
+		log.Println("[ERROR] Fail to read File System Handler bridge config")
+		return
+	}
+
+	for _, bridgeConf := range bridgeRecords {
+		fsh, err := GetFsHandlerByUUID(bridgeConf.FSHUUID)
+		if err != nil {
+			//This fsh is not found. Skip this
+			continue
+		}
+
+		basePool, err := GetStoragePoolByOwner(bridgeConf.SPOwner)
+		if err != nil {
+			//This fsh is not found. Skip this
+			continue
+		}
+
+		err = BridgeFSHandlerToGroup(fsh, basePool)
+		if err != nil {
+			log.Println("Failed to bridge "+fsh.UUID+":/ to "+basePool.Owner, err.Error())
+		}
+		log.Println(fsh.UUID + ":/ bridged to " + basePool.Owner + " Storage Pool")
+	}
+}
+
+func BridgeStoragePoolForGroup(group string) {
+	bridgeRecords, err := bridgeManager.ReadConfig()
+	if err != nil {
+		log.Println("Failed to bridge FSH for group " + group)
+		return
+	}
+
+	for _, bridgeConf := range bridgeRecords {
+		if bridgeConf.SPOwner == group {
+			fsh, err := GetFsHandlerByUUID(bridgeConf.FSHUUID)
+			if err != nil {
+				//This fsh is not found. Skip this
+				continue
+			}
+
+			basePool, err := GetStoragePoolByOwner(bridgeConf.SPOwner)
+			if err != nil {
+				//This fsh is not found. Skip this
+				continue
+			}
+
+			err = BridgeFSHandlerToGroup(fsh, basePool)
+			if err != nil {
+				log.Println("Failed to bridge "+fsh.UUID+":/ to "+basePool.Owner, err.Error())
+			}
+			log.Println(fsh.UUID + ":/ bridged to " + basePool.Owner + " Storage Pool")
+		}
+	}
+}
+
+//Bridge a FSH to a given Storage Pool
+func BridgeFSHandlerToGroup(fsh *fs.FileSystemHandler, sp *storage.StoragePool) error {
+	//Check if the fsh already exists in the basepool
+	for _, thisFSH := range sp.Storages {
+		if thisFSH.UUID == fsh.UUID {
+			return errors.New("Target File System Handler already bridged to this pool")
+		}
+	}
+	sp.Storages = append(sp.Storages, fsh)
+	return nil
+}
+
+func DebridgeFSHandlerFromGroup(fshUUID string, sp *storage.StoragePool) error {
+	isBridged, err := bridgeManager.IsBridgedFSH(fshUUID, sp.Owner)
+	if err != nil || !isBridged {
+		return errors.New("FSH not bridged")
+	}
+
+	newStorageList := []*fs.FileSystemHandler{}
+	fshExists := false
+	for _, fsh := range sp.Storages {
+		if fsh.UUID != fshUUID {
+			newStorageList = append(newStorageList, fsh)
+		} else {
+			fshExists = true
+		}
+	}
+
+	if fshExists {
+		sp.Storages = newStorageList
+		return nil
+	} else {
+		return errors.New("Target File System Handler not found")
+	}
+}

+ 51 - 1
storage.go

@@ -10,6 +10,7 @@ import (
 	"strings"
 
 	"imuslab.com/arozos/mod/permission"
+	"imuslab.com/arozos/mod/storage/bridge"
 
 	fs "imuslab.com/arozos/mod/filesystem"
 	storage "imuslab.com/arozos/mod/storage"
@@ -18,6 +19,8 @@ import (
 var (
 	baseStoragePool *storage.StoragePool    //base storage pool, all user can access these virtual roots
 	fsHandlers      []*fs.FileSystemHandler //All File system handlers. All opened handles must be registered in here
+	//storagePools    []*storage.StoragePool  //All Storage pool opened
+	bridgeManager *bridge.Record //Manager to handle bridged FSH
 )
 
 func StorageInit() {
@@ -31,6 +34,11 @@ func StorageInit() {
 	if err != nil {
 		panic(err)
 	}
+
+	//Create a brdige record manager
+	bm := bridge.NewBridgeRecord("system/bridge.json")
+	bridgeManager = bm
+
 }
 
 func LoadBaseStoragePool() error {
@@ -156,14 +164,50 @@ func LoadStoragePoolForGroup(pg *permission.PermissionGroup) error {
 
 		//Assign storage pool to group
 		pg.StoragePool = sp
+
 	} else {
 		//Storage configuration not exists. Fill in the basic information and move to next storage pool
-		pg.StoragePool.Owner = pg.Name
+
+		//Create a new empty storage pool for this group
+		sp, err := storage.NewStoragePool([]*fs.FileSystemHandler{}, pg.Name)
+		if err != nil {
+			log.Println("Failed to create empty storage pool for group: ", pg.Name)
+		}
+		pg.StoragePool = sp
 		pg.StoragePool.OtherPermission = "denied"
 	}
+
 	return nil
 }
 
+//Check if a storage pool exists by its group owner name
+func StoragePoolExists(poolOwner string) bool {
+	_, err := GetStoragePoolByOwner(poolOwner)
+	return err == nil
+}
+
+func GetAllStoragePools() []*storage.StoragePool {
+	//Append the base pool
+	results := []*storage.StoragePool{baseStoragePool}
+
+	//Add each permissionGroup's pool
+	for _, pg := range permissionHandler.PermissionGroups {
+		results = append(results, pg.StoragePool)
+	}
+
+	return results
+}
+
+func GetStoragePoolByOwner(owner string) (*storage.StoragePool, error) {
+	sps := GetAllStoragePools()
+	for _, pool := range sps {
+		if pool.Owner == owner {
+			return pool, nil
+		}
+	}
+	return nil, errors.New("Storage pool owned by " + owner + " not found")
+}
+
 func GetFsHandlerByUUID(uuid string) (*fs.FileSystemHandler, error) {
 	//Filter out the :/ fropm uuid if exists
 	if strings.Contains(uuid, ":") {
@@ -198,3 +242,9 @@ func CloseAllStorages() {
 		fsh.FilesystemDatabase.Close()
 	}
 }
+
+func closeAllStoragePools() {
+	for _, sp := range GetAllStoragePools() {
+		sp.Close()
+	}
+}

+ 151 - 32
storage.pool.go

@@ -13,6 +13,7 @@ import (
 
 	"imuslab.com/arozos/mod/database"
 	"imuslab.com/arozos/mod/permission"
+	"imuslab.com/arozos/mod/storage/bridge"
 
 	"github.com/tidwall/pretty"
 	fs "imuslab.com/arozos/mod/filesystem"
@@ -29,6 +30,7 @@ import (
 */
 
 func StoragePoolEditorInit() {
+
 	adminRouter := prout.NewModuleRouter(prout.RouterOption{
 		ModuleName:  "System Settings",
 		AdminOnly:   true,
@@ -45,6 +47,8 @@ func StoragePoolEditorInit() {
 	adminRouter.HandleFunc("/system/storage/pool/reload", HandleStoragePoolReload)
 	adminRouter.HandleFunc("/system/storage/pool/toggle", HandleFSHToggle)
 	adminRouter.HandleFunc("/system/storage/pool/edit", HandleFSHEdit)
+	adminRouter.HandleFunc("/system/storage/pool/bridge", HandleFSHBridging)
+	adminRouter.HandleFunc("/system/storage/pool/checkBridge", HandleFSHBridgeCheck)
 }
 
 //Handle editing of a given File System Handler
@@ -81,7 +85,7 @@ func HandleFSHEdit(w http.ResponseWriter, r *http.Request) {
 	} else if opr == "set" {
 		//Set
 		newFsOption := buildOptionFromRequestForm(r)
-		log.Println(newFsOption)
+		//log.Println(newFsOption)
 
 		//Read and remove the original settings from the config file
 		err := setFSHConfigByGroupAndId(group, uuid, newFsOption)
@@ -115,6 +119,10 @@ func getFSHConfigFromGroupAndUUID(group string, uuid string) (*fs.FileSystemOpti
 		return nil, errors.New("Configuration file not found")
 	}
 
+	if !fileExists(filepath.Dir(targerFile)) {
+		os.MkdirAll(filepath.Dir(targerFile), 0775)
+	}
+
 	//Load and parse the file
 	configContent, err := ioutil.ReadFile(targerFile)
 	if err != nil {
@@ -154,6 +162,10 @@ func setFSHConfigByGroupAndId(group string, uuid string, options fs.FileSystemOp
 		return errors.New("Configuration file not found")
 	}
 
+	if !fileExists(filepath.Dir(targerFile)) {
+		os.MkdirAll(filepath.Dir(targerFile), 0775)
+	}
+
 	//Load and parse the file
 	configContent, err := ioutil.ReadFile(targerFile)
 	if err != nil {
@@ -180,7 +192,7 @@ func setFSHConfigByGroupAndId(group string, uuid string, options fs.FileSystemOp
 
 	//Write config back to file
 	js, _ := json.MarshalIndent(newConfig, "", " ")
-	return ioutil.WriteFile(targerFile, js, 0755)
+	return ioutil.WriteFile(targerFile, js, 0775)
 }
 
 //Handle Storage Pool toggle on-off
@@ -295,10 +307,14 @@ func HandleStoragePoolReload(w http.ResponseWriter, r *http.Request) {
 			//If there is no handler in config, the empty one will be kept
 			LoadStoragePoolForGroup(pg)
 		}
+
+		BridgeStoragePoolInit()
+
 	} else {
 
 		if pool == "system" {
 			//Reload basepool
+
 			baseStoragePool.Close()
 			emptyPool := storage.StoragePool{}
 			baseStoragePool = &emptyPool
@@ -313,6 +329,7 @@ func HandleStoragePoolReload(w http.ResponseWriter, r *http.Request) {
 				userHandler.UpdateStoragePool(baseStoragePool)
 			}
 
+			BridgeStoragePoolForGroup("system")
 		} else {
 			//Reload the given storage pool
 			if !permissionHandler.GroupExists(pool) {
@@ -324,6 +341,7 @@ func HandleStoragePoolReload(w http.ResponseWriter, r *http.Request) {
 
 			//Pool should be exists. Close it
 			pg := permissionHandler.GetPermissionGroupByName(pool)
+
 			pg.StoragePool.Close()
 
 			//Create an empty pool for this permission group
@@ -333,6 +351,7 @@ func HandleStoragePoolReload(w http.ResponseWriter, r *http.Request) {
 			//Recreate a new pool for this permission group
 			//If there is no handler in config, the empty one will be kept
 			LoadStoragePoolForGroup(pg)
+			BridgeStoragePoolForGroup(pg.Name)
 		}
 	}
 
@@ -366,40 +385,61 @@ func HandleStoragePoolRemove(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		if fileExists("./system/storage/" + groupname + ".json") {
-			targetConfigFile = "./system/storage/" + groupname + ".json"
-		} else {
-			//No config to delete
-			sendErrorResponse(w, "File system handler not exists")
-			return
+		targetConfigFile = "./system/storage/" + groupname + ".json"
+		if !fileExists(targetConfigFile) {
+			//No config. Create an empty one
+			initConfig := []fs.FileSystemOption{}
+			js, _ := json.MarshalIndent(initConfig, "", " ")
+			ioutil.WriteFile(targetConfigFile, js, 0775)
 		}
 	}
 
-	//Remove it from the json file
-	//Read and parse from old config
-	oldConfigs := []fs.FileSystemOption{}
-	originalConfigFile, _ := ioutil.ReadFile(targetConfigFile)
-	err = json.Unmarshal(originalConfigFile, &oldConfigs)
-	if err != nil {
-		sendErrorResponse(w, "Failed to parse original config file")
+	//Check if this handler is bridged handler
+	bridged, _ := bridgeManager.IsBridgedFSH(uuid, groupname)
+	if bridged {
+		//Bridged FSH. Remove it from bridge config
+		basePool, err := GetStoragePoolByOwner(groupname)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+		err = DebridgeFSHandlerFromGroup(uuid, basePool)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Remove it from the config
+		bridgeManager.RemoveFromConfig(uuid, groupname)
+		sendOK(w)
 		return
-	}
+	} else {
+		//Remove it from the json file
+		//Read and parse from old config
+		oldConfigs := []fs.FileSystemOption{}
+		originalConfigFile, _ := ioutil.ReadFile(targetConfigFile)
+		err = json.Unmarshal(originalConfigFile, &oldConfigs)
+		if err != nil {
+			sendErrorResponse(w, "Failed to parse original config file")
+			return
+		}
 
-	//Generate new confic by filtering
-	newConfigs := []fs.FileSystemOption{}
-	for _, config := range oldConfigs {
-		if config.Uuid != uuid {
-			newConfigs = append(newConfigs, config)
+		//Generate new confic by filtering
+		newConfigs := []fs.FileSystemOption{}
+		for _, config := range oldConfigs {
+			if config.Uuid != uuid {
+				newConfigs = append(newConfigs, config)
+			}
 		}
-	}
 
-	//Parse and put it into file
-	if len(newConfigs) > 0 {
-		js, _ := json.Marshal(newConfigs)
-		resultingJson := pretty.Pretty(js)
-		ioutil.WriteFile(targetConfigFile, resultingJson, 755)
-	} else {
-		os.Remove(targetConfigFile)
+		//Parse and put it into file
+		if len(newConfigs) > 0 {
+			js, _ := json.Marshal(newConfigs)
+			resultingJson := pretty.Pretty(js)
+			ioutil.WriteFile(targetConfigFile, resultingJson, 0777)
+		} else {
+			os.Remove(targetConfigFile)
+		}
 	}
 
 	sendOK(w)
@@ -431,7 +471,6 @@ func buildOptionFromRequestForm(r *http.Request) fs.FileSystemOption {
 }
 
 func HandleStorageNewFsHandler(w http.ResponseWriter, r *http.Request) {
-
 	newFsOption := buildOptionFromRequestForm(r)
 
 	type errorObject struct {
@@ -485,7 +524,7 @@ func HandleStorageNewFsHandler(w http.ResponseWriter, r *http.Request) {
 	js, err := json.Marshal(oldConfigs)
 	resultingJson := pretty.Pretty(js)
 
-	err = ioutil.WriteFile(configFile, resultingJson, 777)
+	err = ioutil.WriteFile(configFile, resultingJson, 0775)
 	if err != nil {
 		//Write Error. This could sometime happens on Windows host for unknown reason
 		js, _ := json.Marshal(errorObject{
@@ -513,10 +552,18 @@ func HandleListStoragePoolsConfig(w http.ResponseWriter, r *http.Request) {
 		targetFile = "./system/storage/" + target + ".json"
 	}
 
+	if !fileExists(targetFile) {
+		//Assume no storage.
+		nofsh := []*fs.FileSystemOption{}
+		js, _ := json.Marshal(nofsh)
+		sendJSONResponse(w, string(js))
+		return
+	}
+
 	//Read and serve it
 	configContent, err := ioutil.ReadFile(targetFile)
 	if err != nil {
-		sendErrorResponse(w, "Given group does not have a config file.")
+		sendErrorResponse(w, err.Error())
 		return
 	} else {
 		sendJSONResponse(w, string(configContent))
@@ -552,3 +599,75 @@ func HandleListStoragePools(w http.ResponseWriter, r *http.Request) {
 	js, _ := json.Marshal(storagePools)
 	sendJSONResponse(w, string(js))
 }
+
+//Handler for bridging two FSH, require admin permission
+func HandleFSHBridging(w http.ResponseWriter, r *http.Request) {
+	//Get the target pool and fsh to bridge
+	basePool, err := mv(r, "base", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid base pool")
+		return
+	}
+
+	//Add the target FSH into the base pool
+	basePoolObject, err := GetStoragePoolByOwner(basePool)
+	if err != nil {
+		log.Println("Bridge FSH failed: ", err.Error())
+		sendErrorResponse(w, "Storage pool not found")
+		return
+	}
+
+	targetFSH, err := mv(r, "fsh", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid fsh given")
+		return
+	}
+
+	fsh, err := GetFsHandlerByUUID(targetFSH)
+	if err != nil {
+		sendErrorResponse(w, "Given FSH UUID does not exists")
+		return
+	}
+
+	err = BridgeFSHandlerToGroup(fsh, basePoolObject)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	bridgeConfig := bridge.BridgeConfig{
+		FSHUUID: fsh.UUID,
+		SPOwner: basePoolObject.Owner,
+	}
+
+	//Write changes to file
+	err = bridgeManager.AppendToConfig(&bridgeConfig)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	sendOK(w)
+}
+
+func HandleFSHBridgeCheck(w http.ResponseWriter, r *http.Request) {
+	basePool, err := mv(r, "base", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid base pool")
+		return
+	}
+
+	fsh, err := mv(r, "fsh", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid fsh UUID")
+		return
+	}
+
+	isBridged, err := bridgeManager.IsBridgedFSH(fsh, basePool)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	js, _ := json.Marshal(isBridged)
+	sendJSONResponse(w, string(js))
+}

+ 1 - 1
subservice.go

@@ -65,7 +65,7 @@ func SubserviceInit() {
 	//Scan and load all subservice modules
 	subservices, _ := filepath.Glob("./subservice/*")
 	for _, servicePath := range subservices {
-		if !fileExists(servicePath + "/.disabled") {
+		if IsDir(servicePath) && !fileExists(servicePath+"/.disabled") {
 			//Only enable module with no suspended config file
 			ssRouter.Launch(servicePath, true)
 		}

+ 0 - 0
subservice/WsTTY/.disabled


+ 56 - 14
system.info.go

@@ -5,6 +5,7 @@ import (
 	"log"
 	"net/http"
 	"runtime"
+	"time"
 
 	info "imuslab.com/arozos/mod/info/hardwareinfo"
 	usage "imuslab.com/arozos/mod/info/usageinfo"
@@ -16,18 +17,20 @@ func SystemInfoInit() {
 	log.Println("Operation System: " + runtime.GOOS)
 	log.Println("System Architecture: " + runtime.GOARCH)
 
-	if *allow_hardware_management {
-		//Updates 5 Dec 2020, Added permission router
-		router := prout.NewModuleRouter(prout.RouterOption{
-			AdminOnly:   false,
-			UserHandler: userHandler,
-			DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
-				sendErrorResponse(w, "Permission Denied")
-			},
-		})
+	//Updates 5 Dec 2020, Added permission router
+	router := prout.NewModuleRouter(prout.RouterOption{
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
 
-		//Create Info Server Object
-		infoServer := info.NewInfoServer(info.ArOZInfo{
+	//Create Info Server Object
+	var infoServer *info.Server = nil
+
+	if *allow_hardware_management {
+		infoServer = info.NewInfoServer(info.ArOZInfo{
 			BuildVersion: build_version + "." + internal_version,
 			DeviceVendor: deviceVendor,
 			DeviceModel:  deviceModel,
@@ -44,9 +47,6 @@ func SystemInfoInit() {
 		router.HandleFunc("/system/info/usbPorts", info.GetUSB)
 		router.HandleFunc("/system/info/getRAMinfo", info.GetRamInfo)
 
-		//ArOZ Info do not need permission router
-		http.HandleFunc("/system/info/getArOZInfo", infoServer.GetArOZInfo)
-
 		//Register as a system setting
 		registerSetting(settingModule{
 			Name:     "Host Info",
@@ -71,8 +71,50 @@ func SystemInfoInit() {
 
 		router.HandleFunc("/system/info/getUsageInfo", InfoHandleTaskInfo)
 
+	} else {
+		//Make a simpler page for the information of system for hardware management disabled nodes
+		registerSetting(settingModule{
+			Name:     "Overview",
+			Desc:     "Overview for user information",
+			IconPath: "SystemAO/info/img/small_icon.png",
+			Group:    "Info",
+			StartDir: "SystemAO/info/overview.html",
+		})
+
+		//Remve hardware information from the infoServer
+		infoServer = info.NewInfoServer(info.ArOZInfo{
+			BuildVersion: build_version + "." + internal_version,
+			DeviceVendor: deviceVendor,
+			DeviceModel:  deviceModel,
+			VendorIcon:   "../../" + iconVendor,
+			SN:           deviceUUID,
+			HostOS:       "virtualized",
+			CPUArch:      "generic",
+			HostName:     *host_name,
+		})
+	}
+
+	//Register endpoints that do not involve hardware management
+	router.HandleFunc("/system/info/getRuntimeInfo", InfoHandleGetRuntimeInfo)
+
+	//ArOZ Info do not need permission router
+	http.HandleFunc("/system/info/getArOZInfo", infoServer.GetArOZInfo)
+
+}
+
+func InfoHandleGetRuntimeInfo(w http.ResponseWriter, r *http.Request) {
+	type RuntimeInfo struct {
+		StartupTime      int64
+		ContinuesRuntime int64
 	}
 
+	runtimeInfo := RuntimeInfo{
+		StartupTime:      startupTime,
+		ContinuesRuntime: time.Now().Unix() - startupTime,
+	}
+
+	js, _ := json.Marshal(runtimeInfo)
+	sendJSONResponse(w, string(js))
 }
 
 func InfoHandleTaskInfo(w http.ResponseWriter, r *http.Request) {

+ 5 - 3
system/auth/register.system

@@ -49,7 +49,7 @@
                         <label>I agree to the <a href="../../SystemAO/vendor/public/termsAndConditions.html" target="_blank">Terms and Conditions</a></label>
                         </div>
                     </div>
-                    <button id="submitbtn" class="ui disabled button" type="submit">Sign Up</button>
+                    <button disabled="disabled" id="submitbtn" class="ui disabled button" type="submit">Sign Up</button>
                 </form>
                 <div id="errmsg" class="ui red inverted segment" style="display:none;">
                     <i class="remove icon"></i> Internal Server Error
@@ -85,9 +85,11 @@
 
             function toggleSignupBox(toggle){
                 if (toggle){
-                    $("#submitbtn").removeClass('disabled')
+                    $("#submitbtn").removeAttr('disabled');
+                    $("#submitbtn").removeClass('disabled');
                 }else{
-                    $("#submitbtn").addClass('disabled')
+                    $("#submitbtn").attr('disabled','disabled');
+                    $("#submitbtn").addClass('disabled');
                 }
             }
 

+ 2 - 2
system/reset/resetCodeTemplate.html

@@ -3,7 +3,7 @@
     <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <title>ArOZ Online - Reset Password</title>
+    <title>ArozOS - Reset Password</title>
     <link rel="stylesheet" href="script/semantic/semantic.css">
     <link rel="stylesheet" href="script/ao.css">
     <script type="application/javascript" src="script/jquery.min.js"></script>
@@ -15,7 +15,7 @@
     <body> 
     <br><br><br>
     <div class="ui container" align="center">
-        <div class="ui yellow segment" style="max-width:400px;" align="left">
+        <div class="ui pink segment" style="max-width:400px;" align="left">
             <div class="imageRight" align="center">
                 <img class="ui small image" src="data:image/png;base64, {{vendor_logo}}"></img>
             </div>

+ 2 - 2
system/reset/resetPasswordTemplate.html

@@ -3,7 +3,7 @@
     <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <title>ArOZ Online - Reset Password</title>
+    <title>ArozOS - Reset Password</title>
     <link rel="stylesheet" href="script/semantic/semantic.css">
     <link rel="stylesheet" href="script/ao.css">
     <script type="application/javascript" src="script/jquery.min.js"></script>
@@ -15,7 +15,7 @@
     <body> 
     <br><br><br>
     <div class="ui container" align="center">
-        <div class="ui yellow segment" style="max-width:400px;" align="left">
+        <div class="ui pink segment" style="max-width:400px;" align="left">
             <div class="imageRight" align="center">
                 <img class="ui small image" src="data:image/png;base64, {{vendor_logo}}"></img>
             </div>

+ 3 - 0
system/share/audio.html

@@ -1,7 +1,10 @@
 <p>Audio File Preview</p>
 <audio id="audioplayer" src="{{preview_url}}" style="width: 100%" controls></audio>
+<textarea id="embeddedCode" class="u-full-width" style="height: 100px;"></textarea>
 <script>
     player=document.getElementById("audioplayer");
     player.volume=0.2;
+
+    $("#embeddedCode").text(`<audio id="audioplayer" src="${location.protocol}//${window.location.hostname}:${window.location.port}{{preview_url}}" style="width: 400px" controls></audio>`);
 </script>
     

+ 5 - 1
system/share/downloadPage.html

@@ -28,6 +28,10 @@
             padding: 20px;
             color: white;
         }
+
+        .filename{
+          word-break: break-word;
+        }
     </style>
     </head>
     <body>
@@ -35,7 +39,7 @@
         <br>
         <div class="container">
             <h5>{{hostname}} File Sharing</h5>
-            <h3>{{filename}}</h3>
+            <h3 class="filename">{{filename}}</h3>
             <div class="row">
                 <div class="one-half column">
                     <table class="u-full-width">

+ 47 - 7
system/share/downloadPageFolder.html

@@ -48,6 +48,18 @@
           -ms-user-select: none; /* Internet Explorer/Edge */
           user-select: none;
         }
+
+        #filelistWrapper{
+          position: relative;
+          padding: 12px;
+          border-top: 4px solid #ffe46c;
+          -webkit-box-shadow: 11px 9px 23px 0px rgba(54,54,54,0.31); 
+          box-shadow: 11px 9px 23px 0px rgba(54,54,54,0.31);
+        }
+
+        td{
+          word-break: break-all;
+        }
     </style>
     </head>
     <body>
@@ -86,11 +98,12 @@
                       </table>
                     <a href="{{downloadurl}}"><button class="button-primary">Download All</button></a>
                     <p style="font-size: 80%;"><b>Depending on folder size, zipping might take a while to complete.</b></p>
-                    <br>
-                    <p>Request File ID: {{reqid}}</p>
-                    <p>Request Timestamp: {{reqtime}}</p>
+                    <p>Request File ID: {{reqid}}<br>
+                    Request Timestamp: {{reqtime}}</p>
+                    <small>📂 Double click any item in the list to open or download</small>
+                    
                 </div>
-                <div class="one-half column" id="filelistWrapper" style="overflow-y: auto; padding-right: 0.5em;">
+                <div class="one-half column" id="filelistWrapper" style="overflow-y: auto; padding-right: 0.5em; min-height: 400px;">
                   <table class="u-full-width">
                     <thead>
                       <tr>
@@ -105,7 +118,7 @@
                   </table>
                 </div>
             </div>
-           
+          
         </div>
         <div class="footer">
             <div class="container">
@@ -119,7 +132,20 @@
       var selectedFile = null;
       renderFileList(treeFileList["."]);
 
-      console.log(treeFileList);
+      handleWindowResize();
+      $(window).on("resize", function(e){
+        handleWindowResize();
+      });
+
+      function handleWindowResize(){
+        if (window.innerWidth < 550){
+          //Assume mobile
+          $(".footer").css("height", "20px");
+        }else{
+          $(".footer").css("height", "50px");
+        }
+      }
+      
 
       function renderFileList(filelist){
         $("#folderList").html("");
@@ -134,10 +160,24 @@
           var filetype = "File";
           var displayName = "";
           if (file.IsDir == true){
+            //Folder
             filetype = "Folder";
             displayName = "📁 " + file.Filename;
           }else{
-            displayName = "📄 " + file.Filename;
+            //File
+            var ext = file.Filename.split(".").pop();
+            var icon = "📄"
+            ext = ext.toLowerCase();
+            if (ext == "mp3" || ext == "wav" || ext == "flac" || ext == "aac" || ext == "ogg" || ext == ""){
+              icon = "🎵";
+            }else if (ext == "mp4" || ext == "avi" || ext == "webm" || ext == "mkv" || ext == "mov" || ext == "rvmb"){
+              icon = "🎞️";
+            }else if (ext == "png" || ext == "jpeg" || ext == "jpg" || ext == "bmp" || ext == "gif"){
+              icon = "🖼️";
+            }
+
+            displayName =  icon + " " + file.Filename;
+            
           }
           $("#folderList").append(`<tr class="fileobject noselect" onclick="highlightThis(this);" filename="${file.Filename}" relpath="${file.RelPath}" type="${filetype.toLocaleLowerCase()}" ondblclick="event.preventDefault(); openThis(this);">
               <td style="padding-left: 8px;">${displayName}</td>

+ 7 - 1
system/share/image.html

@@ -1 +1,7 @@
-<img src="{{preview_url}}" style="width: 100%"/>
+<img src="{{preview_url}}" style="width: 100%"/>
+<textarea id="embeddedCode" class="u-full-width" style="height: 100px;">
+    
+</textarea>
+<script>
+     $("#embeddedCode").text(`<img src="${location.protocol}//${window.location.hostname}:${window.location.port}{{preview_url}}" style="width: 30em"/>`);
+</script>

+ 6 - 0
system/share/video.html

@@ -1,5 +1,11 @@
 <p>Video File Preview</p>
 <video src="{{preview_url}}" style="width:100%" volume="0" controls></video>
+<textarea id="embeddedCode" class="u-full-width" style="height: 100px;">
+    
+</textarea>
 <script>
     document.getElementsByTagName('video')[0].volume = 0.2;
+
+    //Render an embedded code for this video preview
+    $("#embeddedCode").text(`<video src="${location.protocol}//${window.location.hostname}:${window.location.port}{{preview_url}}" style="width:560px; height: 340px; background-color: #000000;" controls></video>`);
 </script>

+ 13 - 0
user.go

@@ -110,6 +110,19 @@ func user_handleUserRemove(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	currentUserinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		//This user has not logged in
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	if currentUserinfo.Username == userinfo.Username {
+		//This user has not logged in
+		sendErrorResponse(w, "You can't remove yourself")
+		return
+	}
+
 	//Clear Core User Data
 	userinfo.RemoveUser()
 

+ 2 - 2
web/FFmpeg Factory/backend/convert.js

@@ -36,7 +36,7 @@ if (filelib.fileExists(targetFilepath)){
     var actualCommand = decodeURIComponent(command);
     actualCommand = actualCommand.replace('{filepath}',srcReal);
     actualCommand = actualCommand.replace('{filename}',dir(srcReal) + "/" + base(srcReal))
-    
+
     //Register this task in on-going task list
     newDBTableIfNotExists("FFmpeg Factory")
     var ts = Math.round((new Date()).getTime() / 1000);
@@ -47,7 +47,7 @@ if (filelib.fileExists(targetFilepath)){
     var results = execpkg("ffmpeg",actualCommand);
 
     //Deregister this task from on-going task list
-
+    deleteDBItem("FFmpeg Factory",taskKey,targetFilepath);
 
     sendJSONResp(JSON.stringify({
         status: "ok",

+ 9 - 9
web/FFmpeg Factory/config/i2i.json

@@ -1,11 +1,11 @@
 {
-    "PNG(Default)": "-i \"{filepath}\" \"{filename}.png\"",
-    "PNG(High Quality)": "-i \"{filepath}\" -compression_level 0 \"{filename}.png\"",
-    "PNG(Low Quality)": "-i \"{filepath}\" -compression_level 200 \"{filename}.png\"",
-    "JPG(Default)": "-i \"{filepath}\" \"{filename}.jpg\"",
-    "JPG(High Quality)": "-i \"{filepath}\" -compression_level 0 \"{filename}.jpg\"",
-    "JPG(Low Quality)": "-i \"{filepath}\" -compression_level 200 \"{filename}.jpg\"",
-    "GIF": "-i \"{filepath}\" \"{filename}.gif\"",
-    "BMP": "-i \"{filepath}\" \"{filename}.bmp\"",
-    "TIF(TIFF)": "-i \"{filepath}\" \"{filename}.tif\""
+    "PNG(Default)": "-y -i \"{filepath}\" \"{filename}.png\"",
+    "PNG(High Quality)": "-y -i \"{filepath}\" -compression_level 0 \"{filename}.png\"",
+    "PNG(Low Quality)": "-y -i \"{filepath}\" -compression_level 200 \"{filename}.png\"",
+    "JPG(Default)": "-y -i \"{filepath}\" \"{filename}.jpg\"",
+    "JPG(High Quality)": "-y -i \"{filepath}\" -compression_level 0 \"{filename}.jpg\"",
+    "JPG(Low Quality)": "-y -i \"{filepath}\" -compression_level 200 \"{filename}.jpg\"",
+    "GIF": "-y -i \"{filepath}\" \"{filename}.gif\"",
+    "BMP": "-y -i \"{filepath}\" \"{filename}.bmp\"",
+    "TIF(TIFF)": "-y -i \"{filepath}\" \"{filename}.tif\""
 }

+ 4 - 4
web/FFmpeg Factory/config/other.json

@@ -1,6 +1,6 @@
 {
-	"MP4 to GIF": "-i \"{filepath}\" -f gif \"{filename}.gif\"",
-	"GIF to MP4": "-i \"{filepath}\" -f mp4 -pix_fmt yuv420p \"{filename}.mp4\"",
-	"SRT to ASS": "-i \"{filepath}\" \"{filename}.ass\"",
-	"ASS to SRT": "-i \"{filepath}\" -c:s srt \"{filename}.srt\""
+	"MP4 to GIF": "-y -i \"{filepath}\" -f gif \"{filename}.gif\"",
+	"GIF to MP4": "-y -i \"{filepath}\" -f mp4 -pix_fmt yuv420p \"{filename}.mp4\"",
+	"SRT to ASS": "-y -i \"{filepath}\" \"{filename}.ass\"",
+	"ASS to SRT": "-y -i \"{filepath}\" -c:s srt \"{filename}.srt\""
 }

+ 9 - 9
web/FFmpeg Factory/config/v2a.json

@@ -1,12 +1,12 @@
 {
-    "MP3(Default)": "-i \"{filepath}\" \"{filename}.mp3\"",
-    "MP3(320 Kbps)": "-i \"{filepath}\" -b:a 320k \"{filename}.mp3\"",
-    "MP3(256 Kbps)": "-i \"{filepath}\" -b:a 256k \"{filename}.mp3\"",
-    "MP3(192 Kbps)": "-i \"{filepath}\" -b:a 192k \"{filename}.mp3\"",
-    "MP3(128 Kbps)": "-i \"{filepath}\" -b:a 128k \"{filename}.mp3\"",
-    "AAC(Default)": "-i \"{filepath}\" -strict experimental \"{filename}.aac\"",
+    "MP3(Default)": "-y -i \"{filepath}\" \"{filename}.mp3\"",
+    "MP3(320 Kbps)": "-y -i \"{filepath}\" -b:a 320k \"{filename}.mp3\"",
+    "MP3(256 Kbps)": "-y -i \"{filepath}\" -b:a 256k \"{filename}.mp3\"",
+    "MP3(192 Kbps)": "-y -i \"{filepath}\" -b:a 192k \"{filename}.mp3\"",
+    "MP3(128 Kbps)": "-y -i \"{filepath}\" -b:a 128k \"{filename}.mp3\"",
+    "AAC(Default)": "-y -i \"{filepath}\" -strict experimental \"{filename}.aac\"",
     "AAC(codec copy)": "-i \"{filepath}\" -vn -acodec copy \"{filename}.aac\"",
-    "FLAC": "-i \"{filepath}\" \"{filename}.flac\"",
-    "OGG": "-i \"{filepath}\" \"{filename}.ogg\"",
-    "WAV": "-i \"{filepath}\" \"{filename}.wav\""
+    "FLAC": "-y -i \"{filepath}\" \"{filename}.flac\"",
+    "OGG": "-y -i \"{filepath}\" \"{filename}.ogg\"",
+    "WAV": "-y -i \"{filepath}\" \"{filename}.wav\""
 }

+ 14 - 14
web/FFmpeg Factory/config/v2v.json

@@ -1,16 +1,16 @@
 {
-    "MP4(Default)": "-i \"{filepath}\" \"{filename}.mp4\"",
-    "MP4(1080p)": "-i \"{filepath}\" -s hd1080 -c:v libx264 -crf 23 -c:a aac -strict -2 \"{filename}.mp4\"",
-    "MP4(720p)": "-i \"{filepath}\" -s hd720 -c:v libx264 -crf 23 -c:a aac -strict -2 \"{filename}.mp4\"",
-    "MP4(480p)": "-i \"{filepath}\" -s hd480 -c:v libx264 -crf 23 -c:a aac -strict -2 \"{filename}.mp4\"",
-    "AVI(Default)": "-i \"{filepath}\" -q:v 2 \"{filename}.avi\"",
-    "AVI(Low Quality)": "-i \"{filepath}\" -q:v 6 \"{filename}.avi\"",
-    "AVI(High Quality)": "-i \"{filepath}\" -q:v 0 \"{filename}.avi\"",
-    "AVI(codec copy)": "-i \"{filepath}\" -vcodec copy -acodec copy \"{filename}.avi\"",
-    "WebM": "-i \"{filepath}\" -c:v libvpx -crf 10 -b:v 1M -c:a libvorbis \"{filename}.webm\"",
-    "WMV": "-i \"{filepath}\" \"{filename}.wmv\"",
-    "FLV": "-i \"{filepath}\" \"{filename}.flv\"",
-    "MKV(Default)": "-i \"{filepath}\" \"{filename}.mkv\"",
-    "MKV(codec copy)": "-i \"{filepath}\" -vcodec copy -acodec copy \"{filename}.mkv\"",
-    "MOV": "-i \"{filepath}\" \"{filename}.mov\""
+    "MP4(Default)": "-y -i \"{filepath}\" \"{filename}.mp4\"",
+    "MP4(1080p)": "-y -i \"{filepath}\" -s hd1080 -c:v libx264 -crf 23 -c:a aac -strict -2 \"{filename}.mp4\"",
+    "MP4(720p)": "-y -i \"{filepath}\" -s hd720 -c:v libx264 -crf 23 -c:a aac -strict -2 \"{filename}.mp4\"",
+    "MP4(480p)": "-y -i \"{filepath}\" -s hd480 -c:v libx264 -crf 23 -c:a aac -strict -2 \"{filename}.mp4\"",
+    "AVI(Default)": "-y -i \"{filepath}\" -q:v 2 \"{filename}.avi\"",
+    "AVI(Low Quality)": "-y -i \"{filepath}\" -q:v 6 \"{filename}.avi\"",
+    "AVI(High Quality)": "-y -i \"{filepath}\" -q:v 0 \"{filename}.avi\"",
+    "AVI(codec copy)": "-y -i \"{filepath}\" -vcodec copy -acodec copy \"{filename}.avi\"",
+    "WebM": "-y -i \"{filepath}\" -c:v libvpx -crf 10 -b:v 1M -c:a libvorbis \"{filename}.webm\"",
+    "WMV": "-y -i \"{filepath}\" \"{filename}.wmv\"",
+    "FLV": "-y -i \"{filepath}\" \"{filename}.flv\"",
+    "MKV(Default)": "-y -i \"{filepath}\" \"{filename}.mkv\"",
+    "MKV(codec copy)": "-y -i \"{filepath}\" -vcodec copy -acodec copy \"{filename}.mkv\"",
+    "MOV": "-y -i \"{filepath}\" \"{filename}.mov\""
 }

+ 1 - 1
web/MDEditor/mde.html

@@ -72,7 +72,7 @@
                 }
 
                 //Load the file into the textarea
-                $.get("../../media?file=" + files[0].filepath,function(data){
+                $.get("../../media?file=" + files[0].filepath + "#" + Date.now(),function(data){
                     if (isJson){
                         data = JSON.stringify(data);
                     }

+ 1 - 1
web/Music/embedded.html

@@ -55,7 +55,7 @@
 	    color:#4b75ff !important;
 	}
 	.playerControlWrapper{
-	    text-align="center";
+	    text-align: center;
 	    position:absolute;
 	    bottom:50px;
 	    width:100% !important;

+ 1 - 1
web/Music/functions/getMeta.js

@@ -23,7 +23,7 @@ if (requirelib("filelib") == true){
     dirname = dirname.join("/");
 
     //Scan nearby files
-    var nearbyFiles = filelib.aglob(dirname + "/*") //aglob must be used here to prevent errors for non-unicode filename
+    var nearbyFiles = filelib.aglob(dirname + "/*", "user") //aglob must be used here to prevent errors for non-unicode filename
     var audioFiles = [];
     var supportedFormats = [".mp3",".flac",".wav",".ogg",".aac",".webm",".mp4"];
     //For each nearby files

+ 104 - 19
web/Photo/embedded.html

@@ -2,46 +2,131 @@
 <meta name="apple-mobile-web-app-capable" content="yes" />
 <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1" />
 <html>
-
 <head>
     <meta charset="UTF-8">
     <meta name="theme-color" content="#4b75ff">
-    <!-- <link rel="stylesheet" href="../script/tocas/tocas.css"> -->
-    <script src="../script/tocas/tocas.js"></script>
+    <title>Photo Preview</title>
     <script src="../script/jquery.min.js"></script>
     <script src="../script/ao_module.js"></script>
     <link rel="manifest" href="manifest.json">
+    <style>
+        .arrow{
+            width: 2em;
+            opacity: 0.5;
+            position: fixed;
+            top: calc(50% - 1em);
+            cursor: pointer;
+        }
 
-</head>
+        .left.arrow{
+            left: 2em;
+        }
 
+        .right.arrow{
+            right: 2em;
+        }
+    </style>
+</head>
 <body style="background:rgba(34,34,34,1);overflow:hidden;">
     <img id="img" style="max-height: 100vh;max-width: 100vw;">
+    <img class="left arrow" onclick="previousImage();" src="embedded/arrow-left.svg">
+    <img class="right arrow" onclick="nextImage();" src="embedded/arrow-right.svg">
     <script>
         //Get file playback info from hash
         var playbackFile = ao_module_loadInputFiles();
+        var nearbyFileList = [];
+        var currentViewingIndex = 0;
         //Only handle one file
         playbackFile = playbackFile[0];
-
-        //Update title name
-        ao_module_setWindowTitle("Photo - " + playbackFile.filename);
-        setTimeout(function() {
-            updateImgSize();
-        }, 500);
-        //Setup img src
-        $("#img").attr("src", '../media?file=' + encodeURIComponent(playbackFile.filepath))
-
-        //realigin to center
-        $('img').on('load', function() {
-            updateImgSize();
-        });
+        loadImage(playbackFile.filename, playbackFile.filepath);
+        
         $(window).on("resize ", function() {
             updateImgSize();
         });
 
+        //Load the nearby image files and allow swapping using <- and -> key
+        function loadNearbyFiles(filepath){
+            ao_module_agirun("Photo/embedded/listNearbyImage.js", {
+                path: filepath
+            }, function(data){
+                if (data.error != undefined){
+                    alert(data.error);
+                }else{
+                    nearbyFileList = data;
+
+                    //Track which index currently the user is viewing
+                    for (var i = 0; i < nearbyFileList.length; i++){
+                        var thisPath = nearbyFileList[i];
+                        if (thisPath == filepath.split("\\").join("/")){
+                            currentViewingIndex = i;
+                        }
+                    }
+                }
+            })
+        }
+
+        function nextImage(){
+            nextPhoto = currentViewingIndex + 1;
+            if (nextPhoto > nearbyFileList.length - 1){
+                nextPhoto = nearbyFileList.length - 1;
+            }
+
+            var filepath = nearbyFileList[nextPhoto];
+            var filename = filepath.split('/').pop();
+            if (nextPhoto != currentViewingIndex){
+                //Change in photo index
+                loadImage(filename, filepath);
+                 currentViewingIndex = nextPhoto;
+            }
+        }
+
+        function previousImage(){
+            nextPhoto = currentViewingIndex - 1;
+            if (nextPhoto < 0){
+                nextPhoto = 0;
+            }
+
+            var filepath = nearbyFileList[nextPhoto];
+            var filename = filepath.split('/').pop();
+            if (nextPhoto != currentViewingIndex){
+                //Change in photo index
+                loadImage(filename, filepath);
+                 currentViewingIndex = nextPhoto;
+            }
+        }
+
+        //Bind arrow key events
+        $("body").on("keydown", function(e){
+            var nextPhoto = currentViewingIndex;
+            if (e.keyCode == 37){
+                //<-
+                previousImage();
+            }else if (e.keyCode == 39){
+                //->
+                nextImage();
+            }else{
+                //Invalid keycode to operate
+                return;
+            }
+        })
+
+        loadNearbyFiles(playbackFile.filepath);
+
+        function loadImage(filename, filepath){
+            $("#img").hide();
+            ao_module_setWindowTitle("Photo - " + filename);
+            $("#img").attr("src", '../media?file=' + encodeURIComponent(filepath))
+            
+            //realigin to center
+            $('#img').on('load', function() {
+                updateImgSize();
+                $("#img").show();
+            });
+        }
 
         function updateImgSize() {
-            $('#img').css("margin-top", (window.innerHeight - $("#img").height()) / 2);
-            $('#img').css("margin-left", (window.innerWidth - $("#img").width()) / 2);
+            $('#img').css("margin-top", (window.innerHeight - $("#img").height()) / 2 - 6);
+            $('#img').css("margin-left", (window.innerWidth - $("#img").width()) / 2 - 6);
         }
     </script>
 </body>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 27 - 0
web/Photo/embedded/arrow-left.ai


+ 8 - 0
web/Photo/embedded/arrow-left.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<polygon fill="#FFFFFF" stroke="#231815" stroke-width="3" stroke-miterlimit="10" points="105.518,113.641 20.981,64.833 
+	105.518,16.027 "/>
+</svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 27 - 0
web/Photo/embedded/arrow-right.ai


+ 8 - 0
web/Photo/embedded/arrow-right.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<polygon fill="#FFFFFF" stroke="#231815" stroke-width="3" stroke-miterlimit="10" points="20.981,16.026 105.518,64.833 
+	20.981,113.64 "/>
+</svg>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio