Ver Fonte

Fixed glob on webdav, done share module

Toby Chui há 3 anos atrás
pai
commit
7bda5a8841

+ 10 - 19
file_system.go

@@ -272,13 +272,13 @@ func system_fs_handleFileSearch(w http.ResponseWriter, r *http.Request) {
 		//Wildcard
 
 		//Updates 31-12-2021: Do not allow wildcard search on virtual type's FSH
-		if targetFSH != nil && targetFSH.IsVirtual() {
-			common.SendErrorResponse(w, "This virtual storage device do not allow wildcard search")
+		if targetFSH == nil {
+			common.SendErrorResponse(w, "Invalid path given")
 			return
 		}
-
+		targetFshAbs := targetFSH.FileSystemAbstraction
 		wildcard := keyword[1:]
-		matchingFiles, err := filepath.Glob(filepath.Join(rpath, wildcard))
+		matchingFiles, err := targetFshAbs.Glob(filepath.Join(rpath, wildcard))
 		if err != nil {
 			common.SendErrorResponse(w, err.Error())
 			return
@@ -287,19 +287,13 @@ func system_fs_handleFileSearch(w http.ResponseWriter, r *http.Request) {
 		//Prepare result struct
 		results := []fs.FileData{}
 
-		//Process the matching files. Do not allow directory escape
-		srcAbs, _ := filepath.Abs(rpath)
-		srcAbs = filepath.ToSlash(srcAbs)
 		escaped := false
 		for _, matchedFile := range matchingFiles {
-			absMatch, _ := filepath.Abs(matchedFile)
-			absMatch = filepath.ToSlash(absMatch)
-			if !strings.Contains(absMatch, srcAbs) {
-				escaped = true
-			}
-
 			thisVpath, _ := targetFSH.FileSystemAbstraction.RealPathToVirtualPath(matchedFile, userinfo.Username)
-			results = append(results, fs.GetFileDataFromPath(targetFSH, thisVpath, matchedFile, 2))
+			isHidden, _ := hidden.IsHidden(thisVpath, true)
+			if !isHidden {
+				results = append(results, fs.GetFileDataFromPath(targetFSH, thisVpath, matchedFile, 2))
+			}
 
 		}
 
@@ -880,10 +874,7 @@ func system_fs_WebSocketScanTrashBin(w http.ResponseWriter, r *http.Request) {
 			//Skip this fsh
 			continue
 		}
-
-		if !storage.IsVirtual() {
-			scanningRoots = append(scanningRoots, storage)
-		}
+		scanningRoots = append(scanningRoots, storage)
 	}
 
 	for _, fsh := range scanningRoots {
@@ -2816,7 +2807,7 @@ func system_fs_FileVersionHistory(w http.ResponseWriter, r *http.Request) {
 
 func system_fs_clearVersionHistories() {
 	for _, fsh := range fsHandlers {
-		if !fsh.IsVirtual() && !fsh.ReadOnly {
+		if !fsh.ReadOnly {
 			localversion.CleanExpiredVersionBackups(fsh.Path, 30*86400)
 		}
 

+ 54 - 14
mod/filesystem/abstractions/webdavfs/webdavfs.go

@@ -3,6 +3,7 @@ package webdavfs
 import (
 	"errors"
 	"io"
+	"io/fs"
 	"log"
 	"os"
 	"path/filepath"
@@ -119,9 +120,9 @@ func (e WebDAVFileSystem) VirtualPathToRealPath(subpath string, username string)
 	}
 
 	if e.Hierarchy == "user" {
-		return filepath.ToSlash(filepath.Join("users", username, subpath)), nil
+		return filepath.ToSlash(filepath.Clean(filepath.Join("users", username, subpath))), nil
 	} else if e.Hierarchy == "public" {
-		return filepath.ToSlash(subpath), nil
+		return filepath.ToSlash(filepath.Clean(subpath)), nil
 	}
 	return "", errors.New("unsupported filesystem hierarchy")
 
@@ -150,27 +151,66 @@ func (e WebDAVFileSystem) IsDir(filename string) bool {
 //Notes: This is not actual Glob function. This just emulate Glob using ReadDir with max depth 1 layer
 func (e WebDAVFileSystem) Glob(wildcard string) ([]string, error) {
 	wildcard = filepath.ToSlash(filepath.Clean(wildcard))
+
 	if !strings.HasPrefix(wildcard, "/") {
 		//Handle case for listing root, "*"
 		wildcard = "/" + wildcard
 	}
 
-	fileInfos, err := e.c.ReadDir(filterFilepath(filepath.ToSlash(filepath.Clean(filepath.Dir(wildcard)))))
-	if err != nil {
-		return []string{}, err
+	//Get the longest path without *
+	chunksWithoutStar := []string{}
+	chunks := strings.Split(wildcard, "/")
+	for _, chunk := range chunks {
+		if !strings.Contains(chunk, "*") {
+			chunksWithoutStar = append(chunksWithoutStar, chunk)
+		} else {
+			//Cut off
+			break
+		}
 	}
 
-	validFiles := []string{}
-	matchingRule := wildCardToRegexp(wildcard)
-	for _, fileInfo := range fileInfos {
-		thisFullPath := filepath.ToSlash(filepath.Join(filepath.Dir(wildcard), fileInfo.Name()))
-		match, _ := regexp.MatchString(matchingRule, thisFullPath)
-		if match {
-			validFiles = append(validFiles, thisFullPath)
+	if strings.Count(wildcard, "*") <= 1 && strings.Contains(chunks[len(chunks)-1], "*") {
+		//Fast Glob
+		fileInfos, err := e.c.ReadDir(filterFilepath(filepath.ToSlash(filepath.Clean(filepath.Dir(wildcard)))))
+		if err != nil {
+			return []string{}, err
+		}
+
+		validFiles := []string{}
+		matchingRule := wildCardToRegexp(wildcard)
+		for _, thisFileInfo := range fileInfos {
+			thisFileFullpath := filepath.ToSlash(filepath.Join(filepath.Dir(wildcard), thisFileInfo.Name()))
+			match, _ := regexp.MatchString(matchingRule, thisFileFullpath)
+			if match {
+				validFiles = append(validFiles, thisFileFullpath)
+			}
+		}
+		return validFiles, nil
+	} else {
+		//Slow Glob
+		walkRoot := strings.Join(chunksWithoutStar, "/")
+		if !strings.HasPrefix(walkRoot, "/") {
+			walkRoot = "/" + walkRoot
+		}
+
+		allFiles := []string{}
+		e.Walk(walkRoot, func(path string, info fs.FileInfo, err error) error {
+			allFiles = append(allFiles, path)
+			return nil
+		})
+
+		validFiles := []string{}
+		matchingRule := wildCardToRegexp(wildcard) + "$"
+		for _, thisFilepath := range allFiles {
+			match, _ := regexp.MatchString(matchingRule, thisFilepath)
+			if match {
+				validFiles = append(validFiles, thisFilepath)
+			}
 		}
-	}
 
-	return validFiles, nil
+		return validFiles, nil
+
+	}
 }
 func (e WebDAVFileSystem) GetFileSize(filename string) int64 {
 	filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))

+ 2 - 0
mod/filesystem/filesystem.go

@@ -232,6 +232,7 @@ func NewFileSystemHandler(option FileSystemOption) (*FileSystemHandler, error) {
 }
 
 //Check if a fsh is virtual (e.g. Network or fs Abstractions that cannot be listed with normal fs API)
+/*
 func (fsh *FileSystemHandler) IsVirtual() bool {
 	if fsh.Hierarchy == "virtual" || fsh.Filesystem == "webdav" {
 		//Check if the config return placeholder
@@ -246,6 +247,7 @@ func (fsh *FileSystemHandler) IsVirtual() bool {
 	}
 	return false
 }
+*/
 
 func (fsh *FileSystemHandler) IsRootOf(vpath string) bool {
 	return strings.HasPrefix(vpath, fsh.UUID+":")

+ 94 - 6
mod/share/share.go

@@ -24,6 +24,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -951,9 +952,9 @@ func (s *Manager) HandleEditShare(w http.ResponseWriter, r *http.Request) {
 
 func (s *Manager) HandleDeleteShare(w http.ResponseWriter, r *http.Request) {
 	//Get the vpath from paramters
-	vpath, err := mv(r, "path", true)
+	uuid, err := mv(r, "uuid", true)
 	if err != nil {
-		sendErrorResponse(w, "Invalid path given")
+		sendErrorResponse(w, "Invalid uuid given")
 		return
 	}
 
@@ -965,7 +966,7 @@ func (s *Manager) HandleDeleteShare(w http.ResponseWriter, r *http.Request) {
 	}
 
 	//Delete the share setting
-	err = s.DeleteShare(userinfo, vpath)
+	err = s.DeleteShareByUUID(userinfo, uuid)
 
 	if err != nil {
 		sendErrorResponse(w, err.Error())
@@ -995,7 +996,7 @@ func (s *Manager) HandleListAllShares(w http.ResponseWriter, r *http.Request) {
 
 		}
 	} else {
-		//List fsh onlya
+		//List fsh only
 		targetFsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(fshId)
 		if err != nil {
 			common.SendErrorResponse(w, err.Error())
@@ -1009,10 +1010,70 @@ func (s *Manager) HandleListAllShares(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	js, _ := json.Marshal(results)
+	//Reduce the data
+	type Share struct {
+		UUID                 string
+		FileVirtualPath      string
+		Owner                string
+		Permission           string
+		IsFolder             bool
+		IsOwnerOfShare       bool
+		CanAccess            bool
+		CanOpenInFileManager bool
+		CanDelete            bool
+	}
+
+	reducedResult := []*Share{}
+	for _, result := range results {
+		permissionText := result.Permission
+		if result.Permission == "groups" || result.Permission == "users" {
+			permissionText = permissionText + " (" + strings.Join(result.Accessibles, ", ") + ")"
+		}
+		thisShareInfo := Share{
+			UUID:                 result.UUID,
+			FileVirtualPath:      result.FileVirtualPath,
+			Owner:                result.Owner,
+			Permission:           permissionText,
+			IsFolder:             result.IsFolder,
+			IsOwnerOfShare:       userinfo.Username == result.Owner,
+			CanAccess:            result.IsAccessibleBy(userinfo.Username, userinfo.GetUserPermissionGroupNames()),
+			CanOpenInFileManager: s.UserCanOpenShareInFileManager(result, userinfo),
+			CanDelete:            s.CanModifyShareEntry(userinfo, result.FileVirtualPath),
+		}
+
+		reducedResult = append(reducedResult, &thisShareInfo)
+	}
+
+	js, _ := json.Marshal(reducedResult)
 	common.SendJSONResponse(w, string(js))
 }
 
+/*
+	Check if the user can open the share in File Manager
+
+	There are two conditions where the user can open the file in file manager
+	1. If the user is the owner of the file
+	2. If the user is NOT the owner of the file but the target fsh is public accessible and in user's fsh list
+*/
+func (s *Manager) UserCanOpenShareInFileManager(share *shareEntry.ShareOption, userinfo *user.User) bool {
+	if share.Owner == userinfo.Username {
+		return true
+	}
+
+	fsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(share.FileVirtualPath)
+	if err != nil {
+		//User do not have permission to access this fsh
+		return false
+	}
+
+	rpath, _ := fsh.FileSystemAbstraction.VirtualPathToRealPath(share.FileVirtualPath, userinfo.Username)
+	if fsh.Hierarchy == "public" && fsh.FileSystemAbstraction.FileExists(rpath) {
+		return true
+	}
+
+	return false
+}
+
 //Craete a new file or folder share
 func (s *Manager) CreateNewShare(userinfo *user.User, srcFsh *filesystem.FileSystemHandler, vpath string) (*shareEntry.ShareOption, error) {
 	//Translate the vpath to realpath
@@ -1085,6 +1146,10 @@ func (s *Manager) ListAllShareByFshId(fshId string, userinfo *user.User) []*shar
 		return true
 	})
 
+	sort.Slice(results, func(i, j int) bool {
+		return results[i].UUID < results[j].UUID
+	})
+
 	return results
 }
 
@@ -1153,10 +1218,20 @@ func (s *Manager) CanModifyShareEntry(userinfo *user.User, vpath string) bool {
 		return true
 	}
 
+	//Public fsh where the user and owner both can access
+	fsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(vpath)
+	if err != nil {
+		return false
+	}
+	rpath, _ := fsh.FileSystemAbstraction.VirtualPathToRealPath(vpath, userinfo.Username)
+	if fsh.Hierarchy == "public" && fsh.FileSystemAbstraction.FileExists(rpath) {
+		return true
+	}
+
 	return false
 }
 
-func (s *Manager) DeleteShare(userinfo *user.User, vpath string) error {
+func (s *Manager) DeleteShareByVpath(userinfo *user.User, vpath string) error {
 	ps := getPathHashFromUsernameAndVpath(userinfo, vpath)
 
 	if !s.CanModifyShareEntry(userinfo, vpath) {
@@ -1165,6 +1240,19 @@ func (s *Manager) DeleteShare(userinfo *user.User, vpath string) error {
 	return s.options.ShareEntryTable.DeleteShareByPathHash(ps)
 }
 
+func (s *Manager) DeleteShareByUUID(userinfo *user.User, uuid string) error {
+	so := s.GetShareObjectFromUUID(uuid)
+	if so == nil {
+		return errors.New("Invalid share uuid")
+	}
+
+	if !s.CanModifyShareEntry(userinfo, so.FileVirtualPath) {
+		return errors.New("Permission denied")
+	}
+
+	return s.options.ShareEntryTable.DeleteShareByUUID(uuid)
+}
+
 func (s *Manager) GetShareUUIDFromUserAndVpath(userinfo *user.User, vpath string) string {
 	ps := getPathHashFromUsernameAndVpath(userinfo, vpath)
 	return s.options.ShareEntryTable.GetShareUUIDFromPathHash(ps)

+ 40 - 16
mod/share/shareEntry/shareEntry.go

@@ -29,14 +29,14 @@ type ShareEntryTable struct {
 }
 
 type ShareOption struct {
-	UUID             string
-	PathHash         string //Path Hash, the key for loading a share from vpath and fsh specific config
-	FileVirtualPath  string
-	FileRealPath     string
-	Owner            string
-	Accessibles      []string //Use to store username or group names if permission is groups or users
-	Permission       string   //Access permission, allow {anyone / signedin / samegroup / groups / users}
-	AllowLivePreview bool
+	UUID            string
+	PathHash        string //Path Hash, the key for loading a share from vpath and fsh specific config
+	FileVirtualPath string
+	FileRealPath    string
+	Owner           string
+	Accessibles     []string //Use to store username or group names if permission is groups or users
+	Permission      string   //Access permission, allow {anyone / signedin / samegroup / groups / users}
+	IsFolder        bool
 }
 
 func NewShareEntryTable(db *database.Database) *ShareEntryTable {
@@ -93,14 +93,14 @@ func (s *ShareEntryTable) CreateNewShare(srcFsh *filesystem.FileSystemHandler, v
 
 		//Create a share object
 		shareOption := ShareOption{
-			UUID:             shareUUID,
-			PathHash:         sharePathHash,
-			FileVirtualPath:  vpath,
-			FileRealPath:     rpath,
-			Owner:            username,
-			Accessibles:      usergroups,
-			Permission:       "anyone",
-			AllowLivePreview: true,
+			UUID:            shareUUID,
+			PathHash:        sharePathHash,
+			FileVirtualPath: vpath,
+			FileRealPath:    rpath,
+			Owner:           username,
+			Accessibles:     usergroups,
+			Permission:      "anyone",
+			IsFolder:        srcFsh.FileSystemAbstraction.IsDir(rpath),
 		}
 
 		//Store results on two map to make sure O(1) Lookup time
@@ -141,6 +141,30 @@ func (s *ShareEntryTable) DeleteShareByPathHash(pathhash string) error {
 
 }
 
+func (s *ShareEntryTable) DeleteShareByUUID(uuid string) error {
+	//Check if the share already exists. If yes, use the previous link
+	val, ok := s.UrlToFileMap.Load(uuid)
+	if ok {
+		//Exists. Send back the old share url
+		so := val.(*ShareOption)
+
+		//Remove this from the database
+		err := s.Database.Delete("share", so.UUID)
+		if err != nil {
+			return err
+		}
+
+		//Remove this form the current sync map
+		s.FileToUrlMap.Delete(so.PathHash)
+		s.UrlToFileMap.Delete(uuid)
+		return nil
+
+	} else {
+		//Already deleted from buffered record.
+		return nil
+	}
+}
+
 func (s *ShareEntryTable) GetShareUUIDFromPathHash(pathhash string) string {
 	shareObject := s.GetShareObjectFromPathHash(pathhash)
 	if shareObject == nil {

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

@@ -242,10 +242,9 @@ func (a aofs) pathRewrite(path string) (string, *fs.FileSystemHandler, error) {
 		fsHandlers := a.userinfo.GetAllFileSystemHandler()
 		for _, fsh := range fsHandlers {
 			//Create a folder representation for this virtual directory
-			if !(fsh.Hierarchy == "backup" || fsh.IsVirtual()) {
+			if !(fsh.Hierarchy == "backup") {
 				os.Mkdir(a.tmpFolder+fsh.UUID, 0755)
 			}
-
 		}
 
 		readmeContent, err := ioutil.ReadFile("./system/ftp/README.txt")

+ 17 - 18
storage.go

@@ -102,24 +102,23 @@ func LoadBaseStoragePool() error {
 		DEBUG REMOVE AFTERWARD
 
 	*/
-	/*
-		webdh, err := fs.NewFileSystemHandler(fs.FileSystemOption{
-			Name:       "Loopback",
-			Uuid:       "loopback",
-			Path:       "http://192.168.1.214:8081/webdav/user",
-			Access:     "readwrite",
-			Hierarchy:  "public",
-			Automount:  false,
-			Filesystem: "webdav",
-			Username:   "TC",
-			Password:   "password",
-		})
-		if err != nil {
-			log.Println(err.Error())
-		} else {
-			fsHandlers = append(fsHandlers, webdh)
-		}
-	*/
+
+	webdh, err := fs.NewFileSystemHandler(fs.FileSystemOption{
+		Name:       "Loopback",
+		Uuid:       "loopback",
+		Path:       "http://192.168.1.214:8081/webdav/user",
+		Access:     "readwrite",
+		Hierarchy:  "public",
+		Automount:  false,
+		Filesystem: "webdav",
+		Username:   "TC",
+		Password:   "password",
+	})
+	if err != nil {
+		log.Println(err.Error())
+	} else {
+		fsHandlers = append(fsHandlers, webdh)
+	}
 
 	//Load all the storage config from file
 	rawConfig, err := ioutil.ReadFile(*storage_config_file)

+ 16 - 2
web/SystemAO/file_system/file_explorer.html

@@ -1116,9 +1116,11 @@
             <div class="item noSelectionOnly" onclick="showCurrentDirectoryProperties();">
                 <i class="notice icon"></i> <span locale="contextmenu/properties">Properties</span>
             </div>
+            <div class="item vrootonly" onclick="showSharesManager();">
+                <i class="share alternate icon"></i> <span locale="contextmenu/share">Open Shares Manager</span>
+            </div>
             <!-- This properties show the vroot properties -->
-             <!-- This properties shows the currentDirectory properties instead of the selected file properties-->
-             <div class="item vrootonly" onclick="showVrootProperties();">
+            <div class="item vrootonly" onclick="showVrootProperties();">
                 <i class="notice icon"></i> <span locale="contextmenu/properties">Properties</span>
             </div>
         </div>
@@ -4577,6 +4579,18 @@
             });
         }
 
+        function showSharesManager(){
+            var rootname = [$("#storageroot").find(".dir.item.active").attr("filepath")];
+            var hashPassthrough = encodeURIComponent(JSON.stringify(rootname));
+            ao_module_newfw({
+                url: "SystemAO/file_system/sharelist.html#" + hashPassthrough,
+                width: 420,
+                height: 580,
+                appicon: "img/system/share.svg",
+                title: "Shares Manager"
+            });
+        }
+
         function openBackupManager(){
             var rootname = [$("#storageroot").find(".dir.item.active").attr("filepath")];
             var hashPassthrough = encodeURIComponent(JSON.stringify(rootname));

+ 172 - 0
web/SystemAO/file_system/sharelist.html

@@ -0,0 +1,172 @@
+<html>
+    <head>
+        <title locale="title/title">Share Entry List</title>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
+        <link rel="stylesheet" href="../../script/semantic/semantic.css">
+        <script type="text/javascript" src="../../script/jquery.min.js"></script>
+        <script type="text/javascript" src="../../script/semantic/semantic.min.js"></script>
+        <script type="text/javascript" src="../../script/ao_module.js"></script>
+        <script type="text/javascript" src="../../script/applocale.js"></script>
+        <script type="text/javascript" src="../../script/clipboard.min.js"></script>
+        <style>
+
+        </style>
+    </head>
+    <body id="filePropertiesWindow">
+        <br>
+        <div class="ui container">
+            <h3 class="ui header">
+                <i class="share alternate icon"></i>
+                <div class="content">
+                    <span locale="title/title">Share Entries</span> <span id="vrootname"></span>
+                    <div class="sub header" locale="title/desc">Shared files in this drive</div>
+                </div>
+            </h3>
+            <div class="ui divider"></div>
+            <div id="succ" style="display:none;" class="ui green message">
+                <i class="ui checkmark icon"></i> <span id="msg" locale="message/removed">Share Removed</span>
+            </div>
+            <div style="max-height: calc(100vh - 120px); overflow-y: auto;">
+                <table class="ui very basic fluid celled compact table unstackable">
+                    <tbody id="shares">
+                      <tr>
+                        <td>
+                          <h4 class="ui header">
+                                <div class="content">
+                                <span locale="message/noshare/title">No Shares</span>
+                                <div locale="message/noshare/desc" class="sub header">Try select a file using File Manager and right click share</div>
+                            </div>
+                            </h4>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+            </div>
+            <br>
+        </div>
+        <script>
+            //Get fsh id from hash if exists
+            let fshId = "";
+            if (window.location.hash.length > 1){
+                var fshIds = window.location.hash.substr(1);
+                fshIds = JSON.parse(decodeURIComponent(fshIds));
+                fshId = fshIds[0];
+                $("#vrootname").text("(" + fshId + ")");
+            }
+
+            applocale.init("../../SystemAO/locale/sharelist.json", function(){
+                applocale.translate();
+                listSharedItems();
+            });
+
+            
+            function listSharedItems(){
+                $("#shares").html("");
+                $.get("../../system/file_system/share/list?fsh=" + fshId, function(data){
+                    console.log(data);
+                    data.forEach(function(entry){
+                        let filename = entry.FileVirtualPath.split("/").pop();
+                        let port = window.location.port;
+                        if (window.location.port == ""){
+                            port = "";
+                        }
+
+                        let openShareButton = ` <a title="Open Share" href="/share/${entry.UUID}" target="_blank" class="ui icon basic button"><i class="external icon"></i></a>`;
+                        if (!entry.CanAccess){
+                            openShareButton = "";
+                        }
+                        let openButton = `<button title="Open in File Manager" path="${entry.FileVirtualPath}" isfolder="${entry.IsFolder}" onclick="openThis(this);" class="ui icon basic button"><i class="folder open icon"></i></button>`;
+                        if (!entry.CanOpenInFileManager){
+                            openButton = "";
+                        }
+                        let deleteButton = `<button title="Delete Share" uuid="${entry.UUID}" onclick="deleteShare(this);" class="ui red icon button"><i class="trash icon"></i></button>`;
+                        if (!entry.CanDelete){
+                            deleteButton = "";
+                        }
+
+                        $("#shares").append(`
+                            <tr>
+                                <td>
+                                    <h4 class="ui header">
+                                        <div class="content">
+                                            <span>${filename} </span>
+                                            <div class="sub header">${applocale.getString("item/creator", "Creator: ")} ${entry.Owner} / ${applocale.getString("item/perm", "Permission: ")} ${entry.Permission} / <span class="linkCopier" style="cursor:pointer; color: #3452eb;" title="Copy Link" data-clipboard-text="${window.location.protocol + '//' + window.location.hostname + ":" + port + "/share/" + entry.UUID}"><i class="linkify icon"></i></span>
+                                        </div>
+                                    </h4>
+                                </td>
+                                <td style="padding-right: 0.6em;">
+                                    <div class="ui small vertical buttons">
+                                        ${openShareButton}
+                                        ${openButton}
+                                        ${deleteButton}
+                                    </div>
+                                </td>
+                       </tr>`);
+                    });
+
+                    var clipboard = new ClipboardJS('.linkCopier');
+                    clipboard.on('success', function(e) {
+                        //console.info('Action:', e.action);
+                        // console.info('Text:', e.text);
+                        // console.info('Trigger:', e.trigger);
+                        let originalContent =  $(e.trigger).html();
+                        $(e.trigger).html(`<i class="ui green checkmark icon"></i>`);
+                        $(e.trigger).css("pointer-events", "none");
+                        setTimeout(function(){
+                            $(e.trigger).html(originalContent);
+                            $(e.trigger).css("pointer-events", "auto");
+                        }, 1500);
+                        e.clearSelection();
+                    });
+
+                    if (data.length == 0){
+                        $("#shares").html(`<tr>
+                        <td>
+                          <h4 class="ui header">
+                                <div class="content">
+                                <span locale="message/noshare/title">No Shares</span>
+                                <div locale="message/noshare/desc" class="sub header">Try select a file using File Manager and right click share</div>
+                            </div>
+                            </h4>
+                        </td>
+                      </tr>`);
+                    }
+
+                    applocale.translate();
+                });
+            }
+
+            function openThis(object){
+                var vpath = $(object).attr("path");
+                var isFolder = $(object).attr("isfolder") == "true";
+                let openingPath = vpath;
+                if (isFolder){
+                    ao_module_openPath(vpath);
+                }else{
+                    let c = vpath.split("/");
+                    let filename = c.pop();
+                    let folderpath = c.join("/");
+                    ao_module_openPath(folderpath, filename);
+                }
+                
+            }
+
+            function deleteShare(object){
+                let deleteUUID = $(object).attr("uuid");
+                if (confirm(applocale.getString("message/delwarning", "All collaborators will lose access to this file via File Share interface. Confirm?"))){
+                    $.ajax({
+                        url: "../../system/file_system/share/delete",
+                        method: "POST",
+                        data: {uuid: deleteUUID},
+                        success: function(data){
+                            console.log(data);
+                            listSharedItems();
+                            $("#succ").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
+                        }
+                    });
+                }
+            }
+        </script>
+    </body>
+</html> 

+ 1 - 0
web/SystemAO/locale/disk_properties.json

@@ -36,6 +36,7 @@
         },
         "zh-hk": {
             "fwtitle" : "磁碟概覽",
+            "fontFamily":"\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
             "strings":{
                 "title/title":"磁碟概覽",
                 "title/desc":"關於此虛擬儲存裝置對應之磁碟機掛載點",

+ 2 - 0
web/SystemAO/locale/file_versions.json

@@ -4,6 +4,7 @@
     "keys": {
         "zh-tw": {
             "fwtitle" : "檔案版本管理",
+            "fontFamily":"\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
             "strings":{
                 "title/title": "檔案版本管理",
                 "title/desc":"此檔案的舊版本將會自動於 30 天後刪除。同時間你也可以手動從下方列表中對版本備份進行刪除。",
@@ -32,6 +33,7 @@
         },
         "zh-hk": {
             "fwtitle" : "檔案版本管理",
+            "fontFamily":"\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
             "strings":{
                 "title/title": "檔案版本管理",
                 "title/desc":"此檔案的舊版本將會自動於 30 天後刪除。同時間你也可以手動從下方列表中對版本備份進行刪除。",

+ 80 - 0
web/SystemAO/locale/sharelist.json

@@ -0,0 +1,80 @@
+{
+    "author": "tobychui",
+    "version": "1.0",
+    "keys": {
+        "zh-tw": {
+            "fwtitle" : "分享管理員",
+            "fontFamily":"\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+            "strings":{
+                "title/title": "分享管理員",
+                "title/desc":"在此虛擬磁碟內的分享連結",
+                "message/noshare/title":"沒有分享連結",
+                "message/noshare/desc":"此虛擬磁碟內並沒有已分享之連結。試試從檔案管理員選擇一個檔案並點擊「分享」?",
+                "message/delwarning": "所有協作者將無法通過檔案共享界面訪問此文件。確認?",
+                "message/removed": "已移除分享",
+                "item/creator":"創建者:",
+                "item/perm":"存取權限:",
+
+                "":""
+            },
+            "titles":{
+                "Copy Link": "複製連結",
+                "Open Share":"打開分享頁",
+                "Open in File Manager": "在檔案管理員打開",
+                "Delete Share": "刪除此分享"
+            },
+            "placeholder":{
+
+            }
+        },
+        "zh-hk": {
+            "fwtitle" : "分享管理員",
+            "fontFamily":"\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+            "strings":{
+                "title/title": "分享管理員",
+                "title/desc":"在此虛擬磁碟內的分享連結",
+                "message/noshare/title":"沒有分享連結",
+                "message/noshare/desc":"此虛擬磁碟內並沒有已分享之連結。試試從檔案管理員選擇一個檔案並點擊「分享」?",
+                "message/delwarning": "所有協作者將無法通過檔案共享界面訪問此文件。確認?",
+                "message/removed": "已移除分享",
+                "item/creator":"創建者:",
+                "item/perm":"存取權限:",
+
+                "":""
+            },
+            "titles":{
+                "Copy Link": "複製連結",
+                "Open Share":"打開分享頁",
+                "Open in File Manager": "在檔案管理員打開",
+                "Delete Share": "刪除此分享"
+            },
+            "placeholder":{
+
+            }
+        },
+        "zh-cn": {
+            "fwtitle" : "分享管理员",
+            "strings":{
+                "title/title": "分享管理员",
+                "title/desc":"在此虚拟磁碟内的分享连结",
+                "message/noshare/title":"没有分享连结",
+                "message/noshare/desc":"此虚拟磁碟内并没有已分享之连结。试试从档案管理员选择一个档案并点击「分享」? ",
+                "message/delwarning": "所有协作者将无法通过档案共享界面访问此文件。确认?",
+                "message/removed": "已移除分享",
+                "item/creator":"创建者:",
+                "item/perm":"存取权限:",
+
+                "":""
+            },
+            "titles":{
+                "Copy Link": "拷贝連結",
+                "Open Share":"打开分享页",
+                "Open in File Manager": "在档案管理员打开",
+                "Delete Share": "删除此分享"
+            },
+            "placeholder":{
+
+            }
+        }
+    }
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 27 - 0
web/img/system/share.ai


+ 11 - 0
web/img/system/share.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<circle fill="#5EC0BB" cx="91.092" cy="35.646" r="16.28"/>
+<circle fill="#5EC0BB" cx="32.737" cy="64.732" r="16.281"/>
+<circle fill="#5EC0BB" cx="91.092" cy="93.086" r="16.28"/>
+<line fill="none" stroke="#5EC0BB" stroke-width="8" stroke-miterlimit="10" x1="91.092" y1="35.646" x2="33.652" y2="64.732"/>
+<line fill="none" stroke="#5EC0BB" stroke-width="8" stroke-miterlimit="10" x1="32.737" y1="64.732" x2="92.372" y2="93.086"/>
+</svg>

+ 8 - 1
web/script/applocale.js

@@ -38,7 +38,7 @@ var applocale = {
 
                 if (data.keys[applocale.lang] != undefined && data.keys[applocale.lang].fontFamily != undefined){
                     //This language has a prefered font family. Inject it
-                    $("h1, h2, h3, p, span, div, span").css({
+                    $("h1, h2, h3, p, span, div, a").css({
                         "font-family":data.keys[applocale.lang].fontFamily
                     });
                     console.log("[Applocale] Updating font family to: ", data.keys[applocale.lang].fontFamily)
@@ -83,6 +83,13 @@ var applocale = {
                 }
             }
         })
+
+        if (applocale.localData.keys[applocale.lang] != undefined && applocale.localData.keys[applocale.lang].fontFamily != undefined){
+            //This language has a prefered font family. Inject it
+            $("h1, h2, h3, p, span, div, a").css({
+                "font-family":applocale.localData.keys[applocale.lang].fontFamily
+            });
+        }
     },
     getString: function(key, original, type = "strings") {
         var targetLang = this.lang;

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff