Kaynağa Gözat

Added v1.120 release drawing

TC pushbot 5 4 yıl önce
ebeveyn
işleme
b055cc2180

BIN
documents/1.120 release drawing/2.1.jpg


BIN
documents/1.120 release drawing/2.1.png


BIN
documents/1.120 release drawing/2.1.psd


+ 2 - 2
file_system.go

@@ -181,11 +181,11 @@ func FileSystemInit() {
 	*/
 
 	//Clear tmp folder if files is placed here too long
-	RegisterNightlyTask(system_fs_clearOldTmpFiles)
+	nightlyManager.RegisterNightlyTask(system_fs_clearOldTmpFiles)
 
 	//Clear shares that its parent file no longer exists in the system
 	shareManager.ValidateAndClearShares()
-	RegisterNightlyTask(shareManager.ValidateAndClearShares)
+	nightlyManager.RegisterNightlyTask(shareManager.ValidateAndClearShares)
 
 }
 

+ 0 - 0
mod/arsm/README.md → legacy/arsm/README.md


BIN
legacy/arsm/aecron.zip


BIN
legacy/arsm/wsterminal.zip


+ 0 - 171
mod/arsm/wsterminal/common.go

@@ -1,171 +0,0 @@
-package wsterminal
-
-import (
-	"bufio"
-	"encoding/base64"
-	"errors"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"os"
-	"time"
-)
-
-/*
-	SYSTEM COMMON FUNCTIONS
-
-	This is a system function that put those we usually use function but not belongs to
-	any module / system.
-
-	E.g. fileExists / IsDir etc
-
-*/
-
-/*
-	Basic Response Functions
-
-	Send response with ease
-*/
-//Send text response with given w and message as string
-func sendTextResponse(w http.ResponseWriter, msg string) {
-	w.Write([]byte(msg))
-}
-
-//Send JSON response, with an extra json header
-func sendJSONResponse(w http.ResponseWriter, json string) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte(json))
-}
-
-func sendErrorResponse(w http.ResponseWriter, errMsg string) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
-}
-
-func sendOK(w http.ResponseWriter) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Write([]byte("\"OK\""))
-}
-
-/*
-	The paramter move function (mv)
-
-	You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
-	r (HTTP Request Object)
-	getParamter (string, aka $_GET['This string])
-
-	Will return
-	Paramter string (if any)
-	Error (if error)
-
-*/
-func mv(r *http.Request, getParamter string, postMode bool) (string, error) {
-	if postMode == false {
-		//Access the paramter via GET
-		keys, ok := r.URL.Query()[getParamter]
-
-		if !ok || len(keys[0]) < 1 {
-			//log.Println("Url Param " + getParamter +" is missing")
-			return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
-		}
-
-		// Query()["key"] will return an array of items,
-		// we only want the single item.
-		key := keys[0]
-		return string(key), nil
-	} else {
-		//Access the parameter via POST
-		r.ParseForm()
-		x := r.Form.Get(getParamter)
-		if len(x) == 0 || x == "" {
-			return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
-		}
-		return string(x), nil
-	}
-
-}
-
-func stringInSlice(a string, list []string) bool {
-	for _, b := range list {
-		if b == a {
-			return true
-		}
-	}
-	return false
-}
-
-func fileExists(filename string) bool {
-	_, err := os.Stat(filename)
-	if os.IsNotExist(err) {
-		return false
-	}
-	return true
-}
-
-func isDir(path string) bool {
-	if fileExists(path) == false {
-		return false
-	}
-	fi, err := os.Stat(path)
-	if err != nil {
-		log.Fatal(err)
-		return false
-	}
-	switch mode := fi.Mode(); {
-	case mode.IsDir():
-		return true
-	case mode.IsRegular():
-		return false
-	}
-	return false
-}
-
-func inArray(arr []string, str string) bool {
-	for _, a := range arr {
-		if a == str {
-			return true
-		}
-	}
-	return false
-}
-
-func timeToString(targetTime time.Time) string {
-	return targetTime.Format("2006-01-02 15:04:05")
-}
-
-func loadImageAsBase64(filepath string) (string, error) {
-	if !fileExists(filepath) {
-		return "", errors.New("File not exists")
-	}
-	f, _ := os.Open(filepath)
-	reader := bufio.NewReader(f)
-	content, _ := ioutil.ReadAll(reader)
-	encoded := base64.StdEncoding.EncodeToString(content)
-	return string(encoded), nil
-}
-
-func pushToSliceIfNotExist(slice []string, newItem string) []string {
-	itemExists := false
-	for _, item := range slice {
-		if item == newItem {
-			itemExists = true
-		}
-	}
-
-	if !itemExists {
-		slice = append(slice, newItem)
-	}
-
-	return slice
-}
-
-func removeFromSliceIfExists(slice []string, target string) []string {
-	newSlice := []string{}
-	for _, item := range slice {
-		if item != target {
-			newSlice = append(newSlice, item)
-		}
-	}
-
-	return newSlice
-}

+ 0 - 144
mod/arsm/wsterminal/wsterminal.go

@@ -1,144 +0,0 @@
-package wsterminal
-
-import (
-	"encoding/json"
-	"net/http"
-	"sync"
-	"time"
-
-	"github.com/gorilla/websocket"
-	"imuslab.com/arozos/mod/auth"
-)
-
-/*
-	WebSocket Terminal
-	Author: tobychui
-
-	This module is a remote support service that allow
-	reverse ssh like connection using websocket.
-
-	For normal weboscket based shell or WsTTY, see wstty module instead.
-*/
-
-type Connection struct {
-	RemoteName            string
-	RemoteUUID            string
-	RemoteIP              string
-	RemoteToken           string
-	ConnectionStartedTime int64
-	LastOnline            int64
-	connection            *websocket.Conn
-	terminateTicker       chan bool
-}
-
-type Server struct {
-	connectionPool sync.Map
-	upgrader       websocket.Upgrader
-	authAgent      *auth.AuthAgent
-}
-
-type Client struct {
-}
-
-//Create a new Server
-func NewWsTerminalServer(authAgent *auth.AuthAgent) *Server {
-	return &Server{
-		connectionPool: sync.Map{},
-		upgrader:       websocket.Upgrader{},
-		authAgent:      authAgent,
-	}
-}
-
-//List all the active connection that is current connected to this server
-func (s *Server) ListConnections(w http.ResponseWriter, r *http.Request) {
-	activeConnections := []Connection{}
-	s.connectionPool.Range(func(key, value interface{}) bool {
-		activeConnections = append(activeConnections, *value.(*Connection))
-		return true
-	})
-	js, _ := json.Marshal(activeConnections)
-	sendJSONResponse(w, string(js))
-}
-
-//Handle new connections
-func (s *Server) HandleConnection(w http.ResponseWriter, r *http.Request) {
-	//Get the token and validate it
-	token, err := mv(r, "token", false)
-	if err != nil {
-		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte(`401 Unauthorized - Invalid token given`))
-		return
-	}
-
-	//Try to get the uuid from connectio
-	uuid, err := mv(r, "uuid", false)
-	if err != nil {
-		uuid = "unknown"
-	}
-
-	//Valida te the token
-	valid, username := s.authAgent.ValidateAutoLoginToken(token)
-	if !valid {
-		//Invalid token
-		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte(`401 Unauthorized - Invalid token given`))
-		return
-	}
-
-	//Create a connection object
-	thisConnection := Connection{
-		RemoteName:            username,
-		RemoteUUID:            uuid,
-		RemoteIP:              "",
-		RemoteToken:           "",
-		ConnectionStartedTime: time.Now().Unix(),
-		LastOnline:            time.Now().Unix(),
-	}
-
-	//Check if the same connection already exists. If yes, disconnect the old one
-	val, ok := s.connectionPool.Load(username)
-	if ok {
-		//Connection already exists. Disconenct the old one first
-		previousConn := val.(*Connection).connection
-		previousConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
-		time.Sleep(1 * time.Second)
-		previousConn.Close()
-	}
-
-	//Upgrade the current connection
-	c, err := s.upgrader.Upgrade(w, r, nil)
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		w.Write([]byte(`500 Internal Server Error`))
-		return
-	}
-
-	thisConnection.connection = c
-
-	//Create a timer for poking the client and check if it is still online
-	ticker := time.NewTicker(5 * time.Minute)
-	done := make(chan bool)
-
-	thisConnection.terminateTicker = done
-	go func(connectionObject Connection) {
-		for {
-			select {
-			case <-done:
-				//Termination from another thread
-				return
-			case <-ticker.C:
-				//Send a ping signal to the client
-				if err := connectionObject.connection.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
-					//Unable to send ping message. Assume closed. Remove from active connection pool
-					s.connectionPool.Delete(thisConnection.RemoteName)
-					return
-				} else {
-					connectionObject.LastOnline = time.Now().Unix()
-				}
-			}
-		}
-	}(thisConnection)
-
-	//Store the connection object to the connection pool
-	s.connectionPool.Store(username, &thisConnection)
-}

+ 8 - 3
mod/filesystem/config.go

@@ -16,8 +16,13 @@ type FileSystemOption struct {
 	Filesystem string `json:"filesystem,omitempty"` //Support {"ext4","ext2", "ext3", "fat", "vfat", "ntfs"}
 	Mountdev   string `json:"mountdev,omitempty"`   //Device file (e.g. /dev/sda1)
 	Mountpt    string `json:"mountpt,omitempty"`    //Device mount point (e.g. /media/storage1)
-	Username   string `json:"username,omitempty"`   //Username if the storage require auth
-	Password   string `json:"password,omitempty"`   //Password if the storage require auth
+
+	//Backup Hierarchy Options
+	Parentuid  string `json:"parentuid,omitempty"`  //The parent mount point for backup source, backup disk only
+	BackupMode string `json:"backupmode,omitempty"` //Backup mode of the virtual disk
+
+	Username string `json:"username,omitempty"` //Username if the storage require auth
+	Password string `json:"password,omitempty"` //Password if the storage require auth
 }
 
 //Parse a list of StorageConfig from the given json content
@@ -45,7 +50,7 @@ func ValidateOption(options *FileSystemOption) error {
 		return errors.New("Not supported access mode: " + options.Access)
 	}
 
-	if !inSlice([]string{"user", "public"}, options.Hierarchy) {
+	if !inSlice([]string{"user", "public", "backup"}, options.Hierarchy) {
 		return errors.New("Not supported hierarchy: " + options.Hierarchy)
 	}
 

+ 9 - 1
mod/filesystem/filesystem.go

@@ -43,6 +43,7 @@ type FileSystemHandler struct {
 	Path               string
 	Hierarchy          string
 	ReadOnly           bool
+	Parentuid          string
 	InitiationTime     int64
 	FilesystemDatabase *db.Database
 	Filesystem         string
@@ -94,15 +95,22 @@ func NewFileSystemHandler(option FileSystemOption) (*FileSystemHandler, error) {
 		}
 
 		//Create the fsdb for this handler
-		fsdb, err := db.NewDatabase(filepath.ToSlash(filepath.Clean(option.Path))+"/aofs.db", false)
+		fsdb, err := db.NewDatabase(filepath.ToSlash(filepath.Join(filepath.Clean(option.Path), "aofs.db")), false)
 		if err != nil {
 			return &FileSystemHandler{}, errors.New("Unable to create fsdb inside the target path. Is the directory read only?")
 		}
+
+		if option.Hierarchy == "backup" {
+			//This is a backup drive.
+
+		}
+
 		return &FileSystemHandler{
 			Name:               option.Name,
 			UUID:               option.Uuid,
 			Path:               filepath.ToSlash(filepath.Clean(option.Path)) + "/",
 			ReadOnly:           option.Access == "readonly",
+			Parentuid:          option.Parentuid,
 			Hierarchy:          option.Hierarchy,
 			InitiationTime:     time.Now().Unix(),
 			FilesystemDatabase: fsdb,

+ 50 - 0
mod/time/nightly/nightly.go

@@ -0,0 +1,50 @@
+package nightly
+
+import "time"
+
+/*
+	Nightly.go
+	Author: tobychui
+
+	This module handles tasks that have to be done every night
+	like updating all user storage capacity and clean trash etc
+
+*/
+
+type TaskManager struct {
+	NightlTasks []func()
+}
+
+func NewNightlyTaskManager(nightlyTaskRunTime int) *TaskManager {
+	//Create a new return structure
+	thisManager := TaskManager{
+		NightlTasks: []func(){},
+	}
+	//Start the nightly scheduler
+	go func(tm *TaskManager) {
+		t := time.Now()
+		n := time.Date(t.Year(), t.Month(), t.Day(), nightlyTaskRunTime, 0, 0, 0, t.Location())
+		d := n.Sub(t)
+		if d < 0 {
+			n = n.Add(24 * time.Hour)
+			d = n.Sub(t)
+		}
+		for {
+			time.Sleep(d)
+			d = 24 * time.Hour
+			tm.NightlyTaskRun()
+		}
+	}(&thisManager)
+
+	return &thisManager
+}
+
+func (tm *TaskManager) NightlyTaskRun() {
+	for _, nightlyTask := range tm.NightlTasks {
+		nightlyTask()
+	}
+}
+
+func (tm *TaskManager) RegisterNightlyTask(task func()) {
+	tm.NightlTasks = append(tm.NightlTasks, task)
+}

+ 1 - 1
mod/arsm/aecron/common.go → mod/time/scheduler/common.go

@@ -1,4 +1,4 @@
-package aecron
+package scheduler
 
 import (
 	"bufio"

+ 5 - 5
mod/arsm/aecron/handlers.go → mod/time/scheduler/handlers.go

@@ -1,4 +1,4 @@
-package aecron
+package scheduler
 
 import (
 	"encoding/json"
@@ -13,7 +13,7 @@ import (
 )
 
 //List all the jobs related to the given user
-func (a *Aecron) HandleListJobs(w http.ResponseWriter, r *http.Request) {
+func (a *Scheduler) HandleListJobs(w http.ResponseWriter, r *http.Request) {
 	userinfo, err := a.userHandler.GetUserInfoFromRequest(w, r)
 	if err != nil {
 		sendErrorResponse(w, "User not logged in")
@@ -52,7 +52,7 @@ func (a *Aecron) HandleListJobs(w http.ResponseWriter, r *http.Request) {
 	sendJSONResponse(w, string(js))
 }
 
-func (a *Aecron) HandleAddJob(w http.ResponseWriter, r *http.Request) {
+func (a *Scheduler) HandleAddJob(w http.ResponseWriter, r *http.Request) {
 	userinfo, err := a.userHandler.GetUserInfoFromRequest(w, r)
 	if err != nil {
 		sendErrorResponse(w, "User not logged in")
@@ -159,7 +159,7 @@ func (a *Aecron) HandleAddJob(w http.ResponseWriter, r *http.Request) {
 	sendOK(w)
 }
 
-func (a *Aecron) HandleJobRemoval(w http.ResponseWriter, r *http.Request) {
+func (a *Scheduler) HandleJobRemoval(w http.ResponseWriter, r *http.Request) {
 	userinfo, err := a.userHandler.GetUserInfoFromRequest(w, r)
 	if err != nil {
 		sendErrorResponse(w, "User not logged in")
@@ -207,7 +207,7 @@ func (a *Aecron) HandleJobRemoval(w http.ResponseWriter, r *http.Request) {
 	sendOK(w)
 }
 
-func (a *Aecron) HandleShowLog(w http.ResponseWriter, r *http.Request) {
+func (a *Scheduler) HandleShowLog(w http.ResponseWriter, r *http.Request) {
 	filename, _ := mv(r, "filename", false)
 	if filename == "" {
 		//Show index

+ 9 - 9
mod/arsm/aecron/aecron.go → mod/time/scheduler/scheduler.go

@@ -1,4 +1,4 @@
-package aecron
+package scheduler
 
 import (
 	"encoding/json"
@@ -35,7 +35,7 @@ type Job struct {
 	ScriptFile        string //The script file being called. Can be an agi script (.agi / .js) or shell script (.bat or .sh)
 }
 
-type Aecron struct {
+type Scheduler struct {
 	jobs        []*Job
 	cronfile    string
 	userHandler *user.UserHandler
@@ -47,7 +47,7 @@ var (
 	logFolder string = "./system/aecron/"
 )
 
-func NewArozEmulatedCrontab(userHandler *user.UserHandler, gateway *agi.Gateway, cronfile string) (*Aecron, error) {
+func NewScheduler(userHandler *user.UserHandler, gateway *agi.Gateway, cronfile string) (*Scheduler, error) {
 	if !fileExists(cronfile) {
 		//Cronfile not exists. Create it
 		emptyJobList := []*Job{}
@@ -65,7 +65,7 @@ func NewArozEmulatedCrontab(userHandler *user.UserHandler, gateway *agi.Gateway,
 	}
 
 	//Create the ArOZ Emulated Crontask
-	aecron := Aecron{
+	aecron := Scheduler{
 		jobs:        jobs,
 		userHandler: userHandler,
 		gateway:     gateway,
@@ -115,7 +115,7 @@ func loadJobsFromFile(cronfile string) ([]*Job, error) {
 	return jobsPointers, nil
 }
 
-func (a *Aecron) createTicker(duration time.Duration) chan bool {
+func (a *Scheduler) createTicker(duration time.Duration) chan bool {
 	ticker := time.NewTicker(duration)
 	stop := make(chan bool, 1)
 
@@ -183,14 +183,14 @@ func (a *Aecron) createTicker(duration time.Duration) chan bool {
 	return stop
 }
 
-func (a *Aecron) Close() {
+func (a *Scheduler) Close() {
 	if a.ticker != nil {
 		//Stop the ticker
 		a.ticker <- true
 	}
 }
 
-func (a *Aecron) GetScheduledJobByName(name string) *Job {
+func (a *Scheduler) GetScheduledJobByName(name string) *Job {
 	for _, thisJob := range a.jobs {
 		if thisJob.Name == name {
 			return thisJob
@@ -200,7 +200,7 @@ func (a *Aecron) GetScheduledJobByName(name string) *Job {
 	return nil
 }
 
-func (a *Aecron) RemoveJobFromScheduleList(taskName string) {
+func (a *Scheduler) RemoveJobFromScheduleList(taskName string) {
 	newJobSlice := []*Job{}
 	for _, j := range a.jobs {
 		if j.Name != taskName {
@@ -211,7 +211,7 @@ func (a *Aecron) RemoveJobFromScheduleList(taskName string) {
 	a.jobs = newJobSlice
 }
 
-func (a *Aecron) JobExists(name string) bool {
+func (a *Scheduler) JobExists(name string) bool {
 	targetJob := a.GetScheduledJobByName(name)
 	if targetJob == nil {
 		return false

+ 1 - 1
quota.go

@@ -29,7 +29,7 @@ func DiskQuotaInit() {
 	})
 
 	//Register the timer for running the global user quota recalculation
-	RegisterNightlyTask(system_disk_quota_updateAllUserQuotaEstimation)
+	nightlyManager.RegisterNightlyTask(system_disk_quota_updateAllUserQuotaEstimation)
 }
 
 //Register the handler for automatically updating all user storage quota

+ 94 - 0
scheduler.go

@@ -0,0 +1,94 @@
+package main
+
+import (
+	"log"
+	"net/http"
+
+	module "imuslab.com/arozos/mod/modules"
+	prout "imuslab.com/arozos/mod/prouter"
+	"imuslab.com/arozos/mod/time/nightly"
+	"imuslab.com/arozos/mod/time/scheduler"
+)
+
+/*
+	Nightly.go
+	author: tobychui
+
+	This is a handle for putting everything that is required to run everynight.
+	Default: Run once every day 3am in the morning.
+
+*/
+
+var (
+	nightlyManager  *nightly.TaskManager
+	systemScheduler *scheduler.Scheduler
+)
+
+func SchedulerInit() {
+	/*
+		Nighty Task Manager
+
+		The tasks that should be done once per night. Internal function only.
+	*/
+	nightlyManager = nightly.NewNightlyTaskManager(*nightlyTaskRunTime)
+
+	/*
+		System Scheudler
+
+		The internal scheudler for arozos
+	*/
+	//Create an user router and its module
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "Tasks Scheduler",
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	//Register the module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:        "Tasks Scheduler",
+		Group:       "System Tools",
+		IconPath:    "SystemAO/arsm/img/scheduler.png",
+		Version:     "1.2",
+		StartDir:    "SystemAO/arsm/scheduler.html",
+		SupportFW:   true,
+		InitFWSize:  []int{1080, 580},
+		LaunchFWDir: "SystemAO/arsm/scheduler.html",
+		SupportEmb:  false,
+	})
+
+	//Startup the ArOZ Emulated Crontab Service
+	newScheduler, err := scheduler.NewScheduler(userHandler, AGIGateway, "system/cron.json")
+	if err != nil {
+		log.Println("ArOZ Emulated Cron Startup Failed. Stopping all scheduled tasks.")
+	}
+
+	systemScheduler = newScheduler
+
+	//Register Endpoints
+	http.HandleFunc("/system/arsm/aecron/list", func(w http.ResponseWriter, r *http.Request) {
+		if authAgent.CheckAuth(r) {
+			//User logged in
+			newScheduler.HandleListJobs(w, r)
+		} else {
+			//User not logged in
+			http.NotFound(w, r)
+		}
+	})
+	router.HandleFunc("/system/arsm/aecron/add", newScheduler.HandleAddJob)
+	router.HandleFunc("/system/arsm/aecron/remove", newScheduler.HandleJobRemoval)
+	router.HandleFunc("/system/arsm/aecron/listlog", newScheduler.HandleShowLog)
+
+	//Register settings
+	registerSetting(settingModule{
+		Name:         "Tasks Scheduler",
+		Desc:         "System Tasks and Excution Scheduler",
+		IconPath:     "SystemAO/arsm/img/small_icon.png",
+		Group:        "Cluster",
+		StartDir:     "SystemAO/arsm/aecron.html",
+		RequireAdmin: false,
+	})
+}

+ 3 - 3
startup.go

@@ -49,6 +49,7 @@ func RunStartup() {
 	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
@@ -73,7 +74,8 @@ func RunStartup() {
 	NetworkServiceInit() //Initalize network serves (ssdp / mdns etc)
 	WiFiInit()           //Inialize WiFi management module
 
-	ArsmInit() //Inialize ArOZ Remote Support & Management Framework
+	//ARSM Moved to scheduler, remote support is rewrite pending
+	//ArsmInit() //Inialize ArOZ Remote Support & Management Framework
 
 	//11. Other stuffs
 	util_init()
@@ -89,8 +91,6 @@ func RunStartup() {
 
 	ModuleInstallerInit() //Start Module Installer
 
-	NightlyInit() //Start Nightly Tasks
-
 	//Finally
 	moduleHandler.ModuleSortList() //Sort the system module list
 

+ 18 - 5
storage.pool.go

@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"imuslab.com/arozos/mod/database"
+	"imuslab.com/arozos/mod/permission"
 
 	"github.com/tidwall/pretty"
 	fs "imuslab.com/arozos/mod/filesystem"
@@ -197,7 +198,7 @@ func HandleFSHToggle(w http.ResponseWriter, r *http.Request) {
 	}
 
 	//Check if group exists
-	if !permissionHandler.GroupExists(group) {
+	if group != "system" && !permissionHandler.GroupExists(group) {
 		sendErrorResponse(w, "Group not exists")
 		return
 	}
@@ -209,8 +210,16 @@ func HandleFSHToggle(w http.ResponseWriter, r *http.Request) {
 	}
 
 	//Check if fsh exists
-	targetpg := permissionHandler.GetPermissionGroupByName(group)
-	storagePool := targetpg.StoragePool
+	var targetpg *permission.PermissionGroup
+	var storagePool *storage.StoragePool
+	if group == "system" {
+		//System storage pool.
+		storagePool = baseStoragePool
+	} else {
+		targetpg = permissionHandler.GetPermissionGroupByName(group)
+		storagePool = targetpg.StoragePool
+	}
+
 	var targetFSH *fs.FileSystemHandler
 	for _, thisFsh := range storagePool.Storages {
 		if thisFsh.UUID == fsh {
@@ -410,8 +419,12 @@ func buildOptionFromRequestForm(r *http.Request) fs.FileSystemOption {
 		Filesystem: r.FormValue("filesystem"),
 		Mountdev:   r.FormValue("mountdev"),
 		Mountpt:    r.FormValue("mountpt"),
-		Username:   r.FormValue("username"),
-		Password:   r.FormValue("password"),
+
+		Parentuid:  r.FormValue("parentuid"),
+		BackupMode: r.FormValue("backupmode"),
+
+		Username: r.FormValue("username"),
+		Password: r.FormValue("password"),
 	}
 
 	return newFsOption

+ 76 - 15
web/SystemAO/storage/poolEditor.html

@@ -43,6 +43,10 @@
         .true{
             color: #05b074;
         }
+
+        .backuponly{
+            display:none;
+        }
     </style>
 </head>
 <body>
@@ -86,30 +90,57 @@
                     <label>Path</label>
                     <input type="text" name="path" placeholder="e.g. /media/myfolder">
                 </div>
-                <div class="field">
+                <div id="accPermission" class="field">
                     <label>Access Permission</label>
                     <div class="ui selection dropdown">
-                    <input type="hidden" name="access" value="readwrite">
-                    <i class="dropdown icon"></i>
-                    <div class="default text">Access Permission</div>
-                    <div class="menu">
-                        <div class="item" data-value="readonly">Read Only</div>
-                        <div class="item" data-value="readwrite">Read Write</div>
-                    </div>
+                        <input type="hidden" name="access" value="readwrite">
+                        <i class="dropdown icon"></i>
+                        <div class="default text">Access Permission</div>
+                        <div class="menu">
+                            <div class="item" data-value="readonly">Read Only</div>
+                            <div class="item" data-value="readwrite">Read Write</div>
+                        </div>
                     </div>
                 </div>
                 <div class="field">
                     <label>Storage Hierarchy</label>
                     <div class="ui selection dropdown">
-                    <input type="hidden" name="hierarchy" value="user">
-                    <i class="dropdown icon"></i>
-                    <div class="default text">Storage Hierarchy</div>
-                    <div class="menu">
-                        <div class="item" data-value="user">Isolated User Folders</div>
-                        <div class="item" data-value="public">Public Access Folders</div>
-                    </div>
+                        <input type="hidden" autocomplete="false" name="hierarchy" value="user" onchange="handleHierarchyChange(this.value);">
+                        <i class="dropdown icon"></i>
+                        <div class="default text">Storage Hierarchy</div>
+                        <div class="menu">
+                            <div class="item" data-value="user">Isolated User Folders</div>
+                            <div class="item" data-value="public">Public Access Folders</div>
+                            <div class="item" data-value="backup">Backup Storage</div>
+                        </div>
                     </div>
                 </div>
+                <div class="ui divider backuponly"></div>
+                <p class="backuponly">Backup Settings</p>
+                <div class="field backuponly">
+                    <label>Backup Virtual Disk UID</label>
+                    <div class="ui selection dropdown">
+                        <input type="hidden" autocomplete="false" name="parentuid" value="">
+                        <i class="dropdown icon"></i>
+                        <div class="default text">Storage Hierarchy</div>
+                        <div class="menu" id="backupIdList">
+                            
+                        </div>
+                    </div>
+                  </div>
+                  <div class="field backuponly">
+                    <label>Backup Mode</label>
+                    <div class="ui selection dropdown">
+                        <input type="hidden" autocomplete="false" name="backupmode" value="">
+                        <i class="dropdown icon"></i>
+                        <div class="default text">Storage Hierarchy</div>
+                        <div class="menu">
+                            <div class="item" data-value="smart">Smart</div>
+                            <div class="item" data-value="nightly">Nightly</div>
+                            <div class="item" data-value="append">Append Only</div>
+                        </div>
+                    </div>
+                  </div>
                 <div class="ui divider"></div>
                 <p>Physical Disks Settings</p>
                 <div class="field">
@@ -301,6 +332,36 @@
             });
         }
 
+        function handleHierarchyChange(newHierarchy){
+            if (newHierarchy == "backup"){
+
+                //Force readwrite mode to read only
+                $("#accPermission").find(".dropdown").dropdown("set selected", "readonly");
+                $("#accPermission").addClass("disabled");
+
+                //Show backup only divs
+                $(".backuponly").slideDown("fast");
+
+                //Render the backup id list
+                $("#backupIdList").html(``);
+                $.get("../../system/storage/pool/list", function(data){
+                     data.forEach(usergroup => {
+                         usergroup.Storages.forEach(storage => {
+                             $("#backupIdList").append(`<div class="item" data-value="${storage.UUID}">${storage.Name} (${storage.UUID}:/)</div>`);
+                         })
+                     });
+                     $("#backupIdList").parent().dropdown();
+                });
+            }else{
+                //Restore readwrite mode
+                $("#accPermission").removeClass("disabled");
+
+                 //Hide backup only divs
+                 $(".backuponly").slideUp("fast");
+                 $(".backuponly").find("input").val("");
+            }
+        }
+
         function removeThisFSH(object){
             var uuid = $(object).attr("uuid");
             var group  = $(object).attr("group");