Przeglądaj źródła

Finalized storage pool upgrade

Toby Chui 3 lat temu
rodzic
commit
378940fb25

+ 5 - 1
desktop.go

@@ -511,7 +511,11 @@ func desktop_theme_handler(w http.ResponseWriter, r *http.Request) {
 		}
 	} else if loadUserTheme != "" {
 		//Load user theme base on folder path
-		userFsh, _ := GetFsHandlerByUUID("user:/")
+		userFsh, err := GetFsHandlerByUUID("user:/")
+		if err != nil {
+			common.SendErrorResponse(w, "Unable to resolve user root path")
+			return
+		}
 		userFshAbs := userFsh.FileSystemAbstraction
 		rpath, err := userFshAbs.VirtualPathToRealPath(loadUserTheme, userinfo.Username)
 		if err != nil {

+ 10 - 3
file_system.go

@@ -256,8 +256,11 @@ func system_fs_handleFileSearch(w http.ResponseWriter, r *http.Request) {
 		common.SendErrorResponse(w, "Invalid path given")
 		return
 	}
-	targetFSH, _ = GetFsHandlerByUUID(vrootID)
-
+	targetFSH, err = GetFsHandlerByUUID(vrootID)
+	if err != nil {
+		common.SendErrorResponse(w, err.Error())
+		return
+	}
 	//Translate the vpath to realpath if this is an actual path on disk
 	resolvedPath, err := targetFSH.FileSystemAbstraction.VirtualPathToRealPath(vpath, userinfo.Username)
 	if err != nil {
@@ -2375,7 +2378,11 @@ func system_fs_getFileProperties(w http.ResponseWriter, r *http.Request) {
 	}
 
 	vrootID, subpath, _ := filesystem.GetIDFromVirtualPath(vpath)
-	fsh, _ := GetFsHandlerByUUID(vrootID)
+	fsh, err := GetFsHandlerByUUID(vrootID)
+	if err != nil {
+		common.SendErrorResponse(w, err.Error())
+		return
+	}
 	fshAbs := fsh.FileSystemAbstraction
 
 	rpath, err := fshAbs.VirtualPathToRealPath(subpath, userinfo.Username)

+ 5 - 28
mod/filesystem/config.go

@@ -3,7 +3,6 @@ package filesystem
 import (
 	"encoding/json"
 	"errors"
-	"strings"
 
 	"imuslab.com/arozos/mod/filesystem/fsdef"
 )
@@ -20,10 +19,6 @@ type FileSystemOption struct {
 	Mountdev   string `json:"mountdev,omitempty"`   //Device file (e.g. /dev/sda1)
 	Mountpt    string `json:"mountpt,omitempty"`    //Device mount point (e.g. /media/storage1)
 
-	//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
 }
@@ -47,7 +42,7 @@ func ValidateOption(options *FileSystemOption) error {
 	}
 
 	//Check if uuid is reserved by system
-	if inSlice([]string{"user", "tmp", "share", "network"}, options.Uuid) {
+	if inSlice([]string{"user", "tmp", "network"}, options.Uuid) {
 		return errors.New("This File System Handler UUID is reserved by the system")
 	}
 
@@ -61,36 +56,18 @@ func ValidateOption(options *FileSystemOption) error {
 	}
 
 	//Check if hierarchy is supported
-	if !inSlice([]string{"user", "public", "backup"}, options.Hierarchy) {
+	if !inSlice([]string{"user", "public"}, 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) {
+	if !inSlice([]string{"ext4", "ext2", "ext3", "fat", "vfat", "ntfs", "webdav", "ftp"}, 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")
-		}
+	if options.Mountpt != "" {
+		return errors.New("Mount point cannot be empty")
 	}
 
 	return nil

+ 6 - 5
mod/filesystem/filesystem.go

@@ -161,7 +161,6 @@ func NewFileSystemHandler(option FileSystemOption) (*FileSystemHandler, error) {
 			Path:                  filepath.ToSlash(filepath.Clean(option.Path)) + "/",
 			ReadOnly:              option.Access == fsdef.FsReadOnly,
 			RequireBuffer:         false,
-			Parentuid:             option.Parentuid,
 			Hierarchy:             option.Hierarchy,
 			HierarchyConfig:       DefaultEmptyHierarchySpecificConfig,
 			InitiationTime:        time.Now().Unix(),
@@ -187,7 +186,6 @@ func NewFileSystemHandler(option FileSystemOption) (*FileSystemHandler, error) {
 			Path:                  option.Path,
 			ReadOnly:              option.Access == fsdef.FsReadOnly,
 			RequireBuffer:         true,
-			Parentuid:             option.Parentuid,
 			Hierarchy:             option.Hierarchy,
 			HierarchyConfig:       nil,
 			InitiationTime:        time.Now().Unix(),
@@ -227,11 +225,14 @@ func (fsh *FileSystemHandler) IsRootOf(vpath string) bool {
 	return strings.HasPrefix(vpath, fsh.UUID+":")
 }
 
-func (fsh *FileSystemHandler) GetUniquePathHash(vpath string, username string) string {
+func (fsh *FileSystemHandler) GetUniquePathHash(vpath string, username string) (string, error) {
 	fshAbs := fsh.FileSystemAbstraction
 	rpath := ""
 	if strings.Contains(vpath, ":/") {
-		rpath, _ = fshAbs.VirtualPathToRealPath(vpath, username)
+		rpath, err := fshAbs.VirtualPathToRealPath(vpath, username)
+		if err != nil {
+			return "", err
+		}
 		rpath = filepath.ToSlash(rpath)
 	} else {
 		//Passed in realpath as vpath.
@@ -239,7 +240,7 @@ func (fsh *FileSystemHandler) GetUniquePathHash(vpath string, username string) s
 	}
 
 	hash := md5.Sum([]byte(fsh.UUID + "_" + rpath))
-	return hex.EncodeToString(hash[:])
+	return hex.EncodeToString(hash[:]), nil
 }
 
 func (fsh *FileSystemHandler) GetDirctorySizeFromRealPath(rpath string, includeHidden bool) (int64, int) {

+ 28 - 11
mod/share/share.go

@@ -822,8 +822,11 @@ func (s *Manager) HandleShareCheck(w http.ResponseWriter, r *http.Request) {
 	}
 
 	fsh, _ := userinfo.GetFileSystemHandlerFromVirtualPath(vpath)
-	pathHash := shareEntry.GetPathHash(fsh, vpath, userinfo.Username)
-
+	pathHash, err := shareEntry.GetPathHash(fsh, vpath, userinfo.Username)
+	if err != nil {
+		sendErrorResponse(w, "Unable to get share from given path")
+		return
+	}
 	type Result struct {
 		IsShared  bool
 		ShareUUID *shareEntry.ShareOption
@@ -1190,8 +1193,7 @@ func (s *Manager) GetPathHashFromShare(thisShareOption *shareEntry.ShareOption)
 	if err != nil {
 		return "", err
 	}
-	pathHash := shareEntry.GetPathHash(fsh, vpath, userinfo.Username)
-	return pathHash, nil
+	return shareEntry.GetPathHash(fsh, vpath, userinfo.Username)
 }
 
 //Check and clear shares that its pointinf files no longe exists
@@ -1244,8 +1246,10 @@ func (s *Manager) CanModifyShareEntry(userinfo *user.User, vpath string) bool {
 }
 
 func (s *Manager) DeleteShareByVpath(userinfo *user.User, vpath string) error {
-	ps := getPathHashFromUsernameAndVpath(userinfo, vpath)
-
+	ps, err := getPathHashFromUsernameAndVpath(userinfo, vpath)
+	if err != nil {
+		return err
+	}
 	if !s.CanModifyShareEntry(userinfo, vpath) {
 		return errors.New("Permission denied")
 	}
@@ -1266,12 +1270,18 @@ func (s *Manager) DeleteShareByUUID(userinfo *user.User, uuid string) error {
 }
 
 func (s *Manager) GetShareUUIDFromUserAndVpath(userinfo *user.User, vpath string) string {
-	ps := getPathHashFromUsernameAndVpath(userinfo, vpath)
+	ps, err := getPathHashFromUsernameAndVpath(userinfo, vpath)
+	if err != nil {
+		return ""
+	}
 	return s.options.ShareEntryTable.GetShareUUIDFromPathHash(ps)
 }
 
 func (s *Manager) GetShareObjectFromUserAndVpath(userinfo *user.User, vpath string) *shareEntry.ShareOption {
-	ps := getPathHashFromUsernameAndVpath(userinfo, vpath)
+	ps, err := getPathHashFromUsernameAndVpath(userinfo, vpath)
+	if err != nil {
+		return nil
+	}
 	return s.options.ShareEntryTable.GetShareObjectFromPathHash(ps)
 }
 
@@ -1280,7 +1290,11 @@ func (s *Manager) GetShareObjectFromUUID(uuid string) *shareEntry.ShareOption {
 }
 
 func (s *Manager) FileIsShared(userinfo *user.User, vpath string) bool {
-	ps := getPathHashFromUsernameAndVpath(userinfo, vpath)
+	ps, err := getPathHashFromUsernameAndVpath(userinfo, vpath)
+	if err != nil {
+		return false
+	}
+
 	return s.options.ShareEntryTable.FileIsShared(ps)
 }
 
@@ -1295,7 +1309,10 @@ func (s *Manager) RemoveShareByUUID(userinfo *user.User, uuid string) error {
 	return s.options.ShareEntryTable.RemoveShareByUUID(uuid)
 }
 
-func getPathHashFromUsernameAndVpath(userinfo *user.User, vpath string) string {
-	fsh, _ := userinfo.GetFileSystemHandlerFromVirtualPath(vpath)
+func getPathHashFromUsernameAndVpath(userinfo *user.User, vpath string) (string, error) {
+	fsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(vpath)
+	if err != nil {
+		return "", err
+	}
 	return shareEntry.GetPathHash(fsh, vpath, userinfo.Username)
 }

+ 6 - 2
mod/share/shareEntry/shareEntry.go

@@ -76,7 +76,11 @@ func (s *ShareEntryTable) CreateNewShare(srcFsh *filesystem.FileSystemHandler, v
 		return nil, errors.New("Unable to find the file on disk")
 	}
 
-	sharePathHash := GetPathHash(srcFsh, vpath, username)
+	sharePathHash, err := GetPathHash(srcFsh, vpath, username)
+
+	if err != nil {
+		return nil, err
+	}
 
 	//Check if the share already exists. If yes, use the previous link
 	val, ok := s.FileToUrlMap.Load(sharePathHash)
@@ -249,6 +253,6 @@ func (s *ShareEntryTable) ResolveShareOptionFromShareSubpath(subpath string) (*S
 	}
 }
 
-func GetPathHash(fsh *filesystem.FileSystemHandler, vpath string, username string) string {
+func GetPathHash(fsh *filesystem.FileSystemHandler, vpath string, username string) (string, error) {
 	return fsh.GetUniquePathHash(vpath, username)
 }

+ 163 - 187
mod/storage/ftp/aofs.go

@@ -5,7 +5,6 @@ package ftp
 
 import (
 	"errors"
-	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -17,154 +16,94 @@ import (
 	"imuslab.com/arozos/mod/user"
 )
 
+var (
+	aofsCanRead  = 1
+	aofsCanWrite = 2
+)
+
 type aofs struct {
 	userinfo  *user.User
 	tmpFolder string
 }
 
 func (a aofs) Create(name string) (afero.File, error) {
-	/*
-		rewritePath, _, err := a.pathRewrite(name)
-		if err != nil {
-			return nil, err
-		}
-		if !a.checkAllowAccess(rewritePath, "write") {
-			return nil, errors.New("Permission Denied")
-		}
-		//log.Println("Create", rewritePath)
-		fd, err := os.Create(rewritePath)
-		if err != nil {
-			return nil, err
-		}
-		return fd, nil
-	*/
-	return nil, nil
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Create(rewritePath)
 }
 
 func (a aofs) Chown(name string, uid, gid int) error {
-	/*
-		rewritePath, _, err := a.pathRewrite(name)
-		if err != nil {
-			return err
-		}
-
-		if !a.checkAllowAccess(rewritePath, "write") {
-			return errors.New("Permission Denied")
-		}
-
-		return os.Chown(name, uid, gid)
-	*/
-	return nil
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chown(rewritePath, uid, gid)
 }
 
 func (a aofs) Mkdir(name string, perm os.FileMode) error {
-	/*
-		rewritePath, _, err := a.pathRewrite(name)
-		if err != nil {
-			return err
-		}
-		if !a.checkAllowAccess(rewritePath, "write") {
-			return errors.New("Permission Denied")
-		}
-
-		os.Mkdir(rewritePath, perm)
-
-	*/
-	return nil
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Mkdir(rewritePath, perm)
 }
 
 func (a aofs) MkdirAll(path string, perm os.FileMode) error {
-	/*
-		rewritePath, _, err := a.pathRewrite(path)
-		if err != nil {
-			return err
-		}
-		if !a.checkAllowAccess(rewritePath, "write") {
-			return errors.New("Permission Denied")
-		}
-
-		os.MkdirAll(rewritePath, perm)
-		return nil
-	*/
-	return nil
+	fsh, rewritePath, err := a.pathRewrite(path)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.MkdirAll(rewritePath, perm)
 }
 
 func (a aofs) Open(name string) (afero.File, error) {
-	fmt.Println("FTP OPEN")
-	/*
-		rewritePath, _, err := a.pathRewrite(name)
-		if err != nil {
-			return nil, err
-		}
-		if !a.checkAllowAccess(rewritePath, "read") {
-			return nil, errors.New("Permission Denied")
-		}
-		//log.Println("Open", name, rewritePath)
-
-		fd, err := os.Open(rewritePath)
-		if err != nil {
-			return nil, err
-		}
-
-		return fd, nil
-	*/
-	return nil, nil
+	//fmt.Println("FTP OPEN")
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Open(rewritePath)
 }
 
 func (a aofs) Stat(name string) (os.FileInfo, error) {
-	fmt.Println("FTP STAT")
-	/*
-		rewritePath, _, err := a.pathRewrite(name)
-		if err != nil {
-			return nil, err
-		}
-		if !a.checkAllowAccess(rewritePath, "read") {
-			return nil, errors.New("Permission Denied")
-		}
-		//log.Println("Stat", rewritePath)
-		fileStat, err := os.Stat(rewritePath)
-
-		return fileStat, err
-	*/
-	return nil, nil
+	//fmt.Println("FTP STAT")
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanRead) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Stat(rewritePath)
 }
 
 func (a aofs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
-	fmt.Println("FTP OPEN FILE")
-	/*
-		rewritePath, _, err := a.pathRewrite(name)
-		if err != nil {
-			return nil, err
-		}
-		//log.Println("OpenFile", rewritePath)
-		if !fileExists(rewritePath) {
-			if !a.checkAllowAccess(rewritePath, "write") {
-				return nil, errors.New("Directory is Read Only")
-			}
-
-			//Set ownership of this file to user.
-			//Cannot use SetOwnership due to the filesize of the given file didn't exists yet
-			fsh, _ := a.userinfo.GetFileSystemHandlerFromRealPath(rewritePath)
-			fsh.CreateFileRecord(rewritePath, a.userinfo.Username)
-
-			//Create the upload pending file
-			fd, err := os.Create(rewritePath)
-			if err != nil {
-				return nil, err
-			}
-			return fd, nil
-		} else {
-			if !a.checkAllowAccess(rewritePath, "read") {
-				return nil, errors.New("Permission Denied")
-			}
-			fd, err := os.Open(rewritePath)
-			if err != nil {
-				return nil, err
-			}
-			return fd, nil
-		}
-	*/
-	return nil, nil
+	//fmt.Println("FTP OPEN FILE")
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.OpenFile(rewritePath, flag, perm)
 }
 
 func (a aofs) AllocateSpace(size int) error {
@@ -175,53 +114,70 @@ func (a aofs) AllocateSpace(size int) error {
 }
 
 func (a aofs) Remove(name string) error {
-	/*
-		rewritePath, _, err := a.pathRewrite(name)
-		if err != nil {
-			return err
-		}
-		if !a.checkAllowAccess(rewritePath, "write") {
-			return errors.New("Target is Read Only")
-		}
-
-		isHiddenFile, _ := hidden.IsHidden(rewritePath, true)
-		if isHiddenFile {
-			//Hidden files, include cache or trash
-			return errors.New("Access denied for hidden files")
-		}
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
 
-		log.Println(a.userinfo.Username + " removed " + rewritePath + " via FTP endpoint")
-		os.MkdirAll(filepath.Dir(rewritePath)+"/.metadata/.trash/", 0755)
-		os.Rename(rewritePath, filepath.Dir(rewritePath)+"/.metadata/.trash/"+filepath.Base(rewritePath)+"."+strconv.Itoa(int(time.Now().Unix())))
-	*/
-	return nil
+	return fsh.FileSystemAbstraction.Remove(rewritePath)
 }
 
 func (a aofs) RemoveAll(path string) error {
-	/*
-		rewritePath, _, err := a.pathRewrite(path)
+	fsh, rewritePath, err := a.pathRewrite(path)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.RemoveAll(rewritePath)
+}
+
+func (a aofs) Rename(oldname, newname string) error {
+	fshsrc, rewritePathsrc, err := a.pathRewrite(oldname)
+	if err != nil {
+		return err
+	}
+
+	fshdest, rewritePathdest, err := a.pathRewrite(newname)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fshsrc, rewritePathsrc, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	if !a.checkAllowAccess(fshdest, rewritePathdest, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+
+	if !fshdest.FileSystemAbstraction.FileExists(filepath.Dir(rewritePathdest)) {
+		fshdest.FileSystemAbstraction.MkdirAll(filepath.Dir(rewritePathdest), 0775)
+	}
+
+	if fshsrc.UUID == fshdest.UUID {
+		//Renaming in same fsh
+		return fshsrc.FileSystemAbstraction.Rename(rewritePathsrc, rewritePathdest)
+	} else {
+		//Cross fsh read write.
+		f, err := fshsrc.FileSystemAbstraction.ReadStream(rewritePathsrc)
 		if err != nil {
 			return err
 		}
-		//log.Println("RemoveAll", rewritePath)
-		isHiddenFile, _ := hidden.IsHidden(rewritePath, true)
-		if isHiddenFile {
-			//Hidden files, include cache or trash
-			return errors.New("Target is Read Only")
+
+		err = fshdest.FileSystemAbstraction.WriteStream(rewritePathdest, f, 0775)
+		if err != nil {
+			return err
 		}
-		if !a.checkAllowAccess(rewritePath, "write") {
-			return errors.New("Permission Denied")
+		f.Close()
+		err = fshsrc.FileSystemAbstraction.RemoveAll(rewritePathsrc)
+		if err != nil {
+			return err
 		}
-		os.MkdirAll(filepath.Dir(rewritePath)+"/.metadata/.trash/", 0755)
-		os.Rename(rewritePath, filepath.Dir(rewritePath)+"/.metadata/.trash/"+filepath.Base(rewritePath)+"."+strconv.Itoa(int(time.Now().Unix())))
-	*/
-	return nil
-}
-
-func (a aofs) Rename(oldname, newname string) error {
-
+	}
 	return nil
-
 }
 
 func (a aofs) Name() string {
@@ -229,13 +185,25 @@ func (a aofs) Name() string {
 }
 
 func (a aofs) Chmod(name string, mode os.FileMode) error {
-	//log.Println("Chmod", name, mode)
-	return nil
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chmod(rewritePath, mode)
 }
 
 func (a aofs) Chtimes(name string, atime time.Time, mtime time.Time) error {
-	//log.Println("Chtimes", name, atime, mtime)
-	return nil
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chtimes(rewritePath, atime, mtime)
 }
 
 //arozos adaptive functions
@@ -245,11 +213,11 @@ func (a aofs) pathRewrite(path string) (*filesystem.FileSystemHandler, string, e
 	//log.Println("Original path: ", path)
 	if path == "/" {
 		//Roots. Show ftpbuf root
-		fsHandlers := a.userinfo.GetAllFileSystemHandler()
-		for _, fsh := range fsHandlers {
+		fshs := a.userinfo.GetAllFileSystemHandler()
+		for _, fsh := range fshs {
 			//Create a folder representation for this virtual directory
-			if !(fsh.Hierarchy == "backup") {
-				os.Mkdir(a.tmpFolder+fsh.UUID, 0755)
+			if !fsh.RequireBuffer {
+				fsh.FileSystemAbstraction.Mkdir(filepath.Join(a.tmpFolder, fsh.UUID), 0755)
 			}
 		}
 
@@ -257,7 +225,7 @@ func (a aofs) pathRewrite(path string) (*filesystem.FileSystemHandler, string, e
 		if err != nil {
 			readmeContent = []byte("DO NOT UPLOAD FILES INTO THE ROOT DIRECTORY")
 		}
-		ioutil.WriteFile(a.tmpFolder+"README.txt", readmeContent, 0755)
+		ioutil.WriteFile(filepath.Join(a.tmpFolder, "README.txt"), readmeContent, 0755)
 
 		//Return the tmpFolder root
 		tmpfs, _ := a.userinfo.GetFileSystemHandlerFromVirtualPath("tmp:/")
@@ -273,23 +241,21 @@ func (a aofs) pathRewrite(path string) (*filesystem.FileSystemHandler, string, e
 		fsHandlerUUID := subpaths[0]
 		remainingPaths := subpaths[1:]
 
-		//Look for the fsHandler with this UUID
-		fsHandlers := a.userinfo.GetAllFileSystemHandler()
-		for _, fsh := range fsHandlers {
-			//Create a folder representation for this virtual directory
-			if fsh.UUID == fsHandlerUUID {
-				//This is the correct handler
-				if fsh.Hierarchy == "user" {
-					return fsh, filepath.ToSlash(filepath.Clean(fsh.Path)) + "/users/" + a.userinfo.Username + "/" + strings.Join(remainingPaths, "/"), nil
-				} else if fsh.Hierarchy == "public" {
-					return fsh, filepath.ToSlash(filepath.Clean(fsh.Path)) + "/" + strings.Join(remainingPaths, "/"), nil
-				}
+		fsh, err := a.userinfo.GetFileSystemHandlerFromVirtualPath(fsHandlerUUID + ":")
+		if err != nil {
+			return nil, "", errors.New("File System Abstraction not found")
+		}
 
-			}
+		if fsh.RequireBuffer {
+			//Not supported
+			return nil, "", errors.New("Buffered file system not supported by FTP driver")
 		}
 
-		//fsh not found.
-		return nil, "", errors.New("Path is READ ONLY")
+		rpath, err := fsh.FileSystemAbstraction.VirtualPathToRealPath(fsh.UUID+":/"+strings.Join(remainingPaths, "/"), a.userinfo.Username)
+		if err != nil {
+			return nil, "", errors.New("File System Handler Hierarchy not supported by FTP driver")
+		}
+		return fsh, rpath, nil
 	} else {
 		//fsh not found.
 		return nil, "", errors.New("Invalid path")
@@ -297,7 +263,17 @@ func (a aofs) pathRewrite(path string) (*filesystem.FileSystemHandler, string, e
 }
 
 //Check if user has access to the given path, mode can be string {read / write}
-func (a aofs) checkAllowAccess(fsh *filesystem.FileSystemHandler, path string, mode string) bool {
+func (a aofs) checkAllowAccess(fsh *filesystem.FileSystemHandler, path string, mode int) bool {
+	vpath, err := fsh.FileSystemAbstraction.RealPathToVirtualPath(path, a.userinfo.Username)
+	if err != nil {
+		return false
+	}
 
-	return true
+	if mode == aofsCanRead {
+		return a.userinfo.CanRead(vpath)
+	} else if mode == aofsCanWrite {
+		return a.userinfo.CanWrite(vpath)
+	} else {
+		return false
+	}
 }

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

@@ -67,9 +67,7 @@ func (m mainDriver) AuthUser(cc ftp.ClientContext, user string, pass string) (ft
 				accessOK = false
 				return nil, err
 			}
-		}
-
-		if !accessOK {
+		} else {
 			//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

+ 1 - 1
mod/user/directoryHandler.go

@@ -223,7 +223,7 @@ func (u *User) RealPathToVirtualPath(rpath string) (string, error) {
 		}
 		if pathContained == true {
 			//This storage is one of the root of the given realpath. Translate it into this
-			if storage.Closed == true {
+			if storage == true {
 				return "", errors.New("Request Filesystem Handler has been closed by another process")
 			}
 			return storage.UUID + ":/" + subPath, nil

+ 1 - 59
storage.go

@@ -64,24 +64,6 @@ func LoadBaseStoragePool() error {
 	}
 	fsHandlers = append(fsHandlers, baseHandler)
 
-	/*
-		//Load the special share folder as storage unit
-		shareHandler, err := fs.NewFileSystemHandler(fs.FileSystemOption{
-			Name:       "Share",
-			Uuid:       "share",
-			Path:       filepath.ToSlash(filepath.Clean(*tmp_directory)) + "/",
-			Access:     fsdef.FsReadOnly,
-			Hierarchy:  "share",
-			Automount:  false,
-			Filesystem: "share",
-		})
-
-		if err != nil {
-			log.Println("Failed to initiate share virtual storage directory: " + err.Error())
-			return err
-		}
-		fsHandlers = append(fsHandlers, shareHandler)
-	*/
 	//Load the tmp folder as storage unit
 	tmpHandler, err := fs.NewFileSystemHandler(fs.FileSystemOption{
 		Name:       "tmp",
@@ -98,46 +80,6 @@ func LoadBaseStoragePool() error {
 	}
 	fsHandlers = append(fsHandlers, tmpHandler)
 
-	/*
-
-		DEBUG REMOVE AFTERWARD
-
-	*/
-
-	webdh, err := fs.NewFileSystemHandler(fs.FileSystemOption{
-		Name:       "Loopback",
-		Uuid:       "loopback",
-		Path:       "http://192.168.1.214:8081/webdav/user",
-		Access:     fsdef.FsReadWrite,
-		Hierarchy:  "public",
-		Automount:  false,
-		Filesystem: "webdav",
-		Username:   "TC",
-		Password:   "password",
-	})
-	if err != nil {
-		log.Println(err.Error())
-	} else {
-		fsHandlers = append(fsHandlers, webdh)
-	}
-
-	webdh2, err := fs.NewFileSystemHandler(fs.FileSystemOption{
-		Name:       "CCNS",
-		Uuid:       "ccns",
-		Path:       "https://ccns.arozos.com:443/webdav/tmp",
-		Access:     fsdef.FsReadWrite,
-		Hierarchy:  "user",
-		Automount:  false,
-		Filesystem: "webdav",
-		Username:   "TC",
-		Password:   "fuckrub",
-	})
-	if err != nil {
-		log.Println(err.Error())
-	} else {
-		fsHandlers = append(fsHandlers, webdh2)
-	}
-
 	//Load all the storage config from file
 	rawConfig, err := ioutil.ReadFile(*storage_config_file)
 	if err != nil {
@@ -292,7 +234,7 @@ func GetFsHandlerByUUID(uuid string) (*fs.FileSystemHandler, error) {
 	}
 
 	for _, fsh := range fsHandlers {
-		if fsh.UUID == uuid {
+		if fsh.UUID == uuid && !fsh.Closed {
 			return fsh, nil
 		}
 	}

+ 104 - 48
storage.pool.go

@@ -43,7 +43,7 @@ func StoragePoolEditorInit() {
 
 	adminRouter.HandleFunc("/system/storage/pool/list", HandleListStoragePools)
 	adminRouter.HandleFunc("/system/storage/pool/listraw", HandleListStoragePoolsConfig)
-	adminRouter.HandleFunc("/system/storage/pool/newHandler", HandleStorageNewFsHandler)
+	//adminRouter.HandleFunc("/system/storage/pool/newHandler", HandleStorageNewFsHandler)
 	adminRouter.HandleFunc("/system/storage/pool/removeHandler", HandleStoragePoolRemove)
 	adminRouter.HandleFunc("/system/storage/pool/reload", HandleStoragePoolReload)
 	adminRouter.HandleFunc("/system/storage/pool/toggle", HandleFSHToggle)
@@ -54,21 +54,20 @@ func StoragePoolEditorInit() {
 
 //Handle editing of a given File System Handler
 func HandleFSHEdit(w http.ResponseWriter, r *http.Request) {
-	opr, _ := common.Mv(r, "opr", false)
-
-	uuid, err := common.Mv(r, "uuid", false)
-	if err != nil {
-		common.SendErrorResponse(w, "Invalid UUID")
-		return
-	}
-
-	group, err := common.Mv(r, "group", false)
+	opr, _ := common.Mv(r, "opr", true)
+	group, err := common.Mv(r, "group", true)
 	if err != nil {
 		common.SendErrorResponse(w, "Invalid group given")
 		return
 	}
 
 	if opr == "get" {
+		uuid, err := common.Mv(r, "uuid", true)
+		if err != nil {
+			common.SendErrorResponse(w, "Invalid UUID")
+			return
+		}
+
 		//Load
 		fshOption, err := getFSHConfigFromGroupAndUUID(group, uuid)
 		if err != nil {
@@ -84,19 +83,79 @@ func HandleFSHEdit(w http.ResponseWriter, r *http.Request) {
 		common.SendJSONResponse(w, string(js))
 		return
 	} else if opr == "set" {
-		//Set
-		newFsOption := buildOptionFromRequestForm(r)
-		//log.Println(newFsOption)
+		config, err := common.Mv(r, "config", true)
+		if err != nil {
+			common.SendErrorResponse(w, "Invalid UUID")
+			return
+		}
+
+		newFsOption, err := buildOptionFromRequestForm(config)
+		if err != nil {
+			common.SendErrorResponse(w, err.Error())
+			return
+		}
+		log.Println(newFsOption)
+
+		uuid := newFsOption.Uuid
 
 		//Read and remove the original settings from the config file
-		err := setFSHConfigByGroupAndId(group, uuid, newFsOption)
+		err = setFSHConfigByGroupAndId(group, uuid, newFsOption)
 		if err != nil {
-			errmsg, _ := json.Marshal(err.Error())
-			http.Redirect(w, r, "../../../SystemAO/storage/updateError.html#"+string(errmsg), 307)
+			common.SendErrorResponse(w, err.Error())
 		} else {
-			http.Redirect(w, r, "../../../SystemAO/storage/updateComplete.html#"+group, 307)
+			common.SendOK(w)
+		}
+	} else if opr == "new" {
+		//New handler
+		config, err := common.Mv(r, "config", true)
+		if err != nil {
+			common.SendErrorResponse(w, "Invalid config")
+			return
+		}
+		newFsOption, err := buildOptionFromRequestForm(config)
+		if err != nil {
+			common.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Check if group exists
+		if !permissionHandler.GroupExists(group) && group != "system" {
+			common.SendErrorResponse(w, "Group not exists: "+group)
+			return
+		}
+
+		//Validate the config is correct
+		err = fs.ValidateOption(&newFsOption)
+		if err != nil {
+			common.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		configFile := "./system/storage.json"
+		if group != "system" {
+			configFile = "./system/storage/" + group + ".json"
+		}
+
+		//Merge the old config file if exists
+		oldConfigs := []fs.FileSystemOption{}
+		if fs.FileExists(configFile) {
+			originalConfigFile, _ := ioutil.ReadFile(configFile)
+			err := json.Unmarshal(originalConfigFile, &oldConfigs)
+			if err != nil {
+				log.Println(err)
+			}
+		}
+
+		oldConfigs = append(oldConfigs, newFsOption)
+		js, _ := json.MarshalIndent(oldConfigs, "", " ")
+		err = ioutil.WriteFile(configFile, js, 0775)
+		if err != nil {
+			common.SendErrorResponse(w, err.Error())
+			return
 		}
 
+		common.SendOK(w)
+
 	} else {
 		//Unknown
 		common.SendErrorResponse(w, "Unknown opr given")
@@ -182,12 +241,23 @@ func setFSHConfigByGroupAndId(group string, uuid string, options fs.FileSystemOp
 
 	//Filter the old fs handler option with given uuid
 	newConfig := []fs.FileSystemOption{}
+	var overwritingConfig fs.FileSystemOption
 	for _, fso := range loadedConfig {
 		if fso.Uuid != uuid {
 			newConfig = append(newConfig, fso)
+		} else {
+			overwritingConfig = fso
 		}
 	}
 
+	//Continue using the old username and password if it is left empty
+	if options.Username == "" {
+		options.Username = overwritingConfig.Username
+	}
+	if options.Password == "" {
+		options.Password = overwritingConfig.Password
+	}
+
 	//Append the new fso to config
 	newConfig = append(newConfig, options)
 
@@ -246,20 +316,19 @@ func HandleFSHToggle(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if targetFSH.Closed == true {
+	if targetFSH.Closed {
 		//Reopen the fsh database and set this to false
 		aofsPath := filepath.ToSlash(filepath.Clean(targetFSH.Path)) + "/aofs.db"
 		conn, err := database.NewDatabase(aofsPath, false)
-		if err != nil {
-			common.SendErrorResponse(w, "Filesystme database startup failed")
-			return
+		if err == nil {
+			targetFSH.FilesystemDatabase = conn
 		}
-
-		targetFSH.FilesystemDatabase = conn
 		targetFSH.Closed = false
 	} else {
 		//Close the fsh database and set this to true
-		targetFSH.FilesystemDatabase.Close()
+		if targetFSH.FilesystemDatabase != nil {
+			targetFSH.FilesystemDatabase.Close()
+		}
 		targetFSH.Closed = true
 	}
 
@@ -447,32 +516,18 @@ func HandleStoragePoolRemove(w http.ResponseWriter, r *http.Request) {
 }
 
 //Constract a fsoption from form
-func buildOptionFromRequestForm(r *http.Request) fs.FileSystemOption {
-	r.ParseForm()
-	autoMount := (r.FormValue("automount") == "on")
-	newFsOption := fs.FileSystemOption{
-		Name:       r.FormValue("name"),
-		Uuid:       r.FormValue("uuid"),
-		Path:       r.FormValue("path"),
-		Access:     r.FormValue("access"),
-		Hierarchy:  r.FormValue("hierarchy"),
-		Automount:  autoMount,
-		Filesystem: r.FormValue("filesystem"),
-		Mountdev:   r.FormValue("mountdev"),
-		Mountpt:    r.FormValue("mountpt"),
-
-		Parentuid:  r.FormValue("parentuid"),
-		BackupMode: r.FormValue("backupmode"),
-
-		Username: r.FormValue("username"),
-		Password: r.FormValue("password"),
-	}
-
-	return newFsOption
+func buildOptionFromRequestForm(payload string) (fs.FileSystemOption, error) {
+	newFsOption := fs.FileSystemOption{}
+	err := json.Unmarshal([]byte(payload), &newFsOption)
+	if err != nil {
+		return fs.FileSystemOption{}, err
+	}
+	return newFsOption, nil
 }
 
+/*
 func HandleStorageNewFsHandler(w http.ResponseWriter, r *http.Request) {
-	newFsOption := buildOptionFromRequestForm(r)
+	newFsOption, _ := buildOptionFromRequestForm(r)
 
 	type errorObject struct {
 		Message string
@@ -522,7 +577,7 @@ func HandleStorageNewFsHandler(w http.ResponseWriter, r *http.Request) {
 	oldConfigs = append(oldConfigs, newFsOption)
 
 	//Prepare the content to be written
-	js, err := json.Marshal(oldConfigs)
+	js, _ := json.Marshal(oldConfigs)
 	resultingJson := pretty.Pretty(js)
 
 	err = ioutil.WriteFile(configFile, resultingJson, 0775)
@@ -538,6 +593,7 @@ func HandleStorageNewFsHandler(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0")
 	http.Redirect(w, r, "../../../SystemAO/storage/poolEditor.html#"+groupName, 307)
 }
+*/
 
 func HandleListStoragePoolsConfig(w http.ResponseWriter, r *http.Request) {
 	target, _ := common.Mv(r, "target", false)

+ 100 - 79
web/SystemAO/storage/fshedit.html

@@ -54,14 +54,14 @@
         <h24 class="ui inverted header">
             <i class="folder icon"></i>
             <div class="content">
-                Edit File System Handler
-            <div class="sub header">Edit the selected File System Handler (FSH)</div>
+                <span id="pagetitle">Edit File System Handler</span>
+            <div class="sub header" id="pageSubTitle">Edit the selected File System Handler (FSH)</div>
             </div>
         </h4>
     </div>
     <br>
     <div class="ui container">
-        <form id="mainForm" class="ui form" action="../../system/storage/pool/edit?opr=set" method="POST">
+        <form id="mainForm" class="ui form" onsubmit="handleFormSubmit(event);">
             <div class="field" style="display: none;">
                 <label>Group</label>
                 <input type="text" name="group" id="groupfield" readonly="true">
@@ -93,86 +93,63 @@
             <div class="field">
                 <label>Storage Hierarchy</label>
                 <div id="hierarchyfield" class="ui selection dropdown" onchange="handleHierarchyChange(this);">
-                <input type="hidden" name="hierarchy" value="user">
+                <input type="hidden" name="hierarchy" value="public">
                 <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>
-            <p class="backuponly">Backup Settings</p>
-            <div class="field backuponly">
-                <label>Backup Virtual Disk UID</label>
-                <div class="ui selection dropdown disabled">
-                    <input type="hidden" autocomplete="false" name="parentuid" value="" readonly="true">
-                    <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 id="backupmode" 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="basic">Basic</div>
-                        <div class="item" data-value="nightly">Nightly</div>
-                        <div class="item" data-value="version">Versioning</div>
-                    </div>
-                </div>
-                </div>
             <div class="ui divider"></div>
             <p>Physical Disks Settings</p>
             <div class="field">
                 <label>Filesystem Type</label>
                 <div id="fstype" class="ui selection dropdown">
-                <input type="hidden" name="filesystem" value="ntfs">
+                <input type="hidden" name="filesystem" value="ntfs" onchange="handleFileSystemTypeChange(this.value);">
                 <i class="dropdown icon"></i>
                 <div class="default text">Filesystem Type</div>
                 <div class="menu">
                     <div class="item" data-value="ext4">EXT4</div>
-                    <div class="item" data-value="ext3">EXT3</div>
+                    <!-- <div class="item" data-value="ext3">EXT3</div> -->
                     <div class="item" data-value="ntfs">NTFS</div>
                     <div class="item" data-value="vfat">VFAT</div>
                     <div class="item" data-value="fat">FAT</div>
+                    <div class="item" data-value="webdav">WebDAV</div>
                 </div>
                 </div>
             </div>
-            <div class="field">
-                <label>Mount Device</label>
-                <input type="text" name="mountdev" placeholder="e.g. /dev/sda1">
-            </div>
-            <div class="field">
-                <label>Mount Point</label>
-                <input type="text" name="mountpt" placeholder="e.g. /media/myfolder">
-            </div>
-
-            <div class="field">
-                <div class="ui checkbox">
-                <input type="checkbox" name="automount" tabindex="0" class="hidden">
-                <label>Automount</label>
+            <div class="localfs">
+                <div class="field">
+                    <label>Mount Device</label>
+                    <input type="text" name="mountdev" placeholder="e.g. /dev/sda1">
                 </div>
+                <div class="field">
+                    <label>Mount Point</label>
+                    <input type="text" name="mountpt" placeholder="e.g. /media/myfolder">
+                </div>
+                <div class="field">
+                    <div class="ui checkbox">
+                    <input type="checkbox" id="automount" tabindex="0" class="hidden">
+                    <label>Automount</label>
+                    </div>
+                </div>
+                <br>
             </div>
-            <br>
-            
-            <div class="ui divider"></div>
-            <p>Security Related (if any)</p>
-            <div class="field">
-                <label>Username</label>
-                <input type="text" name="username" placeholder="">
-            </div>
-            <div class="field">
-                <label>Password</label>
-                <input type="password" name="password" placeholder="">
+            <div class="networkfs" style="display:none;">
+                <div class="ui divider"></div>
+                <p>Security and Authentication</p>
+                <div class="field">
+                    <label>Username</label>
+                    <input type="text" name="username" placeholder="">
+                </div>
+                <div class="field">
+                    <label>Password</label>
+                    <input type="password" name="password" placeholder="">
+                </div>
+                <br>
             </div>
-
             <button class="ui right floated button" onclick='handleCancel();'>Cancel</button>
             <button class="ui green right floated button" type="submit">Update</button>
             <br><br><br><br>
@@ -181,6 +158,7 @@
     <script>
         //Get target fsh uuid and group from hash
         var targetFSH = "";
+        var opr = "set";
         $(".ui.dropdown").dropdown();
         $(".ui.checkbox").checkbox();
 
@@ -194,13 +172,41 @@
                             $("#backupIdList").append(`<div class="item" data-value="${storage.UUID}">${storage.Name} (${storage.UUID}:/)</div>`);
                         });
                     }
-                    
                 });
                 $("#backupIdList").parent().dropdown();
                 renderFSHCurrentSettings();
+            });  
+        }
+
+        function handleFormSubmit(e){
+            e.preventDefault();
+            //Get the form value
+            let payload = new FormData(e.target);
+            let fshObject = {};
+            [...payload.entries()].forEach(function(field){
+                fshObject[field[0]] = field[1];
             });
 
-            
+            //Inject other payloads
+            fshObject.automount = $("#automount")[0].checked;
+            $.ajax({
+                url: "../../system/storage/pool/edit",
+                method: "POST",
+                data: {
+                    opr: opr,
+                    group: $("#groupfield").val(),
+                    config: JSON.stringify(fshObject),
+                },
+                success: function(data){
+                    if (data.error !== undefined){
+                        alert(data.error);
+                    }else{
+                        //Done
+                        window.location.href = "updateComplete.html";
+                    }
+                }
+
+            });
         }
 
         function renderFSHCurrentSettings(){
@@ -208,16 +214,23 @@
             var input = JSON.parse(decodeURIComponent(window.location.hash.substr(1)));
             $("#groupfield").val(input.group);
 
-            $.ajax({
-                url: "../../system/storage/pool/edit",
-                method: "GET",
-                data: {opr: "get", uuid: input.uuid, group: input.group},
-                success: function(data){
-                    renderOptionsToForm(data);
-                }
-            });
+            if (input.uuid == undefined){
+                 //New fsh
+                $("#pagetitle").text("New File System Handler");
+                $("#pageSubTitle").text("Mount a new file system into this host as storage");
+                opr = "new";
+            }else{
+                $.ajax({
+                    url: "../../system/storage/pool/edit",
+                    method: "GET",
+                    data: {opr: "get", uuid: input.uuid, group: input.group},
+                    success: function(data){
+                        renderOptionsToForm(data);
+                    }
+                });
 
-            $("#mainForm").attr("action", "../../system/storage/pool/edit?opr=set&uuid=" + input.uuid + "&group=" + input.group);
+                $("#mainForm").attr("action", "../../system/storage/pool/edit?opr=set&uuid=" + input.uuid + "&group=" + input.group);
+            }
         }
 
         function handleHierarchyChange(object){
@@ -231,6 +244,16 @@
             }
         }
 
+        function handleFileSystemTypeChange(fstype){
+            if (fstype == "webdav" || fstype == "ftp"){
+                $(".localfs").hide();
+                $(".networkfs").show();
+            }else{
+                $(".localfs").show();
+                $(".networkfs").hide();
+            }
+        }
+
         function renderOptionsToForm(option){
             console.log(option);
             $("input[name=name]").val(option.name);
@@ -238,17 +261,9 @@
             $("input[name=path]").val(option.path);
             $("#accessfield").dropdown("set selected", option.access);
             $("#hierarchyfield").dropdown("set selected", option.hierarchy);
-            if (option.hierarchy == "backup"){
-                //Show backup drive options
-                $(".backuponly").slideDown("fast");
-                //Set backup mode
-                $("input[name=backupmode]").dropdown("set selected",option.backupmode);
-                $("#backupIdList").parent().dropdown();
-                //Set parent id and backup mode from dropdown
-                $("#backupIdList").parent().dropdown("set selected",option.parentuid);
-                $("#backupmode").parent().dropdown("set selected",option.backupmode);
-                //Disable the readonly settings
-                $("#accessfield").addClass("disabled");
+            if (isNetworkFs(option.filesystem)){
+                $(".localfs").hide();
+                $(".networkfs").show();
             }
             $("#fstype").dropdown("set selected",option.filesystem);
             $("input[name=mountdev]").val(option.mountdev);
@@ -258,6 +273,12 @@
             }
         }
 
+        function isNetworkFs(name){
+            if (name == "webdav" || name == "ftp"){
+                return true;
+            }
+            return false;
+        }
 
         function handleCancel(){
             ao_module_parentCallback(true);

+ 306 - 136
web/SystemAO/storage/poolList.html

@@ -15,6 +15,42 @@
             height:1em;
             width:1em;
         }
+        .clickable{
+            cursor: pointer;
+            transition: opacity 0.1s ease-in-out;
+        }
+        .clickable:hover{
+            opacity: 0.6;
+        }
+
+        .reloadpoolBtn{
+            position: absolute;
+            right: 0.4em;
+            top: 0.8em;
+        }
+
+        .statusTextWrapper{
+            text-align: right;
+            width: 100%;
+            overflow: hidden;
+            position: absolute;
+            bottom: 0px;
+            right: 0px;
+            height: 3rem;
+            display: flex;
+            justify-content: end;
+        }
+        .statusText{
+            opacity: 0.1;
+            font-weight: lighter;
+            font-size: 2em;
+            align-self:flex-end;
+            margin-bottom: 0.2em;
+            margin-right: 0.2em;
+            pointer-events: none;
+            user-select: none;
+        }
+        
     </style>
 </head>
 <body>
@@ -25,60 +61,50 @@
                 <div class="sub header">Manage system and permission group storage pools</div>
             </h3>
         </div>
+        <div id="ok" class="ui basic modal">
+            <div class="ui icon header">
+                <i class="green checkmark icon"></i>
+                Storage Pool Reloaded
+            </div>
+            <div class="content" align="center">
+                <p>Storage Pool has successfully reloaded.</p>
+            </div>
+            <div class="actions">
+                <div class="ui green ok inverted button">
+                <i class="checkmark icon"></i>
+                OK
+                </div>
+            </div>
+        </div>
         <div class="ui stackable grid">
-            <div class="six wide column">
-                <p>Select a Storage Pool to edit</p>
-                <select id="poolNameList" class="ui search fluid dropdown" onchange="updatePoolSelection(this.value)">
-                    <option value="">Storage Pool</option>
-                    
-                </select>
-                <div class="ui divider"></div>
-                <div id="poolStorageList">
+            <div class="six wide column" style="border-right: 1px solid #e0e0e0;">
+                <div id="poolNameList">
 
                 </div>
-                <button class="ui green fluid disabled button selectedOnly" onclick="editSelectedPool();"><i class="edit icon"></i> Edit Storage Pool</button>
                 <div class="ui divider"></div>
-                <div class="ui red segment">
-                    <h4>Danger Zone</h4>
-                    <p>The following buttons will unmount storage pool and remount them from configuration file.</p>
-                    <button class="ui red basic fluid disabled button selectedOnly" style="margin-top: 8px;" onclick="reloadSelectedStoragePool();"><i class="refresh icon"></i> Reload Selected Pool</button>
-                    <button class="ui red basic fluid button" style="margin-top: 8px;" onclick="reloadAllStoragePool();"><i class="refresh icon"></i> Reload All Storage Pool</button>
+                <div style="width: 100%;" align="center">
+                    <button title="Reload All Storage Pools" onclick="reloadAllStoragePool();" class="circular basic red large ui icon button">
+                        <i class="red refresh icon"></i>
+                    </button>
                 </div>
-               
             </div>
             <div class="ten wide column">
-                <p>Current Storage Pool Structure</p>
-                <div id="poolList" class="ui list">
-                    <div class="item">
-                        <i class="spinner icon"></i>
-                        <div class="content">
-                        <div class="header">Loading</div>
-                        <div class="description"></div>
-                        </div>
-                    </div>
+                <div id="disklist">
+
+                </div>
+                <div class="ui divider"></div>
+                <div style="width: 100%;" align="center">
+                    <button onclick="newFsh();" title="Add Storage" class="circular basic large ui icon button">
+                        <i class="add icon"></i>
+                    </button>
+                    <button title="Bridge Storage" class="circular basic blue large ui icon button">
+                        <i class="linkify icon"></i>
+                    </button>
                 </div>
             </div>
         </div>
-       
-        
-    </div>
-    <div id="ok" class="ui basic modal">
-        <div class="ui icon header">
-            <i class="green checkmark icon"></i>
-            Storage Pool Reloaded
-        </div>
-        <div class="content" align="center">
-            <p>Storage Pool has successfully reloaded.</p>
-        </div>
-        <div class="actions">
-            <div class="ui green ok inverted button">
-            <i class="checkmark icon"></i>
-            OK
-            </div>
-        </div>
-    </div>
     <script>
-        var selectedStoragePool = "";
+        var selectedStoragePool = "system";
         var storagePoolList = [];
         //Create all elements
         $(".ui.dropdown").dropdown();
@@ -92,99 +118,173 @@
                 if (data.error !== undefined){
                     $("#poolList").html("<p>" + data.error + "</p>");
                 }else{
-                    $("#poolNameList").html(`<option value="">Storage Pools</option>`);
+                    $("#poolNameList").html(``);
                     storagePoolList = data;
                     data.forEach(storagePool => {
-                        //Add Options
-                        $("#poolNameList").append(`<option value="${storagePool.Owner}">${storagePool.Owner}</option>`);
+                       $("#poolNameList").append(` <div class="ui clickable segment" onclick="loadThisPoolDetails('${storagePool.Owner}');">
+                        <h4 class="ui header storagepool">
+                            <img src="../../img/system/cluster.svg">
+                            <div class="content">
+                            ${storagePool.Owner}
+                                <div class="sub header">
+                                    Other's Permission: ${storagePool.OtherPermission}
+                                </div>
+                            </div>
+                        </h4>
+                        <button title="Reload Storage Pool" onclick="reloadStoagePool('${storagePool.Owner}', event);" class="circular tiny basic ui icon button reloadpoolBtn">
+                            <i class="green refresh icon"></i>
+                        </button>
+                        </div>`);
+                    });
+                }
 
-                        //Render the pool diagram
-                        let desc = "";
-                        if (storagePool.Owner == "system"){
-                            desc = " (Base Pool)"
-                        }
-                        let spHTML = `<div class="item">
-                        <i class="server icon"></i>
+                loadThisPoolDetails(selectedStoragePool);
+            }); 
+        }
+
+        function loadThisPoolDetails(owner){
+            selectedStoragePool = owner;
+            var targetStoragePool = undefined;
+            for (var i = 0; i < storagePoolList.length; i++){
+                let thisStoragePool = storagePoolList[i];
+                if (thisStoragePool.Owner == owner){
+                    targetStoragePool = thisStoragePool;
+                    break;
+                }
+            }
+
+            $("#disklist").html("");
+            var storages = targetStoragePool.Storages;
+            storages.forEach(function(storage){
+                var color = "#8adb9d";
+                var isReserved = false;
+                var driveIcon = "drive-virtual.svg";
+                var statusText = "Online";
+                var statusClass = ""
+                if (storage.UUID == "tmp" || storage.UUID == "user"){
+                    isReserved = true;
+                    color = "#fade8e";
+                    statusClass = "online"
+                }else if (storage.Closed == true){
+                    color = "#7d7d7d";
+                    statusText = "Stopped"
+                    statusClass = "stopped"
+                }
+
+                if (storage.Hierarchy == "public"){
+                    driveIcon = "drive-public.svg";
+                }else if (isReserved){
+                    driveIcon = "drive.svg";
+                }
+                var editButtons = `<button onclick="editFsh('${storage.UUID}','${owner}');" title="Edit Virtual Disk" onclick="" class="circular tiny basic ui icon button">
+                    <i class="edit icon"></i>
+                </button>
+                <button onclick="toggleFsh('${storage.UUID}','${owner}');" title="Toggle Virtual Disk IO" onclick="" class="circular tiny basic ui icon button shutdownButton">
+                    <i class="power icon"></i>
+                </button>
+                <button onclick="removeFsh('${storage.UUID}','${owner}');" title="Remove Virtual Disk" onclick="" class="circular tiny basic ui icon button removeFshButton">
+                    <i class="red remove icon"></i>
+                </button>`;
+                $("#disklist").append(`<div class="ui vdisk segment ${statusClass}" uuid="${storage.UUID}" style="border-left: 3px solid ${color};">
+                    <h4 class="ui header storagepool">
+                        <img src="../../img/system/${driveIcon}">
                         <div class="content">
-                        <div class="header">${storagePool.Owner + desc}</div>
-                        <div class="description">Other's Permission: ${storagePool.OtherPermission}</div>
-                        <div class="list">`;
+                            ${storage.Name} (${storage.UUID}:/)
+                            <div class="sub header" style="margin-top: 0.4em;">
+                                <div class="ui list">
+                                    <div class="item">Mount Point: ${storage.Path}</div>
+                                    <div class="item">Hierarchy: ${storage.Hierarchy}</div>
+                                    <div class="item">ReadOnly: ${storage.ReadOnly}</div>
+                                </div>
+                            </div>
+                        </div>
+                    </h4>
+                    <div class="fshbuttons" style="position: absolute; top: 0.8em; right: 0.8em;">
+                        ${isReserved?`<span style="color: #c9c9c9; pointer-events: none; user-select: none;">System Reserved</span>`:editButtons}
+                    </div>
+                    <div class="statusTextWrapper">
+                        <div class="statusText">
+                            ${statusText}
+                        </div>
+                    </div>
+                    
+                </div>`);
+            });
 
-                        if (storagePool.Storages !== null && storagePool.Storages.length > 0){
-                            storagePool.Storages.forEach(fsh => {
-                                var diskIcon = `grey disk icon`;
-                                if (fsh.Hierarchy == "user"){
-                                    diskIcon = `blue disk icon`;
-                                }else if (fsh.Hierarchy == "public"){
-                                    diskIcon = `grey disk icon`
-                                }
+            if (storages.length == 0){
+                $("#disklist").append(`<div class="ui segment nostorage" style="border-left: 3px solid #303030;">
+                    <h4 class="ui header storagepool">
+                        <img src="../../img/system/drive-notfound.svg">
+                        <div class="content">
+                            No Storage
+                            <div class="sub header" style="margin-top: 0.4em;">
+                                No storage found under this Storage Pool
+                            </div>
+                        </div>
+                    </h4>
+                `);
+            }
 
-                                var canwriteIcon = `<i class="ui red remove icon" style="margin-left: 0.1em; font-size: 0.9em;"></i>`;
-                                if (!fsh.ReadOnly){
-                                    canwriteIcon = `<i class="ui green checkmark icon" style="margin-left: 0.1em; font-size: 0.9em;"></i>`;
-                                }
+            $.get("../../system/storage/pool/listraw?target=" + owner, function(data){
+                if (data.error == undefined){
+                    data.forEach(function(storage){
+                        console.log(storage);
+                        let thisStorageUUID = storage.uuid;
+                        let matchingDiskFound = false;
+                        $(".vdisk").each(function(){
+                            if ($(this).attr("uuid") == thisStorageUUID && !$(this).hasClass("stopped")){
+                                $(this).addClass("online");
+                                $(this).find(".statusText").text("Online");
+                                matchingDiskFound = true;
+                            }else if ($(this).hasClass("stopped")){
+                                //Found but it has been shutted
+                                matchingDiskFound = true;
+                            }
+                        });
 
-                                spHTML += (`<div class="item">
-                                <i class="${diskIcon}"></i>
-                                <div class="content">
-                                    <div class="header">${fsh.Name} (${fsh.UUID}:/)</div>
-                                    <div class="description">
-                                    <div class="ui breadcrumb">
-                                        Mount Point: ${fsh.Path}
-                                        <span class="divider">|</span>
-                                        Hierarchy: ${fsh.Hierarchy}
-                                        <span class="divider">|</span>
-                                        ReadWrite: ${canwriteIcon}
+                        if (!matchingDiskFound){
+                            //Add offline disk record to the list
+                            $("#disklist").append(`<div class="ui vdisk segment offline" uuid="${storage.uuid}" style="border-left: 3px solid #3e3a39;">
+                                <h4 class="ui header storagepool">
+                                    <img src="../../img/system/drive-notfound.svg">
+                                    <div class="content">
+                                        ${storage.name} (${storage.uuid}:/)
+                                        <div class="sub header" style="margin-top: 0.4em;">
+                                            <div class="ui list">
+                                                <div class="item">Mount Point: ${storage.path}</div>
+                                                <div class="item">Hierarchy: ${storage.hierarchy}</div>
+                                                <div class="item">ReadOnly: ${storage.access=="readonly"}</div>
+                                            </div>
+                                        </div>
                                     </div>
+                                </h4>
+                                <div class="fshbuttons" style="position: absolute; top: 0.8em; right: 0.8em;">
+                                    <button title="Edit Virtual Disk" onclick="editFsh('${storage.uuid}','${selectedStoragePool}');" class="circular tiny basic ui icon button">
+                                        <i class="edit icon"></i>
+                                    </button>
+                                </div>
+                                <div class="statusTextWrapper">
+                                    <div class="statusText">
+                                        Offline
                                     </div>
                                 </div>
-                                </div>`);
-                            });
-                        }else{
-                            spHTML += `<div class="item">
-                                <i class="remove icon"></i>
-                                <div class="content">No storage found under this Storage Pool</div>`;
+                            </div>`);
                         }
-                            
-                        spHTML += (`
-                            </div>
-                            </div>
-                        </div>`);
-
-                        $("#poolList").append(spHTML);
                     });
-                }
-            }); 
-        }
 
-        function updatePoolSelection(value){
-            $("#poolStorageList").html("");
-            $.get("../../system/storage/pool/list", function(data){
-                if (data.error !== undefined){
-                    $("#poolList").html("<p>" + data.error + "</p>");
-                }else{
-                    //List the fshandlers inside this storage pool
-                    var targetStoragePool = undefined;
-                    storagePoolList = data;
-                    data.forEach(sp => {
-                        if (sp.Owner == value){
-                            targetStoragePool = sp;
+                    $('.vdisk').each(function(){
+                        if (!$(this).hasClass("online") && !$(this).hasClass("offline") && !$(this).hasClass("stopped")){
+                            $(this).find(".fshbuttons").hide();
+                            $(this).css({
+                                "border-left": "3px dotted #ff6666",
+                            });
+                            $(this).find(".statusText").text("Removed");
                         }
                     });
+                }
 
-                    if (targetStoragePool == undefined){
-                        $("#poolStorageList").html(`
-                            <div class="ui red inverted segment">
-                                <i class="remove icon"></i>Selected Storage Pool Not Found
-                            </div>
-                        `);
-                        $(".selectedOnly").addClass('disabled');
-                        return
-                    }
-                    
-                    selectedStoragePool = value;
-                    $(".selectedOnly").removeClass('disabled');
-
+                if ($(".vdisk").length > 0 && $(".nostorage").length > 0){
+                    $(".nostorage").remove();
                 }
             });
         }
@@ -230,25 +330,95 @@
             }
         }
 
+        function editFsh(uuid, gpname){
+            if (typeof(ao_module_newfw) == "undefined"){
+                window.open("../../SystemAO/storage/fshedit.html#" + encodeURIComponent(JSON.stringify({uuid: uuid, group: gpname})));
+                return
+            }
+            ao_module_newfw({
+                url:"SystemAO/storage/fshedit.html#" + encodeURIComponent(JSON.stringify({uuid: uuid, group: gpname})),
+                width: 530,
+                height: 740,
+                appicon: "SystemAO/storage/img/fsh.png",
+                title: "Edit File System Handler",
+                callback: "finishFshEdit",
+                parent: ao_module_windowID
+            });
+        }
 
-        function reloadSelectedStoragePool(){
-            if (selectedStoragePool != ""){
-                if (confirm("Confirm reloading " + selectedStoragePool + " storage pool?")){
-                    //OK. Reload storage pool for this pool
-                    $.ajax({
-                        url: "../../system/storage/pool/reload",
-                        data: {pool: selectedStoragePool},
-                        success: function(data){
-                            console.log(data);
-                            loadStoragePoolList();
-                            $("#ok").modal("show");
-                        },
-                        error: function(){
+        function newFsh(){
+            if (typeof(ao_module_newfw) == "undefined"){
+                window.open("../../SystemAO/storage/fshedit.html#" + encodeURIComponent(JSON.stringify({group: selectedStoragePool})));
+                return
+            }
+            ao_module_newfw({
+                url:"SystemAO/storage/fshedit.html#" + encodeURIComponent(JSON.stringify({group: selectedStoragePool})),
+                width: 530,
+                height: 740,
+                appicon: "SystemAO/storage/img/fsh.png",
+                title: "New File System Handler",
+                callback: "finishFshEdit",
+                parent: ao_module_windowID
+            });
+        }
+
+
+        function reloadStoagePool(owner, event){
+            event.preventDefault();
+            event.stopImmediatePropagation();
+            if (confirm("Confirm reloading " + owner + " storage pool?")){
+                //OK. Reload storage pool for this pool
+                $.ajax({
+                    url: "../../system/storage/pool/reload",
+                    data: {pool: owner},
+                    success: function(data){
+                        console.log(data);
+                        loadStoragePoolList();
+                        $("#ok").modal("show");
+                    },
+                    error: function(){
+                        loadStoragePoolList();
+                    }
+                });
+            }
+        }
+
+        function toggleFsh(uuid, gpname){
+            $.ajax({
+                url: "../../system/storage/pool/toggle",
+                data: {"fsh":uuid, "group": gpname},
+                success: function(data){
+                    if (data.error !== undefined){
+                        alert(data.error);
+                    }else{
+                        loadStoragePoolList();
+                    }
+
+                }
+            })
+        }
+
+
+        function removeFsh(uuid, group){
+            if (confirm("Confirm removing FSH: " + uuid + ":/ ?")){
+                $.ajax({
+                    url: "../../system/storage/pool/removeHandler",
+                    data: {uuid: uuid, group: group},
+                    success: function(data){
+                        if (data.error !== undefined){
+                            alert(data.error);
+                        }else{
+                            //Remove succeed
                             loadStoragePoolList();
                         }
-                    });
-                }
-            }   
+                    }
+                });
+            }
+           
+        }
+
+        window.finishFshEdit = function(){
+            loadStoragePoolList();
         }
     </script>
 </body>

Plik diff jest za duży
+ 27 - 0
web/img/system/drive-notfound.ai


BIN
web/img/system/drive-notfound.png


+ 35 - 0
web/img/system/drive-notfound.svg

@@ -0,0 +1,35 @@
+<?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" 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">
+<g id="圖層_1">
+	<rect x="12.458" y="81.333" fill="#727171" width="102.833" height="29.167"/>
+	<polygon fill="#DCDDDD" points="96.374,40.167 30.208,40.167 12.542,81.333 115.374,81.333 	"/>
+	<path fill="#3E3A39" d="M113.125,105.811c0,1.728-1.358,3.127-3.034,3.127H17.535c-1.676,0-3.035-1.399-3.035-3.127V86.002
+		c0-1.728,1.359-3.127,3.035-3.127h92.556c1.676,0,3.034,1.399,3.034,3.127V105.811z"/>
+	<circle fill="#00A0E9" cx="100.375" cy="95.916" r="3.708"/>
+</g>
+<g id="圖層_2">
+</g>
+<g id="圖層_3">
+	<circle fill="#3E3A39" cx="94.833" cy="36" r="25.5"/>
+	<g>
+		<path fill="#F7F8F8" d="M91.648,40.194v-1.445c0-0.664,0.068-1.266,0.205-1.807c0.137-0.54,0.346-1.055,0.625-1.543
+			c0.28-0.488,0.642-0.963,1.084-1.426c0.443-0.462,0.977-0.94,1.602-1.436c0.547-0.43,1.01-0.813,1.387-1.152
+			c0.378-0.338,0.684-0.674,0.918-1.006s0.404-0.68,0.508-1.045c0.104-0.364,0.156-0.788,0.156-1.27c0-0.742-0.25-1.354-0.752-1.836
+			c-0.501-0.481-1.266-0.723-2.295-0.723c-0.898,0-1.865,0.189-2.9,0.566c-1.035,0.378-2.105,0.84-3.213,1.387l-1.992-4.316
+			c0.561-0.325,1.169-0.635,1.826-0.928c0.658-0.293,1.342-0.553,2.051-0.781c0.71-0.228,1.43-0.407,2.158-0.537
+			c0.729-0.13,1.445-0.195,2.148-0.195c1.328,0,2.526,0.16,3.594,0.479c1.068,0.319,1.973,0.785,2.715,1.396
+			c0.742,0.612,1.313,1.354,1.709,2.227c0.398,0.873,0.596,1.869,0.596,2.988c0,0.82-0.09,1.553-0.273,2.197
+			c-0.182,0.645-0.452,1.244-0.811,1.797c-0.357,0.554-0.807,1.087-1.348,1.602c-0.54,0.515-1.168,1.058-1.885,1.631
+			c-0.547,0.43-0.992,0.804-1.338,1.123c-0.345,0.319-0.615,0.622-0.811,0.908c-0.195,0.287-0.328,0.583-0.4,0.889
+			c-0.071,0.306-0.107,0.667-0.107,1.084v1.172H91.648z M91.004,46.874c0-0.612,0.088-1.129,0.264-1.553
+			c0.176-0.423,0.42-0.765,0.732-1.025c0.313-0.26,0.681-0.449,1.104-0.566c0.424-0.117,0.876-0.176,1.357-0.176
+			c0.456,0,0.889,0.059,1.299,0.176s0.771,0.306,1.084,0.566c0.313,0.261,0.561,0.603,0.742,1.025
+			c0.183,0.423,0.273,0.941,0.273,1.553c0,0.586-0.091,1.087-0.273,1.504c-0.182,0.417-0.43,0.762-0.742,1.035
+			s-0.674,0.472-1.084,0.596c-0.41,0.124-0.843,0.186-1.299,0.186c-0.481,0-0.934-0.062-1.357-0.186
+			c-0.423-0.124-0.791-0.322-1.104-0.596s-0.557-0.618-0.732-1.035C91.092,47.961,91.004,47.459,91.004,46.874z"/>
+	</g>
+</g>
+</svg>

Plik diff jest za duży
+ 27 - 0
web/img/system/drive-public.ai


+ 25 - 0
web/img/system/drive-public.svg

@@ -0,0 +1,25 @@
+<?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" 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">
+<g id="圖層_1">
+	<rect x="11.458" y="82.333" fill="#717071" width="102.833" height="29.167"/>
+	<polygon fill="#DBDCDC" points="95.374,41.167 29.208,41.167 11.542,82.333 114.374,82.333 	"/>
+	<path fill="#3E3A39" d="M112.125,106.811c0,1.728-1.358,3.127-3.034,3.127H16.535c-1.676,0-3.035-1.399-3.035-3.127V87.002
+		c0-1.728,1.359-3.127,3.035-3.127h92.556c1.676,0,3.034,1.399,3.034,3.127V106.811z"/>
+	<circle fill="#009FE8" cx="99.375" cy="96.916" r="3.708"/>
+</g>
+<g id="圖層_2">
+	<path fill="#00A0E9" d="M122.084,60.654c0,4.288-3.476,7.764-7.763,7.764H77.014c-4.288,0-7.763-3.477-7.763-7.764V23.347
+		c0-4.287,3.475-7.763,7.763-7.763h37.308c4.287,0,7.763,3.476,7.763,7.763V60.654z"/>
+	<circle fill="#FFFFFF" cx="85.547" cy="33.868" r="6.893"/>
+	<path fill="#FFFFFF" d="M85.167,56.026c5.583,0,10.104-0.762,10.104-1.706c0-7.271-3.032-13.154-6.776-13.154
+		c0,1.119-1.371,2.024-3.069,2.024c-1.615,0-2.921-0.905-2.921-2.024c-3.69,0-6.679,5.883-6.679,13.154
+		c0,0.944,4.211,1.706,9.412,1.706"/>
+	<circle fill="#FFFFFF" cx="106.66" cy="33.868" r="6.893"/>
+	<path fill="#FFFFFF" d="M106.281,56.026c5.582,0,10.103-0.763,10.103-1.706c0-7.271-3.032-13.154-6.776-13.154
+		c0,1.119-1.371,2.024-3.069,2.024c-1.614,0-2.921-0.905-2.921-2.024c-3.69,0-6.679,5.883-6.679,13.154
+		c0,0.943,4.211,1.706,9.412,1.706"/>
+</g>
+</svg>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików